Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e7829ba
style: RiskAnalysisSummarySection의 마진 및 패딩 조정
Dobbymin Aug 22, 2025
c10ad77
Merge branch 'main' of https://github.com/BusanHackathon/lighthouse-f…
Dobbymin Aug 22, 2025
4b1e0cc
style: RiskFactorsBox 컴포넌트의 텍스트 색상 및 폰트 스타일 수정
Dobbymin Aug 22, 2025
efa8619
feat: 주소 입력 필드 및 관련 컴포넌트 추가 (주소, 상세 주소, 예치금, 주택 유형)
Dobbymin Aug 22, 2025
0afc278
feat: 임대 유형 선택 필드 컴포넌트 추가
Dobbymin Aug 22, 2025
3a18633
refactor: RENT_TYPES 상수 제거 및 관련 타입 정리
Dobbymin Aug 22, 2025
b2a22e9
feat: 주소 검색을 위한 유효성 검사 스키마 추가 (주소, 주거형태, 상세주소, 보증금)
Dobbymin Aug 22, 2025
fee9547
feat: @radix-ui/react-select 패키지 추가 및 pnpm-lock.yaml 업데이트
Dobbymin Aug 22, 2025
fddbd15
feat: 스키마 모듈 추가 및 인덱스 파일 업데이트
Dobbymin Aug 22, 2025
5cd0c3e
refactor: RENT_TYPES 및 관련 상수 제거, 보증금 관련 상수 통합
Dobbymin Aug 22, 2025
1e71aa9
feat: Select 컴포넌트 및 관련 하위 컴포넌트 추가
Dobbymin Aug 22, 2025
ecd59a6
style: RiskChartBox 컴포넌트의 패딩 수정
Dobbymin Aug 22, 2025
6add218
feat: 진단 폼 컴포넌트 리팩토링 및 react-hook-form 통합
Dobbymin Aug 22, 2025
efb776f
style: ReasonBox 컴포넌트의 텍스트 색상 변경
Dobbymin Aug 22, 2025
d6c1f7c
feat: 주소 관련 필드 컴포넌트 추가 (주소 필드, 주거형태 필드, 상세주소 필드, 보증금 필드, 임대 유형 필드)
Dobbymin Aug 22, 2025
96ea367
feat: Select 컴포넌트에 선택 기능 추가 및 스타일 수정
Dobbymin Aug 22, 2025
8980aca
feat: 주택 유형 상수에 선택 옵션 추가
Dobbymin Aug 22, 2025
0635573
feat: 주택 유형 필드에 Select 컴포넌트 적용 및 옵션 표시 기능 추가
Dobbymin Aug 22, 2025
f476d68
feat: 진단 API 구현 및 요청 처리 로직 추가
Dobbymin Aug 22, 2025
89304cc
feat: 진단 폼에서 API 요청을 위한 뮤테이션 추가 및 제출 로직 수정
Dobbymin Aug 22, 2025
763da62
feat: 주택 유형 상수에서 HOUSE_TYPES를 HOUSE_TYPE_OPTIONS로 변경 및 HouseType 타입 수정
Dobbymin Aug 22, 2025
5029270
feat: 주택 유형 상수에서 '복층'을 '기타'로 변경 및 HOUSE_TYPE_OPTIONS의 값 수정
Dobbymin Aug 22, 2025
db8ee24
feat: 진단 폼 기능 구현
Dobbymin Aug 22, 2025
e0e8011
style: AlternativeSection, LandlordPropertySection, LandlordReliabili…
Dobbymin Aug 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.12",
"@tanstack/react-query": "^5.85.3",
Expand Down
396 changes: 394 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions src/features/main/apis/diagnosis.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { fetchInstance } from '@/shared';

import type { HouseType } from '../types';

export const DIAGNOSIS_API_PATH = '/api/diagnosis';

interface DiagnosisApiResponse {
address: string;
addressDetail: string;
houseType: HouseType;
deposit: number;
}

export const diagnosisApi = async ({
address,
addressDetail,
houseType,
deposit,
}: DiagnosisApiResponse) => {
const response = await fetchInstance.post<DiagnosisApiResponse>(DIAGNOSIS_API_PATH, {
address,
addressDetail,
houseType,
deposit,
});

return response.data;
};
Comment on lines +7 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

DiagnosisApiResponse라는 인터페이스 이름이 요청 페이로드와 API 응답에 모두 사용될 수 있어 혼란을 줄 수 있습니다. 요청 데이터를 나타내는 타입은 DiagnosisApiPayload 또는 DiagnosisApiRequest와 같이 더 명확한 이름으로 변경하는 것이 좋습니다. 또한, diagnosisApi 함수는 인자로 받은 객체를 그대로 post 메소드에 전달하도록 단순화할 수 있습니다.

interface DiagnosisApiPayload {
  address: string;
  addressDetail: string;
  houseType: HouseType;
  deposit: number;
}

export const diagnosisApi = async (payload: DiagnosisApiPayload) => {
  const response = await fetchInstance.post<DiagnosisApiPayload>(DIAGNOSIS_API_PATH, payload);

  return response.data;
};

1 change: 1 addition & 0 deletions src/features/main/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './diagnosis.api';
23 changes: 23 additions & 0 deletions src/features/main/components/common/AddressField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useFormContext } from 'react-hook-form';

import { FormField, FormItem, FormLabel, FormMessage, Input } from '@/shared';

import { FORM_FIELDS, LABEL_TEXTS, PLACEHOLDER_TEXTS } from '../../constants';
import { type SearchAddressType } from '../../model';

export const AddressField = () => {
const form = useFormContext<SearchAddressType>();
return (
<FormField
control={form.control}
name='address'
render={({ field }) => (
<FormItem className='flex flex-col gap-2'>
<FormLabel htmlFor={FORM_FIELDS.ADDRESS}>{LABEL_TEXTS.ADDRESS}</FormLabel>
<Input {...field} placeholder={PLACEHOLDER_TEXTS.ADDRESS} />
<FormMessage />
</FormItem>
)}
/>
);
};
43 changes: 43 additions & 0 deletions src/features/main/components/common/DepositField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useFormContext } from 'react-hook-form';

import { FormField, FormItem, FormMessage, Input } from '@/shared';

import { PLACEHOLDER_TEXTS } from '../../constants';
import type { SearchAddressType } from '../../model';

export const DepositField = () => {
const form = useFormContext<SearchAddressType>();

const convertToNumber = (value: string, onChange: (value: number) => void) => {
const numValue = value === '' ? 0 : Number(value);
const safeValue = isNaN(numValue) ? 0 : numValue;
onChange(safeValue);
};

const getDisplayValue = (value: number) => {
return value === 0 ? '' : String(value);
};

return (
<FormField
control={form.control}
name='deposit'
render={({ field }) => (
<FormItem className='flex flex-col gap-2'>
<div className='relative'>
<Input
{...field}
placeholder={PLACEHOLDER_TEXTS.DEPOSIT}
onChange={(e) => convertToNumber(e.target.value, field.onChange)}
value={getDisplayValue(field.value)}
/>
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-[#333D4B]'>
</span>
Comment on lines +34 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

text-[#333D4B]와 같이 색상 값이 하드코딩되어 있습니다. 유지보수성과 일관성을 위해 tailwind.config.js에 정의된 테마 색상(예: text-gray-800)을 사용하는 것이 좋습니다.

Suggested change
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-[#333D4B]'>
</span>
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-gray-800'>
</span>

</div>
<FormMessage />
</FormItem>
)}
/>
);
};
23 changes: 23 additions & 0 deletions src/features/main/components/common/DetailAddressField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useFormContext } from 'react-hook-form';

import { FormField, FormItem, FormLabel, FormMessage, Input } from '@/shared';

import { FORM_FIELDS, LABEL_TEXTS, PLACEHOLDER_TEXTS } from '../../constants';
import type { SearchAddressType } from '../../model';

export const DetailAddressField = () => {
const form = useFormContext<SearchAddressType>();
return (
<FormField
control={form.control}
name='detailAddress'
render={({ field }) => (
<FormItem className='flex flex-col gap-2'>
<FormLabel htmlFor={FORM_FIELDS.DETAIL_ADDRESS}>{LABEL_TEXTS.DETAIL_ADDRESS}</FormLabel>
<Input {...field} placeholder={PLACEHOLDER_TEXTS.DETAIL_ADDRESS} />
<FormMessage />
</FormItem>
)}
/>
);
};
53 changes: 53 additions & 0 deletions src/features/main/components/common/HouseTypeField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useFormContext } from 'react-hook-form';

import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '@/shared';

import { HOUSE_TYPE_OPTIONS, LABEL_TEXTS } from '../../constants';
import type { SearchAddressType } from '../../model';

export const HouseTypeField = () => {
const form = useFormContext<SearchAddressType>();

return (
<FormField
control={form.control}
name='houseType'
render={({ field }) => (
<FormItem className='flex flex-col gap-2'>
<FormLabel>{LABEL_TEXTS.HOUSE_TYPE}</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger className='w-full rounded-full border-none shadow-[0px_4px_30px_0px_#0000001A] focus-visible:border-lighthouse-blue focus-visible:shadow-lighthouse-blue-shadow [&_span]:text-input-placeholder-gray'>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

SelectTrigger에 적용된 shadow-[0px_4px_30px_0px_#0000001A] 클래스는 src/shared/components/ui/select.tsxSelectTrigger 컴포넌트에 이미 기본으로 적용되어 있습니다. 중복된 클래스이므로 제거하는 것이 좋습니다.

Suggested change
<SelectTrigger className='w-full rounded-full border-none shadow-[0px_4px_30px_0px_#0000001A] focus-visible:border-lighthouse-blue focus-visible:shadow-lighthouse-blue-shadow [&_span]:text-input-placeholder-gray'>
<SelectTrigger className='w-full rounded-full border-none focus-visible:border-lighthouse-blue focus-visible:shadow-lighthouse-blue-shadow [&_span]:text-input-placeholder-gray'>

<SelectValue placeholder='주택유형을 선택해주세요' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectGroup>
<SelectLabel>{LABEL_TEXTS.HOUSE_TYPE}</SelectLabel>
{HOUSE_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
);
};
22 changes: 22 additions & 0 deletions src/features/main/components/common/RentTypeField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Label, RadioGroup, RadioGroupItem } from '@/shared';

export const RentTypeField = () => {
return (
<div className='flex flex-col gap-2'>
<Label className='text-sm font-medium'>임대 유형</Label>
<RadioGroup value='jeonse' className='flex items-center gap-4'>
<div className='flex items-center gap-2'>
<RadioGroupItem
value='jeonse'
id='jeonse'
checked
className='data-[state=checked]:border-[#8A8A8A] data-[state=checked]:before:bg-[#4353FF]'
/>
<Label htmlFor='jeonse' className='text-sm'>
전세
</Label>
</div>
</RadioGroup>
</div>
);
};
Comment on lines +3 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

RentTypeField 컴포넌트가 현재 정적인 상태로 구현되어 있습니다.

  1. RadioGroupvalue가 'jeonse'로 하드코딩되어 있고, RadioGroupItemchecked 속성이 있어 사용자가 다른 옵션을 선택할 수 없습니다. 현재는 '전세'만 지원하지만, 컴포넌트의 이름(RentTypeField)을 고려할 때 향후 확장을 위해 react-hook-form과 연동되어야 합니다. 만약 '전세'만 표시하는 것이 목적이라면, RadioGroup 대신 단순 텍스트로 표시하는 것이 더 명확할 수 있습니다.
  2. classNameborder-[#8A8A8A]before:bg-[#4353FF] 같은 색상 값이 하드코딩되어 있습니다. 테마 일관성을 위해 tailwind.config.js에 정의된 값을 사용해주세요.

5 changes: 5 additions & 0 deletions src/features/main/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ export * from './ChartLineItem';
export * from './LegendLine';
export * from './Needle';
export * from './Pagination';
export * from './AddressField';
export * from './HouseTypeField';
export * from './DetailAddressField';
export * from './DepositField';
export * from './RentTypeField';
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const RiskChartBox = ({ riskScore, title }: Props) => {
const needleAngle = getGaugeAngle(riskScore);

return (
<div className='w-full rounded-lg bg-white p-6'>
<div className='w-full rounded-lg bg-white pt-6'>
<h3 className='mb-2 text-2xl font-bold text-gray-900'>{title}</h3>
<p className='text-md mb-6 flex items-center gap-2 text-gray-600'>
<span className='font-semibold'>위험 점수 :</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export const RiskFactorsBox = ({ riskFactors }: Props) => {
<div key={factor.name} className='flex items-center gap-5 py-3 text-center font-semibold'>
<div className='size-1 rounded-full bg-black' />
<span className='flex items-center gap-2 text-gray-900'>
<span className='text-lg text-lighthouse-blue'>{factor.name}</span>
<div className='flex items-center gap-1'>
<span className='font-medium text-red-500'>{factor.percent}</span>
<span className='text-lg text-black'>{factor.name}</span>
<div className='flex items-center gap-1 font-semibold'>
<span className='text-red-500'>{factor.percent}</span>
<span className='text-black'>%</span>
</div>
</span>
Expand Down
117 changes: 53 additions & 64 deletions src/features/main/components/features/form/DiagnosticForm.tsx
Original file line number Diff line number Diff line change
@@ -1,78 +1,67 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';

import { Button, Input, Label, RadioGroup, RadioGroupItem } from '@/shared';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';

import { Button, Form } from '@/shared';

import { diagnosisApi } from '../../../apis';
import { type SearchAddressType, searchAddressSchema } from '../../../model';
import type { HouseType } from '../../../types';
import {
FORM_FIELDS,
LABEL_TEXTS,
PLACEHOLDER_TEXTS,
RENT_TYPES,
RENT_TYPE_CONFIG,
} from '../../../constants';
import type { RentType } from '../../../types';
AddressField,
DepositField,
DetailAddressField,
HouseTypeField,
RentTypeField,
} from '../../common';

export const DiagnosticForm = () => {
const [rentType, setRentType] = useState<RentType>(RENT_TYPES.JEONSE);
const form = useForm<SearchAddressType>({
resolver: zodResolver(searchAddressSchema),
defaultValues: {
address: '',
houseType: '',
detailAddress: '',
deposit: 0,
},
});

const handleRentTypeChange = (value: string) => {
setRentType(value as RentType);
};
const { mutate: diagnosisMutate } = useMutation({
mutationFn: (data: SearchAddressType) =>
diagnosisApi({
address: data.address,
addressDetail: data.detailAddress,
houseType: data.houseType as HouseType,
deposit: data.deposit,
}),
});

const currentRentConfig = RENT_TYPE_CONFIG[rentType];
const onSubmit = (data: SearchAddressType) => {
diagnosisMutate(data);
};

//TODO: 추후 Form 컴포넌트로 리팩토링
return (
<div className='mt-10 flex w-full flex-col items-center gap-2'>
<div className='flex w-4/5 flex-col gap-2'>
<div className='flex w-full flex-col gap-4'>
<div className='flex flex-col gap-2'>
<Label htmlFor={FORM_FIELDS.ADDRESS} className='text-sm font-medium'>
{LABEL_TEXTS.ADDRESS}
</Label>
<Input id={FORM_FIELDS.ADDRESS} placeholder={PLACEHOLDER_TEXTS.ADDRESS} />
</div>
<div className='flex flex-col gap-2'>
<Label htmlFor={FORM_FIELDS.HOUSE_TYPE} className='text-sm font-medium'>
{LABEL_TEXTS.HOUSE_TYPE}
</Label>
<Input id={FORM_FIELDS.HOUSE_TYPE} placeholder={PLACEHOLDER_TEXTS.HOUSE_TYPE} />
<Form {...form}>
<form
onSubmit={(e) => e.preventDefault()}
className='mt-10 flex w-full flex-col items-center gap-2'
>
<div className='flex w-4/5 flex-col gap-2'>
<div className='flex w-full flex-col gap-5'>
<AddressField />
<HouseTypeField />
<DetailAddressField />
<RentTypeField />
<DepositField />
</div>
<div className='flex flex-col gap-2'>
<Label htmlFor={FORM_FIELDS.DETAIL_ADDRESS} className='text-sm font-medium'>
{LABEL_TEXTS.DETAIL_ADDRESS}
</Label>
<Input id={FORM_FIELDS.DETAIL_ADDRESS} placeholder={PLACEHOLDER_TEXTS.DETAIL_ADDRESS} />
<div className='py-10'>
<Button className='w-full' onClick={form.handleSubmit(onSubmit)}>
진단하기
</Button>
</div>
<div className='flex flex-col gap-2'>
<Label className='text-sm font-medium'>{LABEL_TEXTS.DEPOSIT}</Label>
<RadioGroup
value={rentType}
onValueChange={handleRentTypeChange}
className='flex items-center gap-4'
>
{Object.entries(RENT_TYPE_CONFIG).map(([value, config]) => (
<div key={value} className='flex items-center gap-2'>
<RadioGroupItem value={value} id={value} />
<Label htmlFor={value} className='text-sm'>
{config.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='flex flex-col gap-2'>
<div className='relative'>
<Input placeholder={currentRentConfig.placeholder} className='pr-12' />
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-gray-600'>
</span>
</div>
</div>
</div>
<div className='py-10'>
<Button className='w-full'>진단하기</Button>
</div>
</div>
</div>
</form>
Comment on lines +46 to +64
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

폼 제출 로직이 <Button>onClick 이벤트에 연결되어 있습니다. 웹 표준과 접근성을 준수하기 위해 <form>onSubmit 이벤트를 사용하고, 버튼의 typesubmit으로 설정하는 것이 좋습니다. 이렇게 하면 사용자가 엔터 키를 눌러 폼을 제출하는 동작도 자연스럽게 지원됩니다.

Suggested change
<form
onSubmit={(e) => e.preventDefault()}
className='mt-10 flex w-full flex-col items-center gap-2'
>
<div className='flex w-4/5 flex-col gap-2'>
<div className='flex w-full flex-col gap-5'>
<AddressField />
<HouseTypeField />
<DetailAddressField />
<RentTypeField />
<DepositField />
</div>
<div className='flex flex-col gap-2'>
<Label htmlFor={FORM_FIELDS.DETAIL_ADDRESS} className='text-sm font-medium'>
{LABEL_TEXTS.DETAIL_ADDRESS}
</Label>
<Input id={FORM_FIELDS.DETAIL_ADDRESS} placeholder={PLACEHOLDER_TEXTS.DETAIL_ADDRESS} />
<div className='py-10'>
<Button className='w-full' onClick={form.handleSubmit(onSubmit)}>
진단하기
</Button>
</div>
<div className='flex flex-col gap-2'>
<Label className='text-sm font-medium'>{LABEL_TEXTS.DEPOSIT}</Label>
<RadioGroup
value={rentType}
onValueChange={handleRentTypeChange}
className='flex items-center gap-4'
>
{Object.entries(RENT_TYPE_CONFIG).map(([value, config]) => (
<div key={value} className='flex items-center gap-2'>
<RadioGroupItem value={value} id={value} />
<Label htmlFor={value} className='text-sm'>
{config.label}
</Label>
</div>
))}
</RadioGroup>
</div>
<div className='flex flex-col gap-2'>
<div className='relative'>
<Input placeholder={currentRentConfig.placeholder} className='pr-12' />
<span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-gray-600'>
</span>
</div>
</div>
</div>
<div className='py-10'>
<Button className='w-full'>진단하기</Button>
</div>
</div>
</div>
</form>
<form
onSubmit={form.handleSubmit(onSubmit)}
className='mt-10 flex w-full flex-col items-center gap-2'
>
<div className='flex w-4/5 flex-col gap-2'>
<div className='flex w-full flex-col gap-5'>
<AddressField />
<HouseTypeField />
<DetailAddressField />
<RentTypeField />
<DepositField />
</div>
<div className='py-10'>
<Button type='submit' className='w-full'>
진단하기
</Button>
</div>
</div>
</form>

</Form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const ReasonBox = () => {
>
<div className='size-1 rounded-full bg-black' />
<span className='flex items-center gap-2 text-gray-900'>
<span className='text-lg text-lighthouse-blue'>{reason.name}</span>
<span className='text-lg text-black'>{reason.name}</span>
<div className='flex items-center gap-1'>
<span className='font-medium text-red-500'>{reason.percent}</span>
<span className='text-black'>%</span>
Expand Down
Loading
Loading