Skip to content

feat: 새 가게 등록 API 연동#59

Merged
dew102938 merged 23 commits into
developfrom
feat/StoreRegistration-api
Feb 11, 2026
Merged

feat: 새 가게 등록 API 연동#59
dew102938 merged 23 commits into
developfrom
feat/StoreRegistration-api

Conversation

@dew102938

@dew102938 dew102938 commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

💡 개요

새 가게 등록을 위한 API들 연동

🔢 관련 이슈 링크

💻 작업내용

  • 사장 인증 API 연동
    • 첫 인증 회원은 인증 완료 후 로그아웃 라우팅 (재로그인 해야 사장토큰 발급됨)
  • 식당 등록 API 연동
    • react-daum-postcode 라이브러리 사용하여 주소 검색 모달 구현
  • 식당 대표 이미지 등록 API 연동
  • 메뉴 이미지 선 업로드 API 연동
  • 메뉴 등록 API 연동
  • 인증 실패, 이미 인증받은 회원 등 실패 시 alert로 사용자에게 알림

사장인증된 계정
이메일: ex@email.com
비번: 123123123123!

새롭게 사 인증 받을 시
00대학교 산학협력단 사업자등록증 검색 시 확인할 수 있는
대표자 이름으로 회원가입 한 뒤
그 대학교 사업자 번호와 개업일자 입력하시면 됩니다

주소 입력할 때 꼭 "서울특별시 강남구 역삼동"이어야 합니다!
(아마 db에 이 지역 데이터만 있는 듯 싶습니다)

📌 변경사항PR

  • FEAT: 새로운 기능 추가
  • FIX: 버그/오류 수정
  • CHORE: 코드/내부 파일/설정 수정
  • DOCS: 문서 수정(README 등)
  • REFACTOR: 코드 리팩토링 (기능 변경 없음)
  • TEST: 테스트 코드 추가/수정
  • STYLE: 스타일 변경(포맷, 세미콜론 등)

🤔 추가 논의하고 싶은 내용

  • 심야 영업시간(ex:23:00~02:00) 설정은 아직 배포가 안되어서 배포 되면 수정할 예정

✅ 체크리스트

  • 브랜치는 잘 맞게 올렸는지
  • 관련 이슈를 맞게 연결했는지
  • 로컬에서 정상 동작을 확있했는지
  • 충돌이 없다(또는 브랜치에서 충돌 해결 후 PR 업데이트 완료)

Summary by CodeRabbit

  • New Features

    • 사업자 인증: 사업자번호 + 시작일로 소유자 검증 기능 추가
    • 음식점 등록 확장: 가게 등록, 메인 이미지 업로드, 메뉴 일괄 등록 및 메뉴 이미지 업로드 지원
    • 메뉴 고도화: 카테고리 선택, 설명 필드, 이미지 첨부 및 가격 입력 개선
    • 주소/지도 연동: 우편번호 검색 모달과 자동 좌표 설정
  • Bug Fixes / UX

    • 폼 검증 강화 및 상세 오류 메시지, 이미지 미리보기/삭제 및 접근성 개선

@dew102938 dew102938 self-assigned this Feb 11, 2026
@coderabbitai

coderabbitai Bot commented Feb 11, 2026

Copy link
Copy Markdown

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

사장 인증, 가게 등록, 메뉴 이미지 업로드·메뉴 등록 관련 API·타입·스키마·훅·유틸·컴포넌트가 추가되어 다단계 가게 등록 흐름(사장 인증 → 가게 정보 입력·주소/지오코딩·이미지 → 메뉴 이미지 업로드·메뉴 등록)이 백엔드와 연동됩니다.

Changes

