feat: 새 가게 등록 API 연동#59
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough사장 인증, 가게 등록, 메뉴 이미지 업로드·메뉴 등록 관련 API·타입·스키마·훅·유틸·컴포넌트가 추가되어 다단계 가게 등록 흐름(사장 인증 → 가게 정보 입력·주소/지오코딩·이미지 → 메뉴 이미지 업로드·메뉴 등록)이 백엔드와 연동됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Frontend as Frontend
participant ApiClient as API Client
participant Backend as Backend
participant Kakao as Kakao SDK
User->>Frontend: 사업자 인증 입력 (businessNumber, startDate)
Frontend->>Frontend: Zod 검증 (BusinessAuthSchema)
Frontend->>ApiClient: patchVerifyOwner(body)
ApiClient->>Backend: PATCH /api/users/role/owner
alt 인증 성공
Backend-->>ApiClient: 200 ResponseVerifyOwnerDto
ApiClient-->>Frontend: result
Frontend->>User: 인증 완료 UI (강제 로그아웃·루트 이동)
else 이미 인증(409)
Backend-->>ApiClient: 409 Error
ApiClient-->>Frontend: error (isVerified=true)
end
User->>Frontend: 가게 정보 입력 + 주소 선택
Frontend->>Kakao: 주소 → geocode 요청
Kakao-->>Frontend: 좌표 반환
Frontend->>ApiClient: postRegisterStore(payload)
ApiClient->>Backend: POST /api/v1/stores
Backend-->>ApiClient: ResponseStoreCreateDto (storeId)
ApiClient-->>Frontend: storeId
opt 대표 이미지 제공
Frontend->>ApiClient: postMainImage(storeId, mainImage)
ApiClient->>Backend: POST /api/v1/stores/{storeId}/main-image
Backend-->>ApiClient: ResponseMainImageDto
end
loop 각 메뉴
opt 메뉴 이미지 제공
Frontend->>ApiClient: postMenuImage(storeId, image)
ApiClient->>Backend: POST /api/v1/stores/{storeId}/menus/images
Backend-->>ApiClient: ResponseMenuImageDto (imageKey)
end
end
Frontend->>ApiClient: postMenuCreate(storeId, menus[])
ApiClient->>Backend: POST /api/v1/stores/{storeId}/menus
Backend-->>ApiClient: ResponseMenuCreateDto
ApiClient-->>Frontend: 완료
Frontend->>User: 등록 완료 모달 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60분 Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/components/store-registration/CompleteModal.tsx (1)
26-33:⚠️ Potential issue | 🟠 Major
handleExit함수를useCallback으로 감싸기부모 컴포넌트(
StoreRegistrationPage)에서handleExit이 매 렌더마다 새로 생성되고 있어요. 그래서onClose의존성이 계속 변경되면서useEffect가 반복 실행되고, 타이머가 계속 리셋돼서 모달이 자동으로 닫히지 않아요.const handleExit = useCallback(() => { setIsCompleteModalOpen(false); navigate("/mypage/store", { replace: true }); }, [navigate]);이렇게 하면
handleExit의 참조가 안정적으로 유지되고,useEffect의 타이머도 정상 작동해요.src/pages/myPage/StoreRegistrationPage.tsx (1)
73-141:⚠️ Potential issue | 🟠 Major제출 중 로딩 상태 처리 및 중복 클릭 방지가 필요합니다
가게 등록 → 이미지 업로드 → 메뉴 생성까지 여러 API 호출이 순차적으로 이루어지는데, 그 사이에 사용자가 "다음/등록" 버튼을 다시 클릭할 수 있어요. 현재 로딩 상태 표시나 버튼 비활성화 로직이 없어서 중복 제출이 발생할 수 있습니다.
useRegisterStore의isPending등을 활용해 제출 중 버튼을 비활성화하고, 로딩 인디케이터를 보여주면 좋겠어요.As per coding guidelines, "
src/pages/**: 라우팅/레이아웃 영향, 로딩/에러/empty 상태 UX 확인."🔧 제안: isPending을 활용한 중복 제출 방지
- const { mutate: registerStore } = useRegisterStore(); + const { mutateAsync: registerStore, isPending: isRegistering } = useRegisterStore();그리고
isNextDisabled에 제출 중 상태를 추가:const isNextDisabled = + isRegistering || (currentStep === 1 && !isStep1Valid) || (currentStep === 2 && !isStep2Valid) || (currentStep === 3 && !isStep3Valid);
🤖 Fix all issues with AI agents
In `@src/api/store.ts`:
- Around line 20-33: postMainImage currently lacks local error handling; wrap
the body of postMainImage (which builds FormData and calls api.post with
`/api/v1/stores/${storeId}/main-image`) in a try-catch, catch any thrown error
from api.post (or network issues), perform appropriate handling (e.g. log via
your logger or normalize the error) and rethrow or return a rejected promise so
callers can handle it; ensure the function signature (RequestMainImageDto ->
Promise<ResponseMainImageDto>) is preserved and any caught error includes enough
context (storeId and operation) before rethrowing.
In `@src/components/store-registration/Menu.schema.ts`:
- Line 3: The two exported enums both named CategoryEnum conflict (menu
CategoryEnum: "MAIN"|"SIDE"|"BEVERAGE"|"ALCOHOL" and store CategoryEnum:
"KOREAN"|"CHINESE"|"JAPANESE"|"WESTERN"|"CAFE"); rename the menu enum exported
symbol to MenuCategoryEnum (update the declaration currently named CategoryEnum
in Menu.schema.ts) and rename the store enum exported symbol to
StoreCategoryEnum (update the declaration currently named CategoryEnum in
StoreInfo.schema.ts), then update all imports/usages to the new names (e.g.,
replace CategoryEnum references with MenuCategoryEnum or StoreCategoryEnum as
appropriate) to avoid auto-import/name collisions.
In `@src/components/store-registration/StepMenuRegistration.tsx`:
- Around line 39-45: The effect in StepMenuRegistration uses onChange in its
dependency array causing potential infinite re-renders if the parent passes a
new onChange each render; stabilize by removing onChange from deps and reading a
ref instead: create a ref (e.g., onChangeRef) set on each render to the latest
onChange, then inside the useEffect callback call onChangeRef.current(isValid,
getValues()) and use watch subscription as before; alternatively note that
stabilizing the parent prop via useCallback for onChange also fixes it.
In `@src/components/store-registration/StepStoreInfo.tsx`:
- Around line 124-133: The effect currently reads from the private API
control._formValues; replace that with the public getValues() from
react-hook-form: destructure getValues from useForm alongside watch, then call
onChange(isValid, getValues() as StoreInfoFormValues) for initial sync; keep the
subscription using watch and unsubscribe as-is and remove control from the
effect deps (use getValues and watch instead) so you no longer rely on the
internal control._formValues.
- Around line 407-453: The outer Label uses htmlFor="mainImage" but the file
input lacks id, breaking label-input accessibility; add id="mainImage" to the
<input ref={fileInputRef} type="file" ...> so the label opens the file picker,
and ensure the input is always rendered (kept hidden) even when previewUrl is
truthy (so the htmlFor remains valid) — alternatively remove htmlFor when
previewUrl is present, but preferred fix is to keep the input in the DOM with
id="mainImage" and reuse fileInputRef/handleImageChange so both the label and
the preview remove button continue to work.
In `@src/components/store-registration/StoreTransform.utils.ts`:
- Around line 28-32: The function formatTimeToBackend currently accepts timeStr:
string | undefined and silently returns "00:00" for falsy inputs; update it to
be explicit: either change the signature to timeStr: string to reflect that
callers must pass a validated value (and remove the undefined branch), or keep
the signature but throw a clear Error (e.g., "영업 시간은 필수입니다.") when timeStr is
undefined so callers know validation failed; update any callers of
formatTimeToBackend (or types like Partial<StoreInfoFormValues>) accordingly to
satisfy the new contract.
In `@src/hooks/queries/useAuth.ts`:
- Around line 46-57: The onError handler in useVerifyOwner currently only logs
the error; update it to parse the error message and alert the user: in
useVerifyOwner's onError (error) handler, extract a user-facing message from the
error (e.g., check for Axios-style error.response?.data?.message or fallback to
error.message), call alert(...) with that message (and include a short Korean
prefix like "인증 실패:"), and still retain console.error(error) for diagnostics;
ensure you handle the generic Error type safely before accessing nested fields.
In `@src/pages/myPage/StoreRegistrationPage.tsx`:
- Around line 25-28: registerStore is destructured with mutate while other hooks
use mutateAsync, causing onSuccess async callbacks not to be awaited; change the
destructure from useRegisterStore to pull mutateAsync (rename to registerStore
or registerStoreAsync to match your naming) and update any calls to await
registerStoreAsync; make handleNext async and wrap the sequential flow that
calls registerStore/uploadImage/uploadMenuImage/createMenu in a try/catch so
errors propagate correctly and are handled; reference useRegisterStore,
registerStore (or registerStoreAsync), handleNext, uploadImage, uploadMenuImage,
and createMenu when making these edits.
🧹 Nitpick comments (20)
src/hooks/queries/useStore.ts (1)
6-29:onError에서console.error만 하는 부분 — 사용자 피드백 고려두 hook 모두
onError에서console.error만 수행하고 있어요. 호출하는 쪽에서mutateAsync로 에러를 직접 처리한다면 괜찮지만,mutate로 호출할 경우 사용자에게 아무 피드백 없이 조용히 실패할 수 있습니다.useVerifyOwner처럼alert이나 toast를 추가하거나, 호출부에서 반드시 에러를 핸들링하는지 확인해 주세요.src/components/store-registration/StoreTransform.utils.ts (2)
65-66: 불필요한as any캐스트 제거 가능
BusinessHour.openTime은string | null타입인데, 삼항 연산자가 이미null과string을 올바르게 반환하고 있어서as any가 필요 없어요. 타입 안전성을 위해 제거하는 게 좋겠습니다.♻️ 수정 제안
- openTime: isClosed ? null : (formatTimeToBackend(openTime) as any), - closeTime: isClosed ? null : (formatTimeToBackend(closeTime) as any), + openTime: isClosed ? null : formatTimeToBackend(openTime), + closeTime: isClosed ? null : formatTimeToBackend(closeTime),
83-88:as any남용 및Number(latitude) || 0의 위도 0도 문제
Line 86-87의
as any는Partial에서 오는undefined가능성을 숨기고 있어요.category나depositRate가undefined로 넘어가면 API에서 400 에러가 날 수 있으니, 기본값을 명시하거나 호출 전에 유효성 검증하는 게 안전합니다.Line 83-84의
Number(latitude) || 0— 위도/경도가 정확히0인 경우도 falsy로 처리돼서0이 반환되긴 하지만, 한국 좌표 범위에서는 문제없습니다. 다만??연산자가 의도를 더 명확하게 표현해요:♻️ 수정 제안
- latitude: Number(latitude) || 0, - longitude: Number(longitude) || 0, + latitude: Number(latitude ?? 0), + longitude: Number(longitude ?? 0),src/hooks/queries/useAuth.ts (1)
49-53:alert()대신 toast/모달 사용 고려
alert()는 브라우저 기본 다이얼로그로 UI 스레드를 차단하고, 스타일링이 불가합니다. 프로젝트에 이미 Dialog 컴포넌트가 있으니 toast 라이브러리나 커스텀 모달로 교체하면 UX가 개선될 거예요. 지금 당장은 아니더라도 후속 작업으로 고려해 주세요.src/types/menus.ts (1)
1-22:MenuCategory와ApiMenuCategory간 불일치가 있어요.기존
MenuCategory는"MAIN" | "SIDE" | "DRINK"인데, 새로 추가된ApiMenuCategory는"MAIN" | "SIDE" | "BEVERAGE" | "ALCOHOL"이에요."DRINK"vs"BEVERAGE"차이가 의도된 것인지 확인이 필요해요.나중에 두 타입 간 변환이 필요할 때 매핑 실수가 생길 수 있으니, 향후 통합하거나 매핑 유틸리티를 두는 것을 고려해보세요.
src/components/store-registration/Menu.schema.ts (1)
19-41:imageKey필드가File | string | undefined를 모두 허용하는데, 필드명과 역할이 혼동될 수 있어요.
imageKey라는 이름은 서버에서 반환된 문자열 키를 연상시키는데, 실제로는 업로드 전File객체도 담고 있어요. 폼 내부에서만 사용되는 패턴이라 당장 문제는 아니지만,imageFile이나image로 이름을 바꾸거나, 업로드 완료 후 key 문자열로 교체하는 흐름을 주석으로 남겨두면 협업 시 이해하기 편할 거예요.src/components/store-registration/StoreInfo.schema.ts (2)
19-21:MAX_FILE_SIZE와ACCEPTED_IMAGE_TYPES가Menu.schema.ts에도 동일하게 선언되어 있어요.두 파일에서 같은 값을 각각 선언하고 있으니, 나중에 정책이 바뀌면 한쪽만 수정하는 실수가 생길 수 있어요. 공통 상수 파일로 추출하면 유지보수가 편해질 거예요.
89-100:openTime < closeTime문자열 비교는 시간 포맷에 의존해요.
"HH:mm"형식이면 문자열 비교로 정상 동작하지만, 포맷이 달라지면 의도치 않은 결과가 나올 수 있어요. PR 설명에 심야 영업(마감 < 시작) 등록 불가가 API 제약이라고 되어 있으니 현재로선 괜찮지만, 포맷 가정을 주석으로 남겨두면 좋겠어요.또한
openTime과closeTime모두 빈 문자열이 아닌 경우에만 비교하는데,min(1)제약 덕에 빈 문자열은 이미 걸러지므로if가드가 불필요하긴 해요. 큰 문제는 아니지만 참고해주세요.src/components/store-registration/StepBusinessAuth.tsx (2)
64-90:error as any캐스팅 대신 AxiosError 타입을 활용하면 더 안전해요.현재
error as any로 캐스팅 후response?.data?.code에 접근하고 있는데,AxiosError타입을 사용하면 타입 안전성을 유지할 수 있어요.♻️ AxiosError 타입 활용 제안
+ import { AxiosError } from "axios"; ... onError: (error) => { - const err = error as any; - const serverCode = err.response?.data?.code || err.code; + const serverCode = + error instanceof AxiosError + ? (error.response?.data as { code?: string })?.code + : undefined; if (serverCode === "OWNER409") {
66-69:await logout()이후 컴포넌트 unmount 가능성에 대해 한 가지 확인이요.
logout()호출 시clearAuth()가 zustand store를 업데이트하면서 인증 상태 변경으로 컴포넌트가 unmount될 수 있어요. 그 후의nav("/", { replace: true })는 react-router API 호출이라 에러가 나진 않지만, 만약 인증 guard가 이미 리다이렉트를 처리한다면 중복 네비게이션이 발생할 수 있어요.현재
isMountedRef체크가logout()이전에만 있으므로, 실질적으로 문제가 되진 않지만 의도를 명확히 하려면 주석을 남겨두면 좋겠어요.src/components/store-registration/StepMenuRegistration.tsx (1)
40-41:value as MenuFormValues타입 단언이 런타임 안전성을 보장하지 못해요.
watch콜백의value는DeepPartialSkipArrayKey<MenuFormValues>타입이라 일부 필드가undefined일 수 있는데,MenuFormValues로 강제 캐스팅하면 이후 로직에서 예상치 못한undefined접근이 생길 수 있어요.
getValues()는 전체 값을 반환하므로, subscription 콜백 내에서도getValues()를 사용하는 것이 더 안전해요:♻️ getValues() 사용 제안
const subscription = watch(() => { - onChange(isValid, value as MenuFormValues); + onChange(isValid, getValues()); });src/pages/myPage/StoreRegistrationPage.tsx (1)
183-189: 닫기(X) 버튼에aria-label추가를 권장합니다아이콘만 있는 버튼이라 스크린 리더 사용자에게 의미가 전달되지 않아요.
♿ 접근성 개선
<button type="button" onClick={() => setIsExitModalOpen(true)} + aria-label="가게 등록 나가기" className="text-gray-500 p-2 rounded-lg hover:bg-gray-100 transition-colors cursor-pointer" >src/components/store-registration/MenuItemInput.tsx (3)
133-139: 이미지 제거 버튼에aria-label이 빠져 있어요아이콘만 있는 버튼이라 스크린 리더가 용도를 전달하지 못합니다. 삭제 버튼(Line 91-98)에는
aria-label="메뉴 삭제"가 잘 들어가 있는데, 여기도 추가해주세요.As per coding guidelines, "
src/components/**: 컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크."♿ 접근성 개선
<button type="button" onClick={handleRemoveImage} + aria-label="메뉴 이미지 삭제" className="absolute top-1 right-1 p-1 bg-white/80 rounded-full text-gray-500 hover:bg-white hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all cursor-pointer" >
154-158: 에러 메시지 접근 시as any캐스팅이 사용되고 있어요react-hook-form의 중첩 에러 타입이 복잡해서 어쩔 수 없는 부분이긴 한데, 타입 안정성을 조금 더 높일 수 있습니다.
✨ 타입 안전한 접근 방법
- • {(errors.menus[index]?.imageKey as any).message} + • {(errors.menus[index]?.imageKey as { message?: string } | undefined)?.message}
221-237: 카테고리 선택에 placeholder 옵션이 없어요현재 첫 번째 값("메인 메뉴")이 자동 선택됩니다. 스키마의 기본값이 "MAIN"이라면 의도된 동작이겠지만, 사용자가 의식적으로 선택하게 하려면 disabled placeholder 옵션을 추가하는 것도 고려해볼 수 있어요.
src/components/store-registration/StepStoreInfo.tsx (5)
12-16:declare global은 별도.d.ts파일로 분리하는 게 좋아요컴포넌트 파일 내 global 타입 선언은 코드 응집도를 낮추고, 다른 파일에서
window.kakao를 사용할 때 이 파일에 의존하게 됩니다.src/types/kakao.d.ts같은 전역 타입 정의 파일로 분리하면 재사용성도 높아져요.
156-170: Geocoder 콜백에서console.log가 남아 있어요Line 161의
console.log("좌표 변환 성공:", lat, lng)는 디버깅용으로 보이는데, 프로덕션에 남기기엔 적절하지 않습니다. 삭제하거나 필요하다면console.debug로 변경해주세요.🧹 디버그 로그 제거
const lat = parseFloat(result[0].y); const lng = parseFloat(result[0].x); - - console.log("좌표 변환 성공:", lat, lng); setValue("latitude", lat); setValue("longitude", lng);
147-151: 숨겨진 주소 필드에shouldValidate누락
address는{ shouldValidate: true }로 설정되어 있는데,sido,sigungu,bname은shouldValidate없이setValue를 호출하고 있어요. 현재 스키마에서 이 필드들이z.string()(빈 문자열 허용)이라 당장 문제는 없지만, 나중에min(1)같은 검증이 추가되면 폼 유효성이 갱신되지 않는 문제가 생길 수 있어요.
latitude,longitude(Line 163-164)도 마찬가지입니다.🛡️ 일관성을 위한 shouldValidate 추가
- setValue("sido", data.sido); - setValue("sigungu", data.sigungu); - setValue("bname", data.bname); + setValue("sido", data.sido, { shouldValidate: true }); + setValue("sigungu", data.sigungu, { shouldValidate: true }); + setValue("bname", data.bname, { shouldValidate: true });
472-496: 주소 검색 모달에 접근성 속성과 키보드 인터랙션이 빠져 있어요현재 모달에
role="dialog",aria-modal="true",aria-label등의 접근성 속성이 없고, Escape 키로 닫을 수 없습니다. 또한 닫기 버튼(Line 481)에도aria-label이 없어요.♿ 접근성 개선 제안
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => setIsOpenPostcode(false)} + role="dialog" + aria-modal="true" + aria-label="주소 검색" + onKeyDown={(e) => { + if (e.key === "Escape") setIsOpenPostcode(false); + }} >닫기 버튼에도:
<button type="button" onClick={() => setIsOpenPostcode(false)} + aria-label="주소 검색 닫기" className="absolute top-4 right-4 text-gray-500 hover:text-black z-10 cursor-pointer" >
220-224: 카테고리 옵션이 하드코딩되어 있어요
MenuItemInput.tsx에서는CATEGORY_LABELS상수 맵을 사용하고 있는데, 여기서는 옵션이 인라인으로 하드코딩되어 있어서 카테고리가 변경되면 두 곳을 각각 수정해야 합니다. 물론 store 카테고리와 menu 카테고리가 다른 enum일 수 있으니, 같은 enum이라면 상수로 공유하면 유지보수가 편해져요.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@src/components/store-registration/Menu.schema.ts`:
- Around line 14-17: The price schema (price in Menu.schema.ts) currently uses
/^\d+$/ which permits "0" and values with leading zeros like "007"; update the
validation to require a positive integer without leading zeros by replacing that
regex with one that enforces no leading zeros (e.g., requiring the first digit
1-9 followed by digits) or alternatively preprocess the string to a number and
validate with z.number().int().min(1); update the error message to something
like "올바른 가격을 입력하세요." and keep the symbol name price and the surrounding
z.string() (or use z.preprocess → z.number()) so tests and uses of price remain
consistent.
In `@src/components/store-registration/StoreInfo.schema.ts`:
- Around line 61-87: The mainImage schema uses z.any() with subsequent refine
checks that return true when the value is not a File, allowing non-File truthy
values to pass; update the mainImage validation (the z.any().refine chain) to
first assert the value is an instance of File (e.g., in the first refine ensure
file instanceof File) so non-File values fail fast, and keep the existing
file.size <= MAX_FILE_SIZE and ACCEPTED_IMAGE_TYPES.includes(file.type) checks
only after that File guard; reference mainImage, z.any(), MAX_FILE_SIZE,
ACCEPTED_IMAGE_TYPES and File when making the change.
- Around line 89-100: The current .refine validator uses string comparison on
data.openTime and data.closeTime (data.openTime < data.closeTime), which breaks
for overnight hours (e.g., "23:00" -> "02:00"); update the StoreInfo schema's
refine block to parse times into minutes (or Date objects anchored to the same
day) and validate by handling the overnight case (if openMinutes ===
closeMinutes -> invalid, else if openMinutes < closeMinutes -> normal open, else
treat as overnight valid), and add a TODO comment in the same refine explaining
this special-case logic for future maintainers; reference the existing .refine
validator and the data.openTime / data.closeTime fields when making the change.
🧹 Nitpick comments (4)
src/components/store-registration/Menu.schema.ts (1)
19-41:z.any()를 사용한 파일 검증 — 타입 안전성 참고사항
z.any()를 쓰면MenuFormValues에서imageKey의 추론 타입이any가 돼서, 이후 사용처에서 타입 가드 없이 접근하면 런타임 에러가 날 수 있어요. 브라우저 환경에서File객체를 zod으로 다루려면z.any()+ refine이 현실적인 패턴이긴 한데, 타입 추론이라도 잡아주면 안전해질 거예요.♻️ 타입 추론 보강 예시
- imageKey: z - .any() - .optional() + imageKey: z + .union([z.string(), z.instanceof(File)]) + .optional()SSR 환경에서
z.instanceof(File)이 문제가 될 수 있으니, CSR 전용이면 이 방법이 괜찮고, 아니라면 현재 패턴 유지 + 별도 타입 단언(as File | string | undefined)을 사용처에서 하는 것도 방법이에요.src/components/store-registration/StoreInfo.schema.ts (2)
50-55:bookingIntervalMinutespreprocess가 조금 느슨해요 (참고 수준).
Number(val)은null→ 0,true→ 1 등 예상치 못한 변환도 허용합니다. 폼 입력에서 이런 값이 들어올 가능성은 낮지만, 방어적으로typeof val === "string"체크를 추가하면 더 안전해요.💡 좀 더 엄격한 preprocess 예시
bookingIntervalMinutes: z.preprocess((val) => { - if (val === "" || val === undefined || Number.isNaN(val)) { + if (val === "" || val === undefined || val === null) { return 0; } - return Number(val); + const num = Number(val); + return Number.isNaN(num) ? 0 : num; }, z.number().min(0)),
3-9: Zod enum과src/types/store.ts의 타입 리터럴이 별도로 관리되고 있어요.
StoreCategoryEnum은store.ts의Category타입과,DepositRateEnum은DepositRate타입과 동일한 값을 각각 독립적으로 정의하고 있어서, 한쪽만 수정하면 불일치가 생길 수 있어요.한 곳에서 정의하고 다른 쪽에서 파생하면 유지보수가 편해질 거예요. 예를 들어:
// store.ts import { StoreCategoryEnum } from "@/components/store-registration/StoreInfo.schema"; export type Category = z.infer<typeof StoreCategoryEnum>;또는 반대로 타입에서 zod enum을 생성하는 방식도 괜찮습니다.
Also applies to: 11-17
src/types/store.ts (1)
30-35:BusinessHour타입이 중복 정의되어 있어요.
src/types/store.ts와src/api/endpoints/stores.ts두 곳에서BusinessHour를 각각 정의하고 있는데,day필드에 사용하는Day와DayOfWeek타입도 내용은 동일하지만 별도로 정의되어 있네요. 지금은 타입 불일치 문제는 없지만, 나중에 한쪽만 수정하면 동기화 문제가 생길 수 있어요.타입 정의를 하나로 통일해서 재사용하는 게 안전합니다. 예를 들어:
리팩토링 예시
// src/types/store.ts에서만 정의하고 export type Day = "MONDAY" | "TUESDAY" | ... ; export type BusinessHour = { day: Day; openTime: string | null; closeTime: string | null; isClosed: boolean; }; // src/api/endpoints/stores.ts에서는 import import type { BusinessHour, Day } from "@/types/store";
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@src/components/store-registration/StepStoreInfo.tsx`:
- Around line 136-180: handleAddressComplete currently silently sets
latitude/longitude to 0 on geocoding failure (or when SDK not loaded), which can
lead to invalid saved locations; update handleAddressComplete to surface a
user-visible error (e.g., call toast.error or set a visible form warning) when
geocoding fails or SDK is missing and avoid writing 0,0 as valid coordinates—use
setValue to set latitude/longitude to null/undefined or call setError
(react-hook-form) for "latitude"/"longitude" to block submission, and also add
validation in StoreInfo.schema.ts to treat (0,0) or null coordinates as invalid
(e.g., require non-zero lat/lng or non-null) so server submissions are rejected
if geocoding failed.
- Around line 474-498: The postcode modal in StepStoreInfo lacks keyboard
accessibility; update the modal to add role="dialog" and aria-modal="true" (and
an aria-labelledby/aria-label referencing the modal title) on the modal
container, add an Escape key handler that calls setIsOpenPostcode(false) when
isOpenPostcode is true, and implement a simple focus trap: save the previously
focused element when opening, move focus into the modal (e.g., to the close
button or the DaumPostcodeEmbed wrapper), and intercept Tab/Shift+Tab to cycle
focus among tabbable elements inside the modal until it closes (restore previous
focus on close). Ensure handlers reference setIsOpenPostcode, isOpenPostcode,
and the modal DOM node (use a ref) and that handleAddressComplete behavior
remains unchanged.
- Line 334: The Label component in StepStoreInfo sets htmlFor="holidays" but
there is no element with id="holidays", breaking the label-to-control
connection; fix by either removing htmlFor from the Label or by making the
button-group a proper labeled group: add id="holidays" to the container that
wraps the holiday buttons and give it role="group" (or use aria-labelledby
pointing to the Label's id), ensuring the Label (or its id) and the group
container (the button wrapper) match so the label correctly associates with the
control.
In `@src/components/store-registration/StoreTransform.utils.ts`:
- Around line 49-51: The current construction of fullAddress in
StoreTransform.utils.ts can produce strings like "undefined 101호" because
step2Data is Partial and address may be undefined; update the logic that builds
fullAddress (the const fullAddress = ... using address and detailAddress) to
defensively handle undefined address by treating it as empty string or by
concatenating only truthy parts (e.g., use address ?? '' and only insert a space
when address exists, or join filtered segments), ensuring final value is either
"address detailAddress", "address", "detailAddress" or empty string but never
contains the literal "undefined".
- Around line 67-68: Remove the unnecessary "as any" casts on the time and enum
assignments: stop casting the results of formatTimeToBackend when assigning to
openTime/closeTime (allow null | string to flow into
BusinessHour.openTime/openTime) and remove the casts when assigning category and
depositRate to the DTO (let the Zod-inferred union types like "KOREAN" |
"CHINESE" etc. map to Category and DepositRate). Update the assignments in the
StoreTransform utilities to rely on the existing types (formatTimeToBackend,
BusinessHour.openTime/openTime, category, depositRate, Category, DepositRate) so
the TypeScript checker can enforce future schema/DTO mismatches.
🧹 Nitpick comments (4)
src/components/store-registration/StoreInfo.schema.ts (2)
66-71:mainImage두 번째 refine에 불필요한 분기가 남아있어요첫 번째
refine에서 이미file instanceof File을 보장하고 있어서, 두 번째 refine의if (file instanceof File)분기는 의미가 없어요. 양쪽 분기 모두file.size <= MAX_FILE_SIZE를 실행하는 데드코드 상태입니다.♻️ 간결하게 정리하는 제안
.refine( (file) => { - if (file instanceof File) { - return file.size <= MAX_FILE_SIZE; - } - return file.size <= MAX_FILE_SIZE; + return file.size <= MAX_FILE_SIZE; }, { message: "이미지 크기는 1MB 이하여야 합니다.", }, )
33-37: hidden 필드(sido,sigungu,bname,latitude,longitude)에 기본값이 없으면 초기 유효성 검사 실패 가능성이 필드들은
z.string()과z.number()로 필수 선언되어 있지만, 폼 초기화 시 주소 검색 전까지는 빈 문자열/0으로 세팅됩니다.z.string()은 빈 문자열도 통과하고z.number()는 0도 통과하니 동작상 문제는 없겠지만, 실제로 좌표0, 0이 유효한 좌표로 서버에 전송될 수 있어요.필요하다면
latitude/longitude에.refine()으로 0이 아닌 값인지 검증하는 것도 고려해볼 수 있어요.src/components/store-registration/StoreTransform.utils.ts (1)
85-86:Number(latitude) || 0— 적도(위도 0) 부근은 falsy로 처리됨한국 주소 기반이니 실질적 문제는 아니지만,
Number(val) || 0패턴은 실제 값이0일 때도 fallback으로 대체하는 특성이 있어요. 참고만 해주세요.src/components/store-registration/StepStoreInfo.tsx (1)
162-162: 프로덕션 코드에console.log/console.warn이 남아있어요
handleAddressComplete안에 디버깅용 로그가 세 곳 있어요. 개발 중엔 유용하지만 배포 전에 제거하거나 구조화된 로거로 교체하는 게 좋겠어요.♻️ 제거 제안
- console.log("좌표 변환 성공:", lat, lng); setValue("latitude", lat); setValue("longitude", lng); } else { - console.warn("좌표를 찾을 수 없습니다. (0으로 설정)"); setValue("latitude", 0); ... - console.warn("SDK 미로드로 좌표 변환 실패"); setValue("latitude", 0);Also applies to: 167-168, 174-174
yooseolhee
left a comment
There was a problem hiding this comment.
작동 잘 되는 거 확인 했습니다! 수고하셨어요!
jjjsun
left a comment
There was a problem hiding this comment.
위에 AI코드리뷰만 반영하고 바로 merge해주세요!
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/components/store-registration/StepStoreInfo.tsx`:
- Around line 131-140: The effect in StepStoreInfo uses onChange directly in its
dependency array which can cause infinite re-renders if the parent passes an
inline function; wrap the onChange prop in a stable ref (e.g., onChangeRef) and
use onChangeRef.current inside the useEffect that subscribes to watch/getValues;
add a small useEffect to update onChangeRef.current whenever the onChange prop
changes, remove onChange from the main effect deps, and keep watch, isValid, and
getValues as dependencies while still calling onChangeRef.current(isValid, value
as StoreInfoFormValues) and onChangeRef.current(isValid, getValues() as
StoreInfoFormValues) so the behavior remains identical.
- Around line 416-463: The preview branch removes the file input so
htmlFor="mainImage" breaks and the remove button is only visible on hover; keep
the <input id="mainImage"> always rendered (hidden) and still wired to
handleImageChange (render it outside or alongside both branches so htmlFor and
fileInputRef remain valid), and make the remove button discoverable by keyboard
by updating the preview wrapper and button focus styles (e.g., keep the
wrapper’s "group" and add focus/ focus-within/focus-visible variants such as
group-focus-within:opacity-100 and make the button include focus:opacity-100 or
focus-visible:opacity-100 and a descriptive aria-label) so handleRemoveImage
remains callable via keyboard.
🧹 Nitpick comments (1)
src/components/store-registration/StepStoreInfo.tsx (1)
482-509: 모달 dialog에 접근 가능한 이름이 없어요
role="dialog"+aria-modal="true"는 잘 적용되어 있는데, dialog 자체에aria-label이나aria-labelledby가 없어서 스크린 리더가 "이 대화 상자가 무엇인지" 안내하지 못해요. 간단하게aria-label만 추가해도 충분합니다.<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={() => setIsOpenPostcode(false)} role="dialog" aria-modal="true" + aria-label="주소 검색" >As per coding guidelines, "
src/components/**: 컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크."
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/components/store-registration/StoreTransform.utils.ts`:
- Around line 86-87: The latitude/longitude conversion currently uses
Number(latitude) || 0 which converts undefined to NaN and then silently falls
back to (0,0); update the mapping in StoreTransform.utils.ts to validate
coordinates instead of defaulting: add or reuse a helper like
requireCoordinate(name) (see formatTimeToBackend for pattern) that checks for
null/empty and Number.isNaN and throws a clear error, then replace
Number(latitude) || 0 and Number(longitude) || 0 with calls to that helper
(e.g., requireCoordinate(latitude, 'latitude') / requireCoordinate(longitude,
'longitude')) so invalid or missing coords fail fast rather than storing (0,0).
🧹 Nitpick comments (1)
src/components/store-registration/StoreTransform.utils.ts (1)
36-38:step2Data를Partial로 받는 설계에 대해 한번 고민해보면 좋겠어요현재
Partial<StoreInfoFormValues>로 받다 보니 함수 내부 곳곳에서|| "",|| 0,as any, 에러 throw 등 방어 코드가 많아지고 있어요. 이 함수가 폼 검증을 통과한 뒤에만 호출된다면, 타입을StoreInfoFormValues(non-partial)로 받는 게 더 깔끔할 수 있습니다.그러면 내부 방어 로직 대부분을 제거할 수 있고, 호출부에서 검증 책임이 명확해져요.
💡 개요
새 가게 등록을 위한 API들 연동
🔢 관련 이슈 링크
💻 작업내용
사장인증된 계정
이메일: ex@email.com
비번: 123123123123!
새롭게 사 인증 받을 시
00대학교 산학협력단 사업자등록증 검색 시 확인할 수 있는
대표자 이름으로 회원가입 한 뒤
그 대학교 사업자 번호와 개업일자 입력하시면 됩니다
주소 입력할 때 꼭 "서울특별시 강남구 역삼동"이어야 합니다!
(아마 db에 이 지역 데이터만 있는 듯 싶습니다)
📌 변경사항PR
🤔 추가 논의하고 싶은 내용
✅ 체크리스트
Summary by CodeRabbit
New Features
Bug Fixes / UX