feat: 내가게관리 메뉴관리 API 연동#61
Conversation
📝 WalkthroughWalkthrough내 가게 관리 메뉴 기능을 서버 API와 연동합니다. 신규 API 모듈을 추가하고 메뉴 폼 및 관리 컴포넌트에 이미지 업로드/삭제, CRUD, 판매중단 토글과 로컬/서버 병합 로직을 구현했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as MenuFormModal
participant Management as MenuManagement
participant API as Owner API
participant Server
rect rgba(100,150,200,0.5)
Note over User,Server: 이미지 업로드 → 메뉴 생성 흐름
User->>UI: 이미지 선택
UI->>API: POST /stores/{id}/menus/images (FormData)
API->>Server: 업로드 요청
Server-->>API: { imageKey, imageUrl }
API-->>UI: 응답 전달
UI->>UI: imageUrl, imageKey 상태 설정
User->>UI: 메뉴 정보 제출
UI->>API: POST /stores/{id}/menus { menus }
API->>Server: 메뉴 생성 요청 (Authorization)
Server-->>API: 생성된 메뉴 데이터
API-->>Management: 생성 응답 전달
Management->>Management: 로컬 merge 및 localStorage 저장
end
rect rgba(200,100,100,0.5)
Note over User,Server: 이미지 삭제(편집 모드)
User->>UI: 이미지 삭제 클릭
UI->>API: DELETE /stores/{id}/menus/{menuId}/image
API->>Server: 이미지 삭제 요청 (Authorization)
Server-->>API: 삭제 성공/에러
API-->>UI: 결과 전달
UI->>Management: onImageDelete 콜백 호출 (로컬 상태 갱신)
Management->>Management: localStorage 업데이트
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
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 |
dew102938
left a comment
There was a problem hiding this comment.
스웨거를 확인해 보니 카테고리 등록/삭제/수정 관련 API가 현재는 없습니다
그래서 카테고리 관리 기능은 아예 제외하는 게 어떨까요?
대신 카테고리는 서버에서 허용하는 값인 main, side, beverage, alcohol
네 가지만 사용할 수 있도록 고정하는 방향으로 수정이 필요할 것 같습니다
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@src/api/owner/menus.ts`:
- Line 92: The variable token retrieved via localStorage.getItem("accessToken")
in the menus handler is unused; either remove the unused const token declaration
or, if authentication/authorization was intended, replace the unused retrieval
with actual usage (e.g., pass token into the relevant functions or include it in
request headers). Locate the line with const token =
localStorage.getItem("accessToken") and either delete that declaration or wire
the token into the intended logic (e.g., function calls that require auth) so
it’s not left unused.
- Line 1: Remove the broken OWNER_TOKEN import from src/api/owner/menus.ts and
delete any manual Authorization header code in each exported function so the
axios request interceptor in api/axios.ts (which uses useAuthStore and handles
token refresh/retry) can manage tokens; also remove the unused local variable
token in uploadMenuImage and change deleteMenuImage's catch to rethrow the
caught error instead of returning a fake success response so callers can detect
failures.
In `@src/components/owner/menuFormModal.tsx`:
- Around line 194-206: The bug is a mismatched property check: the code checks
editingMenu?.menuId but later reads editingMenu.id, causing the early-return to
always run; update the initial guard to check editingMenu?.id (or consistently
use menuId everywhere) so that when an editingMenu with an id exists the
function proceeds to call the server instead of only clearing local state; keep
the existing variables setImageFile, setImageUrl, setImageKey and the menuId
constant (derived from editingMenu.id) so the delete flow executes correctly.
- Line 197: The code sets imageKey to an empty string in one place
(setImageKey("")) and to null in another, which breaks the imageKey !== null
guard and can send payload.imageKey = "" to the server; change the first
occurrence to setImageKey(null) and ensure all places consistently use null for
the “no image” state (update any other assignments or checks around imageKey,
setImageKey, and payload.imageKey so they treat null as the sole empty-state).
- Around line 33-36: The image-related state (imageFile and uploading) isn't
reset when the modal reopens, causing stale values to persist; inside the
existing useEffect that runs on modal open or when editingMenu changes (the same
effect that sets imageUrl/imageKey), call setImageFile(null) and
setUploading(false) (and optionally reset imageUrl/imageKey via
setImageUrl(null)/setImageKey(null) when creating a new menu) so that imageFile,
uploading, imageUrl, and imageKey are properly initialized whenever the modal is
opened or editingMenu changes.
In `@src/components/owner/menuManagement.tsx`:
- Around line 195-198: The code in toggleSoldOutOnServer incorrectly tests
existence instead of the actual boolean value (newSoldOut is set using
(res.result as any).isSoldOut !== undefined), causing false to be treated as
true; change the assignment to derive a real boolean from res.result.isSoldOut
(e.g., const newSoldOut = !!(res.result && (res.result as any).isSoldOut) or
explicitly compare to true) and then use that newSoldOut when calling setMenus
and in the alert so true/false from the server is respected.
- Around line 164-183: The deleteMenu handler currently always converts id via
Number(id) which yields NaN for local-temp IDs like "MENU_{timestamp}" created
by mapServerToLocal; update deleteMenu to detect local IDs (e.g., id
startsWith('MENU_') or Number(id) is NaN) and in that branch avoid calling
deleteMenus: just call setMenus(prev => prev.filter(m => m.id !== id)) (or
compare as strings) and show a local-deletion alert, returning early; otherwise
proceed with the existing numeric conversion, deleteMenus(storeId, [menuIdNum])
flow and success/error handling.
- Line 325: MenuManagement currently uses a non-null assertion on storeId when
rendering MenuFormModal (storeId!) despite MenuManagementProps declaring
storeId?: string; remove the non-null assertion and add a guard so the modal is
only opened or given a storeId when it exists — e.g., check storeId in render or
in the opening handlers (handleAddClick / handleEditClick) and prevent opening
the modal or call API unless storeId is defined; alternatively, adjust
MenuFormModal to accept undefined safely, but the simplest fix is to gate modal
open/prop passing on a defined storeId to avoid invalid API URLs.
🧹 Nitpick comments (5)
src/api/owner/menus.ts (2)
104-127:deleteMenuImage가 에러를 삼키고 가짜 응답을 반환해요
catch블록에서isSuccess: false인 응답 객체를 만들어 반환하고 있는데, 호출부에서 이게 실제 서버 응답인지 로컬 에러 fallback인지 구분할 수 없어요. 또한err: any대신 타입을 좁히는 게 좋겠어요.다른 API 함수들(
deleteMenus,updateMenu등)은 에러를 그대로 throw하는데 이 함수만 패턴이 달라서 혼란을 줄 수 있어요. 에러를 그대로 throw하고, 호출부(menuFormModal.tsx)에서catch로 처리하는 게 일관성 있습니다.♻️ 에러를 그대로 전파하는 방식
export const deleteMenuImage = async ( storeId: string, menuId: string ): Promise<ApiResponse<{ deletedImageKey: string }>> => { - try { const res = await api.delete<ApiResponse<{ deletedImageKey: string }>>( `/api/v1/stores/${storeId}/menus/${menuId}/image`, { headers: { Authorization: `Bearer ${OWNER_TOKEN}`, } } ); return res.data; - } catch (err: any) { - console.error('deleteMenuImage error', err); - return { - isSuccess: false, - code: '_MENU_IMAGE_DELETE_FAILED', - message: err?.response?.data?.message || '이미지 삭제 실패', - result: { deletedImageKey: '' }, - }; - } };As per coding guidelines,
src/api/**: 에러 처리/타임아웃/리트라이 전략 확인, 응답 타입 안전성 유지.
60-69:DeleteMenusResponse가ApiResponse<T>를 재사용하지 않아요
ApiResponse<T>타입이 이미@/types/api에 정의되어 있는데,DeleteMenusResponse가 동일한 구조를 다시 선언하고 있어요.ApiResponse<{ deletedMenuIds: number[] }>로 대체하면 중복을 줄일 수 있어요.♻️ 타입 재사용
-export interface DeleteMenusResponse { - isSuccess: boolean; - code: string; - result: { deletedMenuIds: number[] }; - message: string; -} +export type DeleteMenusResponse = ApiResponse<{ deletedMenuIds: number[] }>;src/components/owner/menuFormModal.tsx (1)
9-13:any타입 사용 — props 타입 안전성 개선 권장
onSubmit: (menuData: any),editingMenu?: any에서any를 사용하고 있어요.LocalMenu또는 전용 인터페이스로 타입을 지정하면 런타임 오류를 사전에 방지할 수 있어요. 지금 당장은 어렵다면, 나중에라도 개선해주세요.As per coding guidelines,
src/components/**: 컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크.src/components/owner/menuManagement.tsx (2)
85-117:menusstale closure 주의 —useEffect내 참조Line 93에서
menus.filter(...)를 호출하는데, 이menus는 effect 생성 시점의 값이에요.restaurantId가 변경될 때 이전menus상태를 읽을 수 있어요.eslint-disable react-hooks/exhaustive-deps로 경고를 끈 상태인데, 실제로 문제가 될 가능성은 낮지만 인지하고 계시면 좋겠어요.로컬 임시 메뉴 병합이 중요하다면,
setMenus(prev => ...)콜백 패턴으로 변경하는 게 안전합니다.♻️ 콜백 패턴 사용
- const localTempMenus = menus.filter(m => m.id.startsWith('MENU_')); - const mergedMenus = [...serverMenus, ...localTempMenus]; - setMenus(mergedMenus); - localStorage.setItem(STORAGE_KEY_MENU, JSON.stringify(mergedMenus)); + setMenus(prev => { + const localTempMenus = prev.filter(m => m.id.startsWith('MENU_')); + const mergedMenus = [...serverMenus, ...localTempMenus]; + localStorage.setItem(STORAGE_KEY_MENU, JSON.stringify(mergedMenus)); + return mergedMenus; + });
43-50:any[]와any타입 사용 —LocalMenu타입 활용 권장
LocalMenu인터페이스를 이미 정의해두셨으니,menus와editingMenu상태에도 적용하면 타입 안전성이 올라가요. 급하지 않으면 후속 작업으로도 괜찮을 것 같아요.♻️ 타입 적용
-const [menus, setMenus] = useState<any[]>([]); +const [menus, setMenus] = useState<LocalMenu[]>([]); // ... -const [editingMenu, setEditingMenu] = useState<any>(null); +const [editingMenu, setEditingMenu] = useState<LocalMenu | null>(null);As per coding guidelines,
src/components/**: 컴포넌트는 단일 책임, props 타입/네이밍 명확히, 접근성(aria) 체크.
💡 개요
내가게관리 메뉴관리 API 연동
🔢 관련 이슈 링크
💻 작업내용
📌 변경사항PR
🤔 추가 논의하고 싶은 내용
✅ 체크리스트
Summary by CodeRabbit
새로운 기능
수정/정리