Cohort / File(s) Summary
인증 API·타입·훅
src/api/auth.ts, src/types/auth.ts, src/hooks/queries/useAuth.ts
PATCH /api/users/role/owner 호출용 patchVerifyOwner + DTO들(RequestVerifyOwnerDto, ResponseVerifyOwnerDto) 추가 및 useVerifyOwner 뮤테이션 훅 추가.
가게 API·타입·훅
src/api/store.ts, src/types/store.ts, src/hooks/queries/useStore.ts
가게 등록/대표 이미지 업로드 함수(postRegisterStore, postMainImage) 및 DTO들(RequestStoreCreateDto, ResponseStoreCreateDto, RequestMainImageDto, ResponseMainImageDto, DepositRate, BusinessNumberDto) 추가, useRegisterStore/useMainImage 뮤테이션 훅 추가.
메뉴 API·타입·훅
src/api/menu.ts, src/types/menus.ts, src/hooks/queries/useMenu.ts
메뉴 생성·이미지 업로드 함수(postMenuCreate, postMenuImage) 및 타입(ApiMenuCategory, MenuCreateItemDto, RequestMenuCreateDto, ResponseMenuCreateDto, RequestMenuImageDto, ResponseMenuImageDto) 추가, useMenuCreate/useMenuImage 훅 추가.
사장 인증 스키마·컴포넌트
src/components/store-registration/BusinessAuth.schema.ts, src/components/store-registration/StepBusinessAuth.tsx
사업자번호·개시일 Zod 스키마 추가, StepBusinessAuth가 스키마 기반 검증 및 useVerifyOwner 사용으로 인증 흐름·상태(409 처리 등) 변경.
가게 정보 스키마·컴포넌트·유틸
src/components/store-registration/StoreInfo.schema.ts, src/components/store-registration/StepStoreInfo.tsx, src/components/store-registration/StoreTransform.utils.ts
카테고리·보증금 enum, mainImage 파일 검증, 주소 검색(다음 우편번호)·카카오 지오코딩 통합, 필드명 변경(phone→phoneNumber 등), 좌표/영업시간 교차검증, transformToRegister 변환 유틸 추가.
메뉴 스키마·입력 컴포넌트·등록 흐름
src/components/store-registration/Menu.schema.ts, src/components/store-registration/MenuItemInput.tsx, src/components/store-registration/StepMenuRegistration.tsx
메뉴 카테고리 enum·이미지 파일 규격 검증 추가, 필드명 변경(menuName→name), React Hook Form control/trigger 통합, useWatch 기반 이미지 프리뷰 및 부모 구독 방식으로 폼 변경 통지 수정.
페이지 통합 및 업로드 시퀀스
src/pages/myPage/StoreRegistrationPage.tsx, src/components/store-registration/CompleteModal.tsx
transformToRegister로 페이로드 구성 후 등록 순서 처리: 가게 등록 → 대표 이미지 업로드(선택) → 메뉴 이미지 업로드(병렬 선택) → 메뉴 생성 → 완료 모달. 완료 모달의 카테고/전화 표시 보정.

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: 등록 완료 모달 표시
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60분

Possibly related PRs

Suggested reviewers

  • yooseolhee
  • jjjsun

Poem

🎉 사업자 번호가 문을 두드리네
이미지가 업로드되어 메뉴가 줄지어 오고
카카오가 좌표를 속삭이며 타입이 질서를 잡네
에러는 잡히고 업로드는 순서대로 ✨
완료 모달이 울리면 배포 준비 끝!

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 반영하고 있습니다. '새 가게 등록 API 연동'은 전체 기능의 핵심을 간결하게 표현하고 있습니다.
Linked Issues check ✅ Passed 연동된 이슈 #58의 요구사항이 충분히 충족되었습니다: 사장 인증·식당 등록·이미지 업로드·메뉴 등록 API 연동, 주소 검색 모달 구현, 예외처리, 강제 로그아웃 라우팅 모두 구현되어 있습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 이슈 #58의 범위 내에 있으며, 새 가게 등록 기능 구현에 필요한 API 연동, 스키마, 유틸리티, 훅, UI 컴포넌트들로만 구성되어 있습니다.
Description check ✅ Passed PR 설명이 제공된 템플릿을 대체로 잘 따르고 있으며, 핵심 내용과 작업 범위가 명확하게 작성되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/StoreRegistration-api

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 호출이 순차적으로 이루어지는데, 그 사이에 사용자가 "다음/등록" 버튼을 다시 클릭할 수 있어요. 현재 로딩 상태 표시나 버튼 비활성화 로직이 없어서 중복 제출이 발생할 수 있습니다.

useRegisterStoreisPending 등을 활용해 제출 중 버튼을 비활성화하고, 로딩 인디케이터를 보여주면 좋겠어요.

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.openTimestring | null 타입인데, 삼항 연산자가 이미 nullstring을 올바르게 반환하고 있어서 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도 문제

  1. Line 86-87의 as anyPartial에서 오는 undefined 가능성을 숨기고 있어요. categorydepositRateundefined로 넘어가면 API에서 400 에러가 날 수 있으니, 기본값을 명시하거나 호출 전에 유효성 검증하는 게 안전합니다.

  2. 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: MenuCategoryApiMenuCategory 간 불일치가 있어요.

기존 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_SIZEACCEPTED_IMAGE_TYPESMenu.schema.ts에도 동일하게 선언되어 있어요.

두 파일에서 같은 값을 각각 선언하고 있으니, 나중에 정책이 바뀌면 한쪽만 수정하는 실수가 생길 수 있어요. 공통 상수 파일로 추출하면 유지보수가 편해질 거예요.


89-100: openTime < closeTime 문자열 비교는 시간 포맷에 의존해요.

"HH:mm" 형식이면 문자열 비교로 정상 동작하지만, 포맷이 달라지면 의도치 않은 결과가 나올 수 있어요. PR 설명에 심야 영업(마감 < 시작) 등록 불가가 API 제약이라고 되어 있으니 현재로선 괜찮지만, 포맷 가정을 주석으로 남겨두면 좋겠어요.

또한 openTimecloseTime 모두 빈 문자열이 아닌 경우에만 비교하는데, 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 콜백의 valueDeepPartialSkipArrayKey<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, bnameshouldValidate 없이 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이라면 상수로 공유하면 유지보수가 편해져요.

Comment thread src/api/store.ts
Comment thread src/components/store-registration/Menu.schema.ts Outdated
Comment thread src/components/store-registration/StepMenuRegistration.tsx
Comment thread src/components/store-registration/StepStoreInfo.tsx Outdated
Comment thread src/components/store-registration/StoreTransform.utils.ts
Comment thread src/hooks/queries/useAuth.ts
Comment thread src/pages/myPage/StoreRegistrationPage.tsx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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: bookingIntervalMinutes preprocess가 조금 느슨해요 (참고 수준).

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의 타입 리터럴이 별도로 관리되고 있어요.

StoreCategoryEnumstore.tsCategory 타입과, DepositRateEnumDepositRate 타입과 동일한 값을 각각 독립적으로 정의하고 있어서, 한쪽만 수정하면 불일치가 생길 수 있어요.

한 곳에서 정의하고 다른 쪽에서 파생하면 유지보수가 편해질 거예요. 예를 들어:

// 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.tssrc/api/endpoints/stores.ts 두 곳에서 BusinessHour를 각각 정의하고 있는데, day 필드에 사용하는 DayDayOfWeek 타입도 내용은 동일하지만 별도로 정의되어 있네요. 지금은 타입 불일치 문제는 없지만, 나중에 한쪽만 수정하면 동기화 문제가 생길 수 있어요.

타입 정의를 하나로 통일해서 재사용하는 게 안전합니다. 예를 들어:

리팩토링 예시
// 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";

Comment thread src/components/store-registration/Menu.schema.ts Outdated
Comment thread src/components/store-registration/StoreInfo.schema.ts
Comment thread src/components/store-registration/StoreInfo.schema.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread src/components/store-registration/StepStoreInfo.tsx
Comment thread src/components/store-registration/StepStoreInfo.tsx Outdated
Comment thread src/components/store-registration/StepStoreInfo.tsx
Comment thread src/components/store-registration/StoreTransform.utils.ts Outdated
Comment thread src/components/store-registration/StoreTransform.utils.ts Outdated

@yooseolhee yooseolhee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

작동 잘 되는 거 확인 했습니다! 수고하셨어요!

@jjjsun jjjsun left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

위에 AI코드리뷰만 반영하고 바로 merge해주세요!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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) 체크."

Comment thread src/components/store-registration/StepStoreInfo.tsx Outdated
Comment thread src/components/store-registration/StepStoreInfo.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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: step2DataPartial로 받는 설계에 대해 한번 고민해보면 좋겠어요

현재 Partial<StoreInfoFormValues>로 받다 보니 함수 내부 곳곳에서 || "", || 0, as any, 에러 throw 등 방어 코드가 많아지고 있어요. 이 함수가 폼 검증을 통과한 뒤에만 호출된다면, 타입을 StoreInfoFormValues(non-partial)로 받는 게 더 깔끔할 수 있습니다.

그러면 내부 방어 로직 대부분을 제거할 수 있고, 호출부에서 검증 책임이 명확해져요.

Comment thread src/components/store-registration/StoreTransform.utils.ts Outdated
@dew102938 dew102938 merged commit 658ef65 into develop Feb 11, 2026
1 check passed
@jjjsun jjjsun deleted the feat/StoreRegistration-api branch February 11, 2026 19:43
@jjjsun jjjsun added ✨ Feature 새로운 기능 추가 📪 API 서버 API 연결, 통신 labels Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📪 API 서버 API 연결, 통신 ✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 새 가게 등록 API 연동

3 participants