Conversation
…into feat#41-address-search
There was a problem hiding this comment.
Summary of Changes
Hello @Dobbymin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
이번 PR은 주소 검색 및 진단 폼 기능을 새롭게 구현하고, 관련 UI 컴포넌트들을 개선하는 것을 목표로 합니다. 사용자가 주소, 주택 유형, 상세 주소, 보증금 등의 정보를 입력하여 진단 API를 통해 주택 진단을 받을 수 있도록 하며, 기존 UI 요소들의 일관성과 사용자 경험을 향상시키는 데 중점을 두었습니다.
Highlights
- 주소 검색 및 진단 폼 기능 구현: 주소, 상세 주소, 보증금, 임대 유형, 주택 유형을 입력받는 새로운 필드 컴포넌트들이 추가되었으며, 이를 활용한 진단 폼이 구현되었습니다. 진단 API 연동 및 폼 제출 로직이 포함됩니다.
- UI/UX 개선: Select 컴포넌트의 스타일이 Input 컴포넌트와 일관성 있게 개선되었고, 주택 유형 옵션의 value가 영어로 변경되었습니다. 또한, 여러 섹션 컴포넌트들의 패딩이 조정되어 전반적인 레이아웃이 개선되었습니다.
- 기술적 개선: react-hook-form과 zod를 활용하여 폼 유효성 검사 및 상태 관리가 강화되었으며, TypeScript 타입 안정성이 향상되었습니다. 새로운 API 연동 로직이 추가되었습니다.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
| 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> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
RentTypeField 컴포넌트가 현재 정적인 상태로 구현되어 있습니다.
RadioGroup의value가 'jeonse'로 하드코딩되어 있고,RadioGroupItem에checked속성이 있어 사용자가 다른 옵션을 선택할 수 없습니다. 현재는 '전세'만 지원하지만, 컴포넌트의 이름(RentTypeField)을 고려할 때 향후 확장을 위해react-hook-form과 연동되어야 합니다. 만약 '전세'만 표시하는 것이 목적이라면,RadioGroup대신 단순 텍스트로 표시하는 것이 더 명확할 수 있습니다.className에border-[#8A8A8A]와before:bg-[#4353FF]같은 색상 값이 하드코딩되어 있습니다. 테마 일관성을 위해tailwind.config.js에 정의된 값을 사용해주세요.
| <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> |
There was a problem hiding this comment.
폼 제출 로직이 <Button>의 onClick 이벤트에 연결되어 있습니다. 웹 표준과 접근성을 준수하기 위해 <form>의 onSubmit 이벤트를 사용하고, 버튼의 type을 submit으로 설정하는 것이 좋습니다. 이렇게 하면 사용자가 엔터 키를 눌러 폼을 제출하는 동작도 자연스럽게 지원됩니다.
| <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> |
| import { z } from 'zod'; | ||
|
|
||
| export const searchAddressSchema = z.object({ | ||
| address: z.string().min(1, { message: '주소를 입력해주세요.' }), | ||
| houseType: z.string().min(1, { message: '주거형태를 선택해주세요.' }), | ||
| detailAddress: z.string().min(1, { message: '상세주소를 입력해주세요.' }), | ||
| deposit: z.number().min(1, { message: '보증금을 입력해주세요.' }), | ||
| }); | ||
|
|
||
| export type SearchAddressType = z.infer<typeof searchAddressSchema>; |
There was a problem hiding this comment.
houseType 필드가 z.string()으로 정의되어 있어 타입 안정성이 떨어집니다. HOUSE_TYPE_OPTIONS에 정의된 값들만 허용하도록 z.enum()을 사용하면 타입 캐스팅(as HouseType) 없이 안전하게 타입을 사용할 수 있습니다.
| import { z } from 'zod'; | |
| export const searchAddressSchema = z.object({ | |
| address: z.string().min(1, { message: '주소를 입력해주세요.' }), | |
| houseType: z.string().min(1, { message: '주거형태를 선택해주세요.' }), | |
| detailAddress: z.string().min(1, { message: '상세주소를 입력해주세요.' }), | |
| deposit: z.number().min(1, { message: '보증금을 입력해주세요.' }), | |
| }); | |
| export type SearchAddressType = z.infer<typeof searchAddressSchema>; | |
| import { z } from 'zod'; | |
| import { HOUSE_TYPE_OPTIONS } from '../../constants'; | |
| const houseTypeValues = HOUSE_TYPE_OPTIONS.map((option) => option.value); | |
| export const searchAddressSchema = z.object({ | |
| address: z.string().min(1, { message: '주소를 입력해주세요.' }), | |
| houseType: z.enum(houseTypeValues, { | |
| errorMap: () => ({ message: '주거형태를 선택해주세요.' }), | |
| }), | |
| detailAddress: z.string().min(1, { message: '상세주소를 입력해주세요.' }), | |
| deposit: z.number().min(1, { message: '보증금을 입력해주세요.' }), | |
| }); | |
| export type SearchAddressType = z.infer<typeof searchAddressSchema>; |
| 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; | ||
| }; |
There was a problem hiding this comment.
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;
};| <span className='absolute top-1/2 right-4 -translate-y-1/2 transform text-sm font-semibold text-[#333D4B]'> | ||
| 원 | ||
| </span> |
There was a problem hiding this comment.
text-[#333D4B]와 같이 색상 값이 하드코딩되어 있습니다. 유지보수성과 일관성을 위해 tailwind.config.js에 정의된 테마 색상(예: text-gray-800)을 사용하는 것이 좋습니다.
| <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> |
| <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'> |
There was a problem hiding this comment.
SelectTrigger에 적용된 shadow-[0px_4px_30px_0px_#0000001A] 클래스는 src/shared/components/ui/select.tsx의 SelectTrigger 컴포넌트에 이미 기본으로 적용되어 있습니다. 중복된 클래스이므로 제거하는 것이 좋습니다.
| <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'> |
| data-slot='select-trigger' | ||
| data-size={size} | ||
| className={cn( | ||
| 'flex h-9 w-full min-w-0 rounded-full bg-transparent py-1 pr-3 pl-5 text-base shadow-[0px_4px_30px_0px_#0000001A] transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-input-placeholder-gray disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30', |
📋 변경사항 요약
이번 PR에서는 주소 검색 기능과 진단 폼 기능을 구현하고, 관련 UI 컴포넌트들을 개선했습니다.
🚀 주요 기능
1. 주소 검색 기능 구현
2. 진단 폼 기능 구현
3. UI/UX 개선
📁 변경된 파일들
새로운 컴포넌트
API 및 타입
상수 및 설정
UI 컴포넌트 개선
�� 테스트 체크리스트
🔧 기술적 개선사항
📝 커밋 히스토리