diff --git a/src/api/axios.ts b/src/api/axios.ts index 38110ff..33f52f5 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -36,15 +36,20 @@ api.interceptors.request.use((config: InternalAxiosRequestConfig) => { let refreshPromise: ReturnType | null = null; +type ApiResponseWithFlags = { + code?: string; + message?: string; + success?: boolean; + isSuccess?: boolean; +}; + api.interceptors.response.use( (res) => { const data = res.data; if (isApiResponse(data)) { + const responseData: ApiResponseWithFlags = data; const failed = - (typeof (data as any).success === "boolean" && - (data as any).success === false) || - (typeof (data as any).isSuccess === "boolean" && - (data as any).isSuccess === false); + responseData.success === false || responseData.isSuccess === false; if (failed) { return Promise.reject({ diff --git a/src/api/bookings.ts b/src/api/bookings.ts index 7c9e290..80b37cc 100644 --- a/src/api/bookings.ts +++ b/src/api/bookings.ts @@ -1,6 +1,6 @@ import { api } from "./axios"; -type ApiBookingStatus = "CONFIRMED" | "COMPLETED" | "CANCELED"; +type ApiBookingStatus = "PENDING" | "CONFIRMED" | "COMPLETED" | "CANCELED"; interface Booking { bookingId: number; @@ -9,7 +9,8 @@ interface Booking { bookingDate: string; bookingTime: string; partySize: number; - amount: number; + tableNumbers: string; + amount: number | null; paymentMethod: string; status: ApiBookingStatus; } @@ -23,11 +24,16 @@ interface BookingResponse { isLast: boolean; } +type GetBookingParams = { + page: number; + status?: ApiBookingStatus; +}; + export const getBookings = async ( status?: ApiBookingStatus, page: number = 1, ): Promise => { - const params: any = { page }; + const params: GetBookingParams = { page }; if (status) params.status = status; const response = await api.get<{ result: BookingResponse }>( diff --git a/src/api/owner/menus.ts b/src/api/owner/menus.ts index e1e09c3..d0193ed 100644 --- a/src/api/owner/menus.ts +++ b/src/api/owner/menus.ts @@ -1,3 +1,4 @@ +import axios from "axios"; import { api } from "../axios"; import type { ApiResponse } from "@/types/api"; @@ -96,12 +97,20 @@ export const deleteMenuImage = async ( `/api/v1/stores/${storeId}/menus/${menuId}/image`, ); return res.data; - } catch (err: any) { + } catch (err: unknown) { console.error("deleteMenuImage error", err); + if (axios.isAxiosError(err)) { + return { + isSuccess: false, + code: "_MENU_IMAGE_DELETE_FAILED", + message: err?.response?.data?.message || "이미지 삭제 실패", + result: { deletedImageKey: "" }, + }; + } return { isSuccess: false, code: "_MENU_IMAGE_DELETE_FAILED", - message: err?.response?.data?.message || "이미지 삭제 실패", + message: "이미지 삭제 실패", result: { deletedImageKey: "" }, }; } diff --git a/src/api/owner/storeLayout.ts b/src/api/owner/storeLayout.ts index 96e8c09..f22c065 100644 --- a/src/api/owner/storeLayout.ts +++ b/src/api/owner/storeLayout.ts @@ -1,6 +1,7 @@ import type { ApiResponse } from "@/types/api"; import { api } from "../axios"; import type { SeatsType } from "@/types/table"; +import axios from "axios"; export interface LayoutTable { tableId: number; @@ -53,8 +54,8 @@ export const getActiveLayout = async ( return null; } return null; - } catch (e: any) { - if (e.response?.status === 404) { + } catch (e: unknown) { + if (axios.isAxiosError(e) && e.response?.status === 404) { console.error("가게를 찾을 수 없음"); } else { console.error(e); @@ -97,8 +98,13 @@ export const createTable = async ( } console.error("테이블 생성 실패 응답:", res.data); return null; - } catch (e: any) { - console.error("테이블 생성 실패:", e?.response?.data ?? e); + } catch (e: unknown) { + if (axios.isAxiosError(e)) { + console.error("테이블 생성 실패:", e?.response?.data ?? e); + } else { + console.error("테이블 생성 실패:", e); + } + return null; } }; diff --git a/src/api/owner/stores.ts b/src/api/owner/stores.ts index 58d9f34..093b9e4 100644 --- a/src/api/owner/stores.ts +++ b/src/api/owner/stores.ts @@ -1,5 +1,6 @@ import { api } from "@/api/axios"; import type { ApiResponse } from "@/types/api"; +import type { UpdateStoreResponse } from "@/types/store"; interface StoreDetail { storeId: number; @@ -70,7 +71,10 @@ export function updateStore( phoneNumber: string; }, ) { - return api.patch>(`/api/v1/stores/${storeId}`, body); + return api.patch>( + `/api/v1/stores/${storeId}`, + body, + ); } export function updateBusinessHours( diff --git a/src/api/owner/table.ts b/src/api/owner/table.ts index bcb6b3b..88cb415 100644 --- a/src/api/owner/table.ts +++ b/src/api/owner/table.ts @@ -11,7 +11,7 @@ interface DeleteTableImageResult { tableId: number; } -interface PatchTableRequest { +export interface PatchTableRequest { tableNumber?: string; minSeatCount?: number; maxSeatCount?: number; diff --git a/src/components/auth/ChangePasswordDiaLog.tsx b/src/components/auth/ChangePasswordDiaLog.tsx index 22d2006..a2f3834 100644 --- a/src/components/auth/ChangePasswordDiaLog.tsx +++ b/src/components/auth/ChangePasswordDiaLog.tsx @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { Button } from "../ui/button"; import { X } from "lucide-react"; +import axios from "axios"; const schema = z .object({ @@ -43,8 +44,12 @@ export function ChangePasswordDialog({ form.reset(); onOpenChange(false); }, - onError: (e: any) => { - const msg = e?.response?.data?.message ?? "비밀번호 변경에 실패했습니다."; + onError: (e: unknown) => { + let msg = "비밀번호 변경에 실패했습니다."; + + if (axios.isAxiosError(e)) { + msg = e.response?.data?.message ?? msg; + } if (typeof msg === "string" && /현재|기존|일치|틀렸/.test(msg)) { form.setError("currentPassword", { type: "server", message: msg }); return; diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index da0eb58..32880b6 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -82,7 +82,7 @@ export function SignupDialog({ return ( !open && onClose()}> - + 회원가입 diff --git a/src/components/auth/WithdrawDialog.tsx b/src/components/auth/WithdrawDialog.tsx index 2598c67..01df2b1 100644 --- a/src/components/auth/WithdrawDialog.tsx +++ b/src/components/auth/WithdrawDialog.tsx @@ -6,8 +6,10 @@ import { Button } from "../ui/button"; import { X } from "lucide-react"; import { useState } from "react"; import { logout as performLogout } from "@/api/auth"; +import axios from "axios"; -function isWithdrawBlockByBookings(e: any) { +function isWithdrawBlockByBookings(e: unknown) { + if (!axios.isAxiosError(e)) return false; const msg = e?.response?.data?.message; const result = e?.response?.data?.result; const code = e?.response?.data?.code; @@ -56,12 +58,16 @@ export function WithdrawDialog({ onOpenChange(false); nav("/", { replace: true }); }, - onError: (e: any) => { + onError: (e: unknown) => { if (isWithdrawBlockByBookings(e)) { setBlocked(true); return; } - alert(e?.response?.data?.message ?? "회원 탈퇴에 실패했습니다."); + let msg = "회원 탈퇴에 실패했습니다"; + if (axios.isAxiosError(e)) { + msg = e?.response?.data?.message ?? msg; + } + alert(msg); }, }); diff --git a/src/components/customer-support/SupportHero.tsx b/src/components/customer-support/SupportHero.tsx index 6813201..310460d 100644 --- a/src/components/customer-support/SupportHero.tsx +++ b/src/components/customer-support/SupportHero.tsx @@ -9,7 +9,7 @@ export default function SupportHero() { return ( <> -
+

무엇을 도와드릴까요?

diff --git a/src/components/map/KakaoMap.tsx b/src/components/map/KakaoMap.tsx index 5e2ae63..e812e06 100644 --- a/src/components/map/KakaoMap.tsx +++ b/src/components/map/KakaoMap.tsx @@ -1,10 +1,14 @@ import { loadKakaoMapSdk } from "@/lib/kakao"; +import type { + KakaoInfoWindowInstance, + KaKaoMapInstance, + KakaoMarkerInstance, + LatLng, + MarkerWithLocation, +} from "@/types/map"; import type { RestaurantSummary } from "@/types/store"; import { useEffect, useMemo, useRef, useState } from "react"; -type LatLng = { lat: number; lng: number }; -type MarkerWithLocation = RestaurantSummary & { location: LatLng }; - type Props = { center: LatLng; markers: RestaurantSummary[]; @@ -14,22 +18,17 @@ type Props = { defaultLevel?: number; selectedLevel?: number; }; -declare global { - interface Window { - kakao: any; - } -} - const toNum = (v: unknown) => { const n = typeof v === "string" ? parseFloat(v) : Number(v); return Number.isFinite(n) ? n : null; }; -const normalizeLatLng = (loc: any): LatLng | null => { - if (!loc) return null; +const normalizeLatLng = (loc: unknown): LatLng | null => { + if (!loc || typeof loc !== "object") return null; + const maybeLoc = loc as { lat?: unknown; lng?: unknown }; - let lat = toNum(loc.lat); - let lng = toNum(loc.lng); + let lat = toNum(maybeLoc.lat); + let lng = toNum(maybeLoc.lng); if (lat == null || lng == null) return null; @@ -52,17 +51,17 @@ export default function KakaoMap({ selectedLevel, }: Props) { const containerRef = useRef(null); - const mapRef = useRef(null); - const markersRef = useRef>(new Map()); - const infoRef = useRef(null); + const mapRef = useRef(null); + const markersRef = useRef>(new Map()); + const infoRef = useRef(null); const prevSelectedIdRef = useRef(null); const safeMarkers = useMemo(() => { return markers - .map((m) => { - const norm = normalizeLatLng((m as any).location); + .map((marker) => { + const norm = normalizeLatLng((marker as MarkerWithLocation).location); if (!norm) return null; - return { ...m, location: norm } as MarkerWithLocation; + return { ...marker, location: norm } as MarkerWithLocation; }) .filter(Boolean) as MarkerWithLocation[]; }, [markers]); @@ -76,7 +75,9 @@ export default function KakaoMap({ try { mapRef.current.relayout(); - } catch {} + } catch { + //지도 초기화 타이밍에서 발생 가능 + } }; //1. 지도 최초 1회 생성 @@ -87,10 +88,11 @@ export default function KakaoMap({ try { await loadKakaoMapSdk(); if (cancelled) return; + setSdkReady(true); const kakao = window.kakao; - if (!containerRef.current) return; + if (!kakao?.maps || !containerRef.current) return; if (mapRef.current) return; const options = { @@ -112,7 +114,7 @@ export default function KakaoMap({ return () => { cancelled = true; }; - }, [defaultLevel]); + }, [defaultLevel, center.lat, center.lng]); // 2. 컨테이너 사이즈 변하면 relayout useEffect(() => { @@ -160,21 +162,22 @@ export default function KakaoMap({ //5. 마커 바뀌면 마커 재생성 useEffect(() => { const kakao = window.kakao; - if (!kakao?.maps || !mapRef.current) return; + const maps = kakao?.maps; + if (!maps || !mapRef.current) return; markersRef.current.forEach((mk) => mk.setMap(null)); markersRef.current.clear(); safeMarkers.forEach((store) => { - const pos = new kakao.maps.LatLng(store.location.lat, store.location.lng); - const marker = new kakao.maps.Marker({ + const pos = new maps.LatLng(store.location.lat, store.location.lng); + const marker = new maps.Marker({ map: mapRef.current, position: pos, clickable: true, zIndex: 1, }); - kakao.maps.event.addListener(marker, "click", () => { + maps.event.addListener(marker, "click", () => { mapRef.current?.panTo(pos); if (selectedLevel != null) { mapRef.current?.setLevel(selectedLevel); @@ -192,7 +195,7 @@ export default function KakaoMap({ markersRef.current.set(store.id, marker); }); relayout(); - }, [safeMarkers, onSelectMarker]); + }, [safeMarkers, onSelectMarker, selectedLevel]); //6. 선택변경시 zIndex 처리 useEffect(() => { @@ -212,22 +215,22 @@ export default function KakaoMap({ //7. 선택 없으면 bounds 맞추기 useEffect(() => { const kakao = window.kakao; - if (!kakao?.maps || !mapRef.current) return; - if (selectedId != null) return; - if (safeMarkers.length === 0) return; - const bounds = new kakao.maps.LatLngBounds(); + const maps = kakao?.maps; + if (!maps || !mapRef.current) return; + if (selectedId != null || safeMarkers.length === 0) return; + const bounds = new maps.LatLngBounds(); safeMarkers.forEach((store) => { - bounds.extend( - new kakao.maps.LatLng(store.location.lat, store.location.lng), - ); + bounds.extend(new maps.LatLng(store.location.lat, store.location.lng)); }); requestAnimationFrame(() => { try { - mapRef.current.relayout(); - mapRef.current.setBounds(bounds); - } catch {} + mapRef.current?.relayout(); + mapRef.current?.setBounds(bounds); + } catch { + //지도 초기화 타이밍 이슈무시 + } }); if (safeMarkers.length === 1 && defaultLevel != null) { diff --git a/src/components/owner/BreakTimeModal.tsx b/src/components/owner/BreakTimeModal.tsx index 1fde920..8d691a7 100644 --- a/src/components/owner/BreakTimeModal.tsx +++ b/src/components/owner/BreakTimeModal.tsx @@ -37,8 +37,11 @@ const BreakTimeModal: React.FC = ({ onClick={onClose} >

e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-labelledby="breaktime-modal-title" > -
+
브레이크 타임 설정
diff --git a/src/components/owner/MenuManagement.tsx b/src/components/owner/MenuManagement.tsx index 8626689..499dde5 100644 --- a/src/components/owner/MenuManagement.tsx +++ b/src/components/owner/MenuManagement.tsx @@ -3,28 +3,30 @@ import { Plus, Pencil, Trash2 } from "lucide-react"; import MenuFormModal from "./menuFormModal"; import { deleteMenus } from "@/api/owner/menus"; import { getMenus, updateMenuSoldOut } from "@/api/owner/menus"; +import type { MenuCategory, MenuItem } from "@/types/menus"; interface MenuManagementProps { storeId?: string; } interface Category { - id: string; + id: CategoryType; label: string; } -interface LocalMenu { - id: string; - name: string; +type ServerMenu = { + menuId?: number | string; + name?: string; description?: string; - price: number; + price?: number; category?: string; - imageUrl?: string | null; + imageUrl?: string; + imageKey?: string; isSoldOut?: boolean; isActive?: boolean; -} +}; -type CategoryType = string; +type CategoryType = "ALL" | MenuCategory; const MenuManagement: React.FC = ({ storeId }) => { const restaurantId = storeId; @@ -37,24 +39,27 @@ const MenuManagement: React.FC = ({ storeId }) => { { id: "ALCOHOL", label: "주류" }, ]; - const [menus, setMenus] = useState([]); - + const [menus, setMenus] = useState([]); const [categories] = useState(DEFAULT_CATEGORIES); const [activeCategory, setActiveCategory] = useState("ALL"); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); - const [editingMenu, setEditingMenu] = useState(null); + const [editingMenu, setEditingMenu] = useState(null); - const mapServerToLocal = (s: any): LocalMenu => ({ + const mapServerToLocal = ( + s: ServerMenu, + restaurantId?: string, + ): MenuItem => ({ id: String(s.menuId ?? `MENU_${Date.now()}`), + restaurantId: restaurantId ?? "", name: s.name ?? "", description: s.description ?? "", price: s.price ?? 0, - category: s.category ?? undefined, - imageUrl: s.imageUrl ?? null, - isSoldOut: !!s.isSoldOut, - isActive: true, + category: (s.category as MenuCategory) ?? "MAIN", + imageUrl: s.imageUrl ?? undefined, + isSoldOut: s.isSoldOut ?? false, + isActive: s.isActive ?? true, }); useEffect(() => { @@ -63,9 +68,13 @@ const MenuManagement: React.FC = ({ storeId }) => { : "menu-items-temp"; const savedMenus = localStorage.getItem(STORAGE_KEY_MENU); + + let parsedSavedMenus: MenuItem[] = []; + if (savedMenus) { try { - setMenus(JSON.parse(savedMenus)); + parsedSavedMenus = JSON.parse(savedMenus) as MenuItem[]; + setMenus(parsedSavedMenus); } catch { setMenus([]); } @@ -81,9 +90,13 @@ const MenuManagement: React.FC = ({ storeId }) => { try { const res = await getMenus(restaurantId); if (res.isSuccess && res.result && Array.isArray(res.result.menus)) { - const serverMenus = res.result.menus.map(mapServerToLocal); + const serverMenus = (res.result.menus as ServerMenu[]).map((menu) => + mapServerToLocal(menu, restaurantId), + ); - const localTempMenus = menus.filter((m) => m.id.startsWith("MENU_")); + const localTempMenus = parsedSavedMenus.filter((m) => + m.id.startsWith("MENU_"), + ); const mergedMenus = [...serverMenus, ...localTempMenus]; @@ -92,7 +105,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } else { setError(res.message || "메뉴를 가져오는 중 문제가 발생했습니다."); } - } catch (err: any) { + } catch (err: unknown) { console.error("getMenus error", err); setError("메뉴를 불러오는 데 실패했습니다. 네트워크를 확인해주세요."); } finally { @@ -108,7 +121,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } }, [menus, restaurantId]); - const handleFormSubmit = (menuData: any) => { + const handleFormSubmit = (menuData: MenuItem) => { setMenus((prev) => { const incomingId = menuData.id ? String(menuData.id) : null; @@ -130,7 +143,7 @@ const MenuManagement: React.FC = ({ storeId }) => { setEditingMenu(null); }; - const handleEditClick = (menu: any) => { + const handleEditClick = (menu: MenuItem) => { setEditingMenu(menu); setIsModalOpen(true); }; @@ -196,7 +209,7 @@ const MenuManagement: React.FC = ({ storeId }) => { } else { alert("품절 상태 변경 실패: " + res.message); } - } catch (err: any) { + } catch (err: unknown) { console.error("updateMenuSoldOut error", err); alert("품절 상태 변경 중 오류가 발생했습니다."); } @@ -332,14 +345,14 @@ const MenuManagement: React.FC = ({ storeId }) => { onClose={() => setIsModalOpen(false)} onSubmit={handleFormSubmit} categories={categories} - editingMenu={editingMenu} + editingMenu={editingMenu ?? undefined} storeId={storeId!} onImageDelete={() => { if (!editingMenu) return; setMenus((prev) => prev.map((m) => m.id === editingMenu.id - ? { ...m, imageUrl: null, imageKey: null } + ? { ...m, imageUrl: undefined, imageKey: undefined } : m, ), ); diff --git a/src/components/owner/StoreSettings.tsx b/src/components/owner/StoreSettings.tsx index d6419fd..0f54b08 100644 --- a/src/components/owner/StoreSettings.tsx +++ b/src/components/owner/StoreSettings.tsx @@ -11,14 +11,16 @@ import { type TableImage, deleteTableImages, } from "@/api/owner/stores"; +import type { Day } from "@/types/store"; interface StoreSettingsProps { storeId?: string; } -const days = ["월", "화", "수", "목", "금", "토", "일"]; +const days = ["월", "화", "수", "목", "금", "토", "일"] as const; +type DayKor = (typeof days)[number]; -const dayMapFromApi: Record = { +const dayMapFromApi: Record = { MONDAY: "월", TUESDAY: "화", WEDNESDAY: "수", @@ -28,7 +30,7 @@ const dayMapFromApi: Record = { SUNDAY: "일", }; -const dayMapToApi: Record = { +const dayMapToApi: Record = { 월: "MONDAY", 화: "TUESDAY", 수: "WEDNESDAY", @@ -47,7 +49,7 @@ const StoreSettings: React.FC = ({ storeId }) => { const [openTime, setOpenTime] = useState("11:00"); const [closeTime, setCloseTime] = useState("22:00"); - const [closedDays, setClosedDays] = useState([]); + const [closedDays, setClosedDays] = useState([]); const [reservationPeriod, setReservationPeriod] = useState("1주일 전까지"); const [minGuests, setMinGuests] = useState(1); @@ -87,12 +89,13 @@ const StoreSettings: React.FC = ({ storeId }) => { setNewFiles([]); } catch (e) { alert("가게 정보를 불러오는데 실패했습니다."); + console.error(e); return; } })(); }, [storeId]); - const toggleDay = (day: string) => { + const toggleDay = (day: DayKor) => { setClosedDays((prev) => prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day], ); @@ -170,7 +173,7 @@ const StoreSettings: React.FC = ({ storeId }) => {
= ({ storeId }) => {
= ({ storeId }) => {
= ({ storeId }) => {
= ({ storeId }) => { }); } catch (e) { alert("기본 정보 저장에 실패했습니다."); + console.error(e); return; } @@ -451,6 +455,7 @@ const StoreSettings: React.FC = ({ storeId }) => { alert( "영업시간 저장에 실패했습니다. 기본 정보는 저장되었습니다.", ); + console.error(e); return; } try { @@ -459,6 +464,7 @@ const StoreSettings: React.FC = ({ storeId }) => { } } catch (e) { alert("이미지 삭제에 실패했습니다."); + console.error(e); return; } try { @@ -467,6 +473,7 @@ const StoreSettings: React.FC = ({ storeId }) => { } } catch (e) { alert("이미지 업로드에 실패했습니다."); + console.error(e); return; } diff --git a/src/components/owner/TableCreateModal.tsx b/src/components/owner/TableCreateModal.tsx index 39b95e3..cd77731 100644 --- a/src/components/owner/TableCreateModal.tsx +++ b/src/components/owner/TableCreateModal.tsx @@ -16,7 +16,7 @@ const TableCreateModal: React.FC = ({ onClose, onConfirm }) => { onClick={onClose} >
e.stopPropagation()} role="dialog" aria-modal="true" diff --git a/src/components/owner/menuFormModal.tsx b/src/components/owner/menuFormModal.tsx index ba85517..a278c75 100644 --- a/src/components/owner/menuFormModal.tsx +++ b/src/components/owner/menuFormModal.tsx @@ -7,13 +7,14 @@ import { type MenuUpdateItem, } from "@/api/owner/menus"; import { deleteMenuImage } from "@/api/owner/menus"; +import type { MenuCategory, MenuItem } from "@/types/menus"; interface MenuFormModalProps { isOpen: boolean; onClose: () => void; - onSubmit: (menuData: any) => void; + onSubmit: (menuData: MenuItem) => void; categories: { id: string; label: string }[]; - editingMenu?: any; + editingMenu?: MenuItem; storeId: string; onImageDelete?: () => void; } @@ -27,7 +28,12 @@ const MenuFormModal: React.FC = ({ storeId, onImageDelete, }) => { - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState<{ + name: string; + category: MenuCategory; + price: string; + description: string; + }>({ name: "", category: "MAIN", price: "", @@ -91,18 +97,21 @@ const MenuFormModal: React.FC = ({ payload.imageKey = editingMenu.imageKey; } - if (isEditing) { + if (isEditing && editingMenu) { const res = await updateMenu(storeId, editingMenu.id, payload); if (res.isSuccess) { alert("메뉴가 성공적으로 수정되었습니다."); onSubmit({ - ...formData, id: String(editingMenu.id), - imageUrl: res.result.imageUrl ?? null, - imageKey: imageKey ?? null, + restaurantId: storeId, + name: formData.name, + category: formData.category, + price: Number(formData.price), + description: formData.description.trim() || "", + imageUrl: res.result.imageUrl ?? undefined, + imageKey: imageKey ?? editingMenu.imageKey ?? undefined, isActive: editingMenu?.isActive ?? true, isSoldOut: editingMenu?.isSoldOut ?? false, - price: Number(formData.price), }); onClose(); } else { @@ -113,10 +122,14 @@ const MenuFormModal: React.FC = ({ if (res.isSuccess) { alert("메뉴 등록 성공!"); onSubmit({ - ...formData, id: String(res.result.menus[0].menuId), + restaurantId: storeId, + name: formData.name, + category: formData.category, + price: Number(formData.price), + description: formData.description.trim() || "", imageUrl: res.result.menus[0].imageUrl, - imageKey: res.result.menus[0].imageKey || null, + imageKey: res.result.menus[0].imageKey ?? undefined, isActive: true, isSoldOut: false, }); @@ -269,7 +282,10 @@ const MenuFormModal: React.FC = ({ className="cursor-pointer w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition-all text-gray-700 bg-white" value={formData.category} onChange={(e) => - setFormData({ ...formData, category: e.target.value }) + setFormData({ + ...formData, + category: e.target.value as MenuCategory, + }) } > {categories diff --git a/src/components/owner/tableDashboard.tsx b/src/components/owner/tableDashboard.tsx index 9d99784..aad17ce 100644 --- a/src/components/owner/tableDashboard.tsx +++ b/src/components/owner/tableDashboard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Store, Plus, Clock, Pencil, Check, X, Lightbulb } from "lucide-react"; import TableCreateModal from "./TableCreateModal"; import BreakTimeModal, { type BreakTime } from "./BreakTimeModal"; @@ -11,10 +11,16 @@ import { type CreateTableRequest, type LayoutTable, } from "@/api/owner/storeLayout"; -import { patchTableInfo, type UpdatedTable } from "@/api/owner/table"; +import { + patchTableInfo, + type PatchTableRequest, + type UpdatedTable, +} from "@/api/owner/table"; import { patchBreakTime } from "@/api/owner/reservation"; import { SEATS_TYPE_LABEL, type SeatsType } from "@/types/table"; import TableDetailModal from "./tableDetailModal"; +import { getErrorMessage } from "@/utils/error"; +import axios from "axios"; interface TableDashboardProps { storeId: number; @@ -89,7 +95,7 @@ const TableDashboard: React.FC = ({ ? `table-dashboard-state-${storeId}` : "table-dashboard-state-temp"; - const getSavedData = () => { + const initialData = (() => { try { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : null; @@ -97,9 +103,7 @@ const TableDashboard: React.FC = ({ console.error("로컬스토리지 파싱 실패", e); return null; } - }; - - const initialData = useMemo(() => getSavedData(), []); + })(); const [config, setConfig] = useState<{ columns: number; rows: number }>( initialData?.config ?? { columns: 0, rows: 0 }, @@ -241,7 +245,7 @@ const TableDashboard: React.FC = ({ isEditingNum: false, isSaved: true, tableImageUrl: - (t as any).tableImageUrl ?? + t.tableImageUrl ?? tableData[t.gridY * layout.gridInfo.gridCol + t.gridX + 1] ?.tableImageUrl ?? null, @@ -272,7 +276,7 @@ const TableDashboard: React.FC = ({ }; fetchLayout(); - }, [storeId]); + }, [storeId, tableData]); const handleCreateLayout = async (columns: number, rows: number) => { if (!storeId) return; @@ -311,8 +315,7 @@ const TableDashboard: React.FC = ({ ]); const slotId = (data.gridY - 1) * config.columns + data.gridX; - const extractedNum = - extractLeadingNumber((newTable as any).tableNumber) ?? slotId; + const extractedNum = extractLeadingNumber(newTable.tableNumber) ?? slotId; setTableData((prev) => ({ ...prev, @@ -370,17 +373,25 @@ const TableDashboard: React.FC = ({ setSelectedTable(null); alert("테이블이 삭제되었습니다."); - } catch (e: any) { - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 400) { - alert("미래 예약이 있어 삭제할 수 없습니다. 예약을 먼저 취소해주세요."); + alert( + message || + "미래 예약이 있어 삭제할 수 없습니다. 예약을 먼저 취소해주세요.", + ); } else if (status === 404) { alert( - "가게 또는 테이블을 찾을 수 없습니다. 새로고침 후 다시 시도하세요.", + message || + "가게 또는 테이블을 찾을 수 없습니다. 새로고침 후 다시 시도하세요.", ); } else { - console.error(e); - alert("테이블 삭제 중 오류가 발생했습니다. 콘솔을 확인하세요."); + alert( + message || "테이블 삭제 중 오류가 발생했습니다. 콘솔을 확인하세요.", + ); } } }; @@ -389,9 +400,9 @@ const TableDashboard: React.FC = ({ tableNumber?: number | null; min?: number | null; max?: number | null; - seatsType?: string | null; + seatsType?: PatchTableRequest["seatsType"] | null; }) => { - const body: any = {}; + const body: PatchTableRequest = {}; if (opts.tableNumber !== null && opts.tableNumber !== undefined) body.tableNumber = String(opts.tableNumber); if (opts.min !== null && opts.min !== undefined) @@ -417,7 +428,7 @@ const TableDashboard: React.FC = ({ tableNumber?: number; min?: number; max?: number; - seatsType?: string; + seatsType?: PatchTableRequest["seatsType"]; }, ) => { if (!storeId) { @@ -433,8 +444,9 @@ const TableDashboard: React.FC = ({ max: changes.max ?? null, seatsType: changes.seatsType ?? null, }); - } catch (err: any) { - alert(err.message); + } catch (error: unknown) { + const message = getErrorMessage(error); + alert(message); return; } @@ -479,10 +491,8 @@ const TableDashboard: React.FC = ({ ...pt, tableInfo: { ...pt.tableInfo, - minCapacity: - (match as any).minSeatCount ?? pt.tableInfo.minCapacity, - maxCapacity: - (match as any).maxSeatCount ?? pt.tableInfo.maxCapacity, + minCapacity: match.minSeatCount ?? pt.tableInfo.minCapacity, + maxCapacity: match.maxSeatCount ?? pt.tableInfo.maxCapacity, numValue: extractLeadingNumber(match.tableNumber) ?? pt.tableInfo.numValue, @@ -494,15 +504,19 @@ const TableDashboard: React.FC = ({ ); alert("테이블 정보가 업데이트 되었습니다."); - } catch (e: any) { - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 400) { alert("잘못된 요청입니다. (좌석 범위 오류 또는 수정 필드 없음)"); } else if (status === 404) { alert("가게 또는 테이블을 찾을 수 없습니다."); } else { - console.error(e); - alert("테이블 수정 중 오류가 발생했습니다. 콘솔을 확인하세요."); + alert( + message || "테이블 수정 중 오류가 발생했습니다. 콘솔을 확인하세요", + ); } } }; @@ -546,19 +560,19 @@ const TableDashboard: React.FC = ({ } alert("브레이크 타임이 설정되었습니다."); - } catch (err: any) { - console.error("브레이크타임 설정 실패", err?.response?.data ?? err); - const status = err?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); + if (status === 400) { - alert( - err?.response?.data?.message ?? "잘못된 브레이크타임 요청입니다.", - ); + alert(message || "잘못된 브레이크타임 요청입니다."); } else if (status === 404) { alert("가게를 찾을 수 없습니다."); } else { alert( - err?.response?.data?.message ?? - "브레이크타임 설정에 실패했습니다. 콘솔을 확인하세요.", + message || "브레이크타임 설정에 실패했습니다. 콘솔을 확인하세요.", ); } } @@ -681,7 +695,7 @@ const TableDashboard: React.FC = ({ }} > {Array.from({ length: config.columns * config.rows }).map( - (_: any, i: number) => { + (_, i: number) => { const id = i + 1; const table = getTableData(id); const style = getTableStyle(table.maxCapacity); @@ -858,7 +872,7 @@ const TableDashboard: React.FC = ({ e.stopPropagation(); startEditingCapacity(id); }} - className={`${style.badge} text-white px-2 py-2 rounded-sm text-xs shadow-md min-w-[60px] text-center transition-transform active:scale-95`} + className={`${style.badge} text-white px-2 py-2 rounded-sm text-xs shadow-md min-w-15 text-center transition-transform active:scale-95`} > {table.minCapacity}~{table.maxCapacity}인
diff --git a/src/components/owner/tableDetailModal.tsx b/src/components/owner/tableDetailModal.tsx index b25ff57..723c5ba 100644 --- a/src/components/owner/tableDetailModal.tsx +++ b/src/components/owner/tableDetailModal.tsx @@ -31,6 +31,8 @@ import { } from "@/api/owner/table"; import { cancelBookingByOwner } from "@/api/owner/reservation"; import { SEATS_TYPE_LABEL, type SeatsType } from "@/types/table"; +import axios, { type AxiosProgressEvent } from "axios"; +import { getErrorMessage } from "@/utils/error"; interface TableInfo { minCapacity: number; @@ -159,9 +161,9 @@ const TableDetailModal: React.FC = ({ onUpdateCapacity(Number(tempMin), Number(tempMax)); setIsEditing(false); - } catch (e: any) { - console.error("테이블 정보 수정 실패", e?.response?.data ?? e); - alert(e?.response?.data?.message ?? "테이블 정보 수정에 실패했습니다."); + } catch (error: unknown) { + const message = getErrorMessage(error); + alert(message); } }; @@ -185,11 +187,10 @@ const TableDetailModal: React.FC = ({ setError(null); const res = await getTableSlots(storeId, tableId, formatDate(date)); setSlots(res.data.result.slots); - } catch (e: any) { - console.error("슬롯 조회 실패", e?.response?.data ?? e); - setError( - e?.response?.data?.message ?? "예약 정보를 불러오지 못했습니다.", - ); + } catch (error: unknown) { + const message = + getErrorMessage(error) || "예약 정보를 불러오지 못했습니다"; + setError(message); } finally { setLoading(false); } @@ -214,15 +215,14 @@ const TableDetailModal: React.FC = ({ partySize: result.partySize, amount: result.amount, }); - } catch (e: any) { - console.error("예약 상세 조회 실패", e?.response?.data ?? e); - const status = e?.response?.status; + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + const message = getErrorMessage(error); if (status === 403) setDetailError("접근 권한이 없습니다."); else if (status === 404) setDetailError("해당 예약을 찾을 수 없습니다."); - else - setDetailError( - e?.response?.data?.message ?? "예약 상세를 불러오지 못했습니다.", - ); + else setDetailError(message || "예약 상세 내용을 불러오지 못했습니다"); setBookingDetail(null); setBookingDetailBookingId(null); } finally { @@ -257,15 +257,16 @@ const TableDetailModal: React.FC = ({ await updateTableSlotStatus(storeId, tableId, payload); await fetchSlots(selectedFullDate); - } catch (e: any) { - const statusCode = e?.response?.status; - if (statusCode === 404 && nextStatus === "AVAILABLE") { + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + if (status === 404 && nextStatus === "AVAILABLE") { await fetchSlots(selectedFullDate); return; } - - console.error("슬롯 상태 변경 실패", e?.response?.data ?? e); - alert(e?.response?.data?.message ?? "슬롯 상태 변경에 실패했습니다."); + const message = getErrorMessage(error); + alert(message || "슬롯 상태 변경에 실패했습니다"); } finally { setLoading(false); } @@ -301,7 +302,7 @@ const TableDetailModal: React.FC = ({ storeId, tableId, selectedFile, - (ev) => { + (ev: AxiosProgressEvent) => { if (ev.total) setUploadProgress(Math.round((ev.loaded / ev.total) * 100)); }, @@ -314,9 +315,9 @@ const TableDetailModal: React.FC = ({ if (onImageUpload) onImageUpload(tableId, newUrl); alert("이미지 업로드에 성공했습니다."); - } catch (err: any) { - console.error("이미지 업로드 실패", err?.response?.data ?? err); - alert(err?.response?.data?.message ?? "이미지 업로드에 실패했습니다."); + } catch (error: unknown) { + const message = getErrorMessage(error) || "이미지 업로드에 실패했습니다"; + alert(message); } finally { setUploading(false); } @@ -344,11 +345,10 @@ const TableDetailModal: React.FC = ({ } else { alert("이미지 삭제 실패: " + (res.data.message ?? "알 수 없는 오류")); } - } catch (err: any) { - console.error("이미지 삭제 실패", err?.response?.data ?? err); - alert( - err?.response?.data?.message ?? "이미지 삭제 중 오류가 발생했습니다.", - ); + } catch (error: unknown) { + const message = + getErrorMessage(error) || "이미지 삭제 중 오류가 발생했습니다"; + alert(message); } }; @@ -370,12 +370,27 @@ const TableDetailModal: React.FC = ({ setShowBookingDetail(false); setBookingDetail(null); if (selectedFullDate) fetchSlots(selectedFullDate); - } catch (err: any) { - console.error("예약 취소 실패", err?.response?.data ?? err); - const status = err?.response?.status; - if (status === 403) alert("접근 권한이 없습니다."); - else if (status === 404) alert("예약 정보를 찾을 수 없습니다."); - else alert(err?.response?.data?.message ?? "예약 취소에 실패했습니다."); + } catch (error: unknown) { + const status = axios.isAxiosError(error) + ? error.response?.status + : undefined; + + if (status === 403) { + alert("접근 권한이 없습니다"); + return; + } + if (status === 404) { + alert("예약 정보를 찾을 수 없습니다"); + setShowBookingDetail(false); + setBookingDetail(null); + setBookingDetailBookingId(null); + if (selectedFullDate) { + await fetchSlots(selectedFullDate); + } + return; + } + const message = getErrorMessage(error); + alert(message || "예약 취소에 실패했습니다"); } finally { setDetailLoading(false); } @@ -416,7 +431,7 @@ const TableDetailModal: React.FC = ({ className="bg-white w-full max-w-2xl rounded-lg shadow-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200" onClick={(e) => e.stopPropagation()} > -
+
{step !== "DETAIL" && (
-
+
인원
@@ -833,7 +848,7 @@ const TableDetailModal: React.FC = ({ 날짜 변경
-
+
{slots.map((slot) => { const isBreak = isBreakTime(slot.time, breakTimes); const isAvailable = !isBreak && slot.status === "AVAILABLE"; diff --git a/src/components/reservation/modals/PaymentModal.tsx b/src/components/reservation/modals/PaymentModal.tsx index 24033f6..3d9934c 100644 --- a/src/components/reservation/modals/PaymentModal.tsx +++ b/src/components/reservation/modals/PaymentModal.tsx @@ -26,6 +26,16 @@ type Props = { draft: ReservationDraft; booking: CreateBookingResult | null; }; +type TossPaymentsInstance = Awaited>; +type TossWidgetsInstance = ReturnType; + +type PaymentMethodWidgetInstance = Awaited< + ReturnType +>; + +type AgreementWidgetInstance = Awaited< + ReturnType +>; export default function PaymentModal({ open, @@ -48,10 +58,12 @@ export default function PaymentModal({ const amount = booking?.totalDeposit ?? 0; - const paymentMethodWidgetRef = useRef(null); - const agreementWidgetRef = useRef(null); + const paymentMethodWidgetRef = useRef( + null, + ); + const agreementWidgetRef = useRef(null); - const widgetsRef = useRef(null); + const widgetsRef = useRef(null); const initedRef = useRef(false); const payOrderRef = useRef<{ @@ -78,14 +90,17 @@ export default function PaymentModal({ if (!booking) return; let cancelled = false; - (async () => { + const paymentContainer = paymentMethodContainerRef.current; + const agreementContainer = agreementContainerRef.current; + + void (async () => { try { setLoading(true); - if (paymentMethodContainerRef.current) { - paymentMethodContainerRef.current.innerHTML = ""; + if (paymentContainer) { + paymentContainer.innerHTML = ""; } - if (agreementContainerRef.current) { - agreementContainerRef.current.innerHTML = ""; + if (agreementContainer) { + agreementContainer.innerHTML = ""; } const clientKey = import.meta.env.VITE_TOSS_CLIENT_KEY as | string @@ -108,6 +123,7 @@ export default function PaymentModal({ const tossPayments = await loadTossPayments(clientKey); if (cancelled) return; + const customerKey = `user_${userId}`; const widgets = tossPayments.widgets({ customerKey }); widgetsRef.current = widgets; @@ -139,20 +155,20 @@ export default function PaymentModal({ try { paymentMethodWidgetRef.current?.destroy?.(); agreementWidgetRef.current?.destroy?.(); - } catch {} - + } catch (error: unknown) { + console.error("토스 위젯 정리중 오류발생", error); + } paymentMethodWidgetRef.current = null; agreementWidgetRef.current = null; - widgetsRef.current = null; initedRef.current = false; payOrderRef.current = null; - if (paymentMethodContainerRef.current) { - paymentMethodContainerRef.current.innerHTML = ""; + if (paymentContainer) { + paymentContainer.innerHTML = ""; } - if (agreementContainerRef.current) { - agreementContainerRef.current.innerHTML = ""; + if (agreementContainer) { + agreementContainer.innerHTML = ""; } }; }, [open, booking, nav, userId]); diff --git a/src/components/reservation/modals/ReservationCompleteModal.tsx b/src/components/reservation/modals/ReservationCompleteModal.tsx index 4e2899c..5090491 100644 --- a/src/components/reservation/modals/ReservationCompleteModal.tsx +++ b/src/components/reservation/modals/ReservationCompleteModal.tsx @@ -10,8 +10,8 @@ import { useEffect } from "react"; type Props = { open: boolean; - restaurant: RestaurantDetail; - draft: ReservationDraft; + restaurant: Pick; + draft: Pick; onClose: () => void; autoCloseMs?: number; }; diff --git a/src/components/reservation/modals/ReservationConfirmModal.tsx b/src/components/reservation/modals/ReservationConfirmModal.tsx index e14015c..f08aabf 100644 --- a/src/components/reservation/modals/ReservationConfirmModal.tsx +++ b/src/components/reservation/modals/ReservationConfirmModal.tsx @@ -9,6 +9,7 @@ import type { ReservationDraft } from "@/types/restaurant"; import type { RestaurantDetail } from "@/types/store"; import { toYmd } from "@/utils/date"; import { toDepositRate } from "@/utils/depositRate"; +import { getErrorMessage } from "@/utils/error"; import { calcMenuTotal } from "@/utils/menu"; import { backdropMotionClass, panelMotionClass } from "@/utils/modalMotion"; import { formatKrw } from "@/utils/money"; @@ -59,16 +60,21 @@ export default function ReservationConfirmModal({ return; } const tableId = draft.tableId; + const time = draft.time; if (!restaurant.id) return; if (createBookingMutation.isPending) return; + + if (!time) { + alert("예약 시간을 먼저 선택해주세요"); + return; + } if (typeof tableId !== "number" || tableId <= 0) { alert("테이블을 먼저 선택해주세요"); return; } const body = { date: toYmd(draft.date), - time: draft.time, - + time, partySize: draft.people, tableIds: [tableId], menuItems: (draft.selectedMenus ?? []).map((m) => ({ @@ -84,9 +90,9 @@ export default function ReservationConfirmModal({ body, }); onConfirm(result); - } catch (err) { - const msg = (err as any)?.message ?? "예약 생성에 실패했습니다."; - alert(msg); + } catch (error) { + const message = getErrorMessage(error) || "예약 생성에 실패했습니다"; + alert(message); } }; diff --git a/src/components/reservation/modals/ReservationMenuModal.tsx b/src/components/reservation/modals/ReservationMenuModal.tsx index 1ec3d24..b589f7b 100644 --- a/src/components/reservation/modals/ReservationMenuModal.tsx +++ b/src/components/reservation/modals/ReservationMenuModal.tsx @@ -1,7 +1,13 @@ import type { ReservationDraft } from "@/types/restaurant"; import { Minus, Plus, X } from "lucide-react"; import { Button } from "../../ui/button"; -import type { SelectedMenu, MenuCategory, MenuItem } from "@/types/menus"; +import { + type SelectedMenu, + type MenuItem, + type MenuCategory, + type UiCategory, + MenuCategoryLabel, +} from "@/types/menus"; import { useMenus } from "@/hooks/reservation/useMenus"; import { useEffect, useMemo, useState } from "react"; import { calcMenuTotal } from "@/utils/menu"; @@ -24,15 +30,6 @@ type Props = { draft: ReservationDraft; }; -const CategoryLabel: Record = { - MAIN: "메인 메뉴", - SIDE: "사이드 메뉴", - DRINK: "음료", - OTHER: "기타", -}; - -type UiCategory = MenuCategory | "OTHER"; - export default function ReservationMenuModal({ open, restaurant, @@ -77,7 +74,9 @@ export default function ReservationMenuModal({ return "SIDE"; case "DRINK": case "BEVERAGE": - return "DRINK"; + return "BEVERAGE"; + case "ALCOHOL": + return "ALCOHOL"; default: return "OTHER"; } @@ -87,7 +86,8 @@ export default function ReservationMenuModal({ const by: Record = { MAIN: [], SIDE: [], - DRINK: [], + BEVERAGE: [], + ALCOHOL: [], OTHER: [], }; for (const m of activeMenus ?? []) { @@ -183,116 +183,118 @@ export default function ReservationMenuModal({ 아직 등록된 메뉴가 없어요
) : ( - (["MAIN", "SIDE", "DRINK"] as MenuCategory[]).map((cat) => { - const list = grouped[cat]; - if (list.length === 0) return null; - const safeLabel = CategoryLabel[cat] ?? "기타"; + (["MAIN", "SIDE", "BEVERAGE", "ALCOHOL"] as MenuCategory[]).map( + (cat) => { + const list = grouped[cat]; + if (list.length === 0) return null; + const safeLabel = MenuCategoryLabel[cat] ?? "기타"; - return ( -
-
{safeLabel}
-
- {list.map((menu) => { - const qty = qtyMap.get(Number(menu.id)) ?? 0; - const img = - menu.imageUrl && menu.imageUrl.trim().length > 0 - ? menu.imageUrl - : "/modernKoreaRestaurant.jpg"; - return ( -
-
- {/* 음식사진 */} -
- {menu.name} +
{safeLabel}
+
+ {list.map((menu) => { + const qty = qtyMap.get(Number(menu.id)) ?? 0; + const img = + menu.imageUrl && menu.imageUrl.trim().length > 0 + ? menu.imageUrl + : "/modernKoreaRestaurant.jpg"; + return ( +
+
+ {/* 음식사진 */} +
+ {menu.name} + {menu.isSoldOut && ( +
+ + 품절 + +
)} - /> - {menu.isSoldOut && ( -
- - 품절 - -
- )} -
- {/* 내용 */} -
-
-
-
-

- {menu.name} -

- {menu.description ? ( -

- {menu.description} +

+ {/* 내용 */} +
+
+
+
+

+ {menu.name}

+ {menu.description ? ( +

+ {menu.description} +

+ ) : null} +
+ {qty > 0 ? ( + + {qty}개 + ) : null}
- {qty > 0 ? ( - - {qty}개 - - ) : null} +

+ {formatKrw(menu.price)}원 +

-

- {formatKrw(menu.price)}원 -

-
- {/* 수량 조절 */} -
-
- - {qty} - + {/* 수량 조절 */} +
+
+ + {qty} + +
-
- ); - })} -
-
- ); - }) + ); + })} +
+
+ ); + }, + ) )} diff --git a/src/components/reservation/modals/ReservationModal.tsx b/src/components/reservation/modals/ReservationModal.tsx index e55697a..a877b4c 100644 --- a/src/components/reservation/modals/ReservationModal.tsx +++ b/src/components/reservation/modals/ReservationModal.tsx @@ -83,7 +83,7 @@ export default function ReservationModal({ if (initialDraft) { setPeople(initialDraft.people); setDate(initialDraft.date); - setTime(initialDraft.time); + setTime(initialDraft.time ?? ""); setSeatType(initialDraft.seatType); setTablePref(initialDraft.tablePref); setSelectedTableId(initialDraft.tableId); @@ -264,7 +264,7 @@ export default function ReservationModal({ - +

- {categoryLabel[restaurant.category]} • {restaurant.address} + {storeCategoryLabel[restaurant.category]} • {restaurant.address}

diff --git a/src/components/store-registration/CompleteModal.tsx b/src/components/store-registration/CompleteModal.tsx index 4c3ca03..4d44789 100644 --- a/src/components/store-registration/CompleteModal.tsx +++ b/src/components/store-registration/CompleteModal.tsx @@ -8,7 +8,7 @@ import { import { useEffect } from "react"; import type { StoreInfoFormValues } from "./StoreInfo.schema"; import { Check } from "lucide-react"; -import { categoryLabel } from "@/types/store"; +import { storeCategoryLabel } from "@/types/store"; interface CompleteModalProps { isOpen: boolean; @@ -52,7 +52,7 @@ export default function CompleteModal({
음식 종류 - {data.category ? categoryLabel[data.category] : "-"} + {data.category ? (storeCategoryLabel[data.category] ?? "-") : "-"}
diff --git a/src/components/store-registration/MenuItemInput.tsx b/src/components/store-registration/MenuItemInput.tsx index 3525e32..93244b5 100644 --- a/src/components/store-registration/MenuItemInput.tsx +++ b/src/components/store-registration/MenuItemInput.tsx @@ -17,6 +17,7 @@ import { } from "react"; import { Trash2, Upload, X } from "lucide-react"; import { Label } from "@/components/ui/label"; +import { MenuCategoryLabel } from "@/types/menus"; interface MenuItemInputProps { index: number; @@ -28,13 +29,6 @@ interface MenuItemInputProps { trigger: UseFormTrigger; } -const CATEGORY_LABELS: Record = { - MAIN: "메인 메뉴", - SIDE: "사이드 메뉴", - BEVERAGE: "음료", - ALCOHOL: "주류", -}; - export default function MenuItemInput({ index, onDelete, @@ -82,6 +76,10 @@ export default function MenuItemInput({ setValue(`menus.${index}.imageKey`, undefined, { shouldValidate: true }); }; + const imageError = errors.menus?.[index]?.imageKey; + const imageErrorMessage = + typeof imageError?.message === "string" ? imageError.message : undefined; + return (
@@ -149,10 +147,8 @@ export default function MenuItemInput({

• 최대 용량: 1MB

• 형식: JPG(JPEG), PNG

- {errors.menus?.[index]?.imageKey && ( -

- • {(errors.menus[index]?.imageKey as any).message} -

+ {imageErrorMessage && ( +

• {imageErrorMessage}

)}
@@ -225,7 +221,7 @@ export default function MenuItemInput({ {...field} className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer" > - {Object.entries(CATEGORY_LABELS).map(([value, label]) => ( + {Object.entries(MenuCategoryLabel).map(([value, label]) => ( diff --git a/src/components/store-registration/RegistrationStepper.tsx b/src/components/store-registration/RegistrationStepper.tsx index e662956..4c2f9ae 100644 --- a/src/components/store-registration/RegistrationStepper.tsx +++ b/src/components/store-registration/RegistrationStepper.tsx @@ -41,7 +41,7 @@ export default function RegistrationStepper({
{index !== steps.length - 1 && (
step.number ? "bg-blue-500" : "bg-gray-200" }`} style={{ minWidth: "80px" }} diff --git a/src/components/store-registration/StepBusinessAuth.tsx b/src/components/store-registration/StepBusinessAuth.tsx index 8970f50..9b6090f 100644 --- a/src/components/store-registration/StepBusinessAuth.tsx +++ b/src/components/store-registration/StepBusinessAuth.tsx @@ -12,6 +12,7 @@ import { getErrorMessage } from "@/utils/error"; import { useNavigate } from "react-router-dom"; import { logout } from "@/api/auth"; import ConfirmModal from "./ConfirmModal"; +import axios from "axios"; interface StepBusinessAuthProps { defaultValues: { @@ -53,7 +54,7 @@ export default function StepBusinessAuth({ const { register, handleSubmit, - watch, + getValues, formState: { isValid, errors, touchedFields }, } = useForm({ resolver: zodResolver(BusinessAuthSchema), @@ -65,10 +66,6 @@ export default function StepBusinessAuth({ }, }); - const name = watch("name"); - const businessNumber = watch("businessNumber"); - const startDate = watch("startDate"); - const onSubmit = async (data: BusinessAuthFormValues) => { verifyOwner(data, { onSuccess: async () => { @@ -77,8 +74,9 @@ export default function StepBusinessAuth({ nav("/", { replace: true }); }, onError: (error) => { - const err = error as any; - const serverCode = err.response?.data?.code || err.code; + const serverCode = axios.isAxiosError(error) + ? error.response?.data?.code + : undefined; if (serverCode === "OWNER409") { setPendingData(data); return; @@ -130,7 +128,9 @@ export default function StepBusinessAuth({ variant="primary" />
{ + void handleSubmit(onSubmit)(e); + }} className="max-w-md mx-auto space-y-6 sm:space-y-8" >
@@ -149,6 +149,7 @@ export default function StepBusinessAuth({ {...register("name", { onChange: (e) => { if (isVerified) { + const { businessNumber, startDate } = getValues(); setIsVerified(false); onComplete({ name: e.target.value, @@ -192,11 +193,12 @@ export default function StepBusinessAuth({ {...register("businessNumber", { onChange: (e) => { if (isVerified) { + const { name, businessNumber } = getValues(); setIsVerified(false); onComplete({ name, - businessNumber: e.target.value, - startDate, + businessNumber, + startDate: e.target.value, isVerified: false, }); } @@ -236,11 +238,12 @@ export default function StepBusinessAuth({ {...register("startDate", { onChange: (e) => { if (isVerified) { + const { name, startDate } = getValues(); setIsVerified(false); onComplete({ name, - businessNumber, - startDate: e.target.value, + businessNumber: e.target.value, + startDate, isVerified: false, }); } diff --git a/src/components/store-registration/StepMenuRegistration.tsx b/src/components/store-registration/StepMenuRegistration.tsx index 6560d8f..1f271d2 100644 --- a/src/components/store-registration/StepMenuRegistration.tsx +++ b/src/components/store-registration/StepMenuRegistration.tsx @@ -1,6 +1,6 @@ import { Plus } from "lucide-react"; import { useEffect } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; +import { useFieldArray, useForm, useWatch } from "react-hook-form"; import { MenuSchema, type MenuFormValues } from "./Menu.schema"; import { zodResolver } from "@hookform/resolvers/zod"; import MenuItemInput from "./MenuItemInput"; @@ -18,9 +18,7 @@ export default function StepMenuRegistration({ register, control, setValue, - watch, trigger, - getValues, formState: { errors, isValid }, } = useForm({ resolver: zodResolver(MenuSchema), @@ -35,13 +33,11 @@ export default function StepMenuRegistration({ name: "menus", }); + const menus = useWatch({ control, name: "menus" }); + useEffect(() => { - const subscription = watch((value) => { - onChange(isValid, value as MenuFormValues); - }); - onChange(isValid, getValues()); - return () => subscription.unsubscribe(); - }, [watch, isValid, onChange, getValues]); + onChange(isValid, { menus: menus ?? [] }); + }, [menus, isValid, onChange]); return (
diff --git a/src/components/store-registration/StepStoreInfo.tsx b/src/components/store-registration/StepStoreInfo.tsx index 13ff869..c32fcbf 100644 --- a/src/components/store-registration/StepStoreInfo.tsx +++ b/src/components/store-registration/StepStoreInfo.tsx @@ -1,18 +1,17 @@ -import { Controller, useForm } from "react-hook-form"; +import { Controller, useForm, useWatch } from "react-hook-form"; import { Label } from "../ui/label"; import { StoreInfoSchema, type StoreInfoFormValues } from "./StoreInfo.schema"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { phoneNumber } from "@/utils/phoneNumber"; import DaumPostcodeEmbed from "react-daum-postcode"; import { loadKakaoMapSdk } from "@/lib/kakao"; import { Upload, X } from "lucide-react"; - -declare global { - interface Window { - kakao: any; - } -} +import type { AddressSearchResult } from "@/types/store"; +import type { + KakaoAddressSearchResult, + KakaoAddressSearchStatus, +} from "@/types/kakao"; interface StepStoreInfoProps { defaultValues: Partial; @@ -33,19 +32,15 @@ export default function StepStoreInfo({ onChange, }: StepStoreInfoProps) { const [isOpenPostcode, setIsOpenPostcode] = useState(false); - - const [previewUrl, setPreviewUrl] = useState(null); const fileInputRef = useRef(null); const { register, control, - watch, setValue, trigger, - getValues, formState: { errors, isValid, touchedFields }, - } = useForm({ + } = useForm({ resolver: zodResolver(StoreInfoSchema), mode: "onChange", defaultValues: { @@ -68,23 +63,28 @@ export default function StepStoreInfo({ }, }); - const watchedMainImage = watch("mainImage"); + const watchedMainImage = useWatch({ + control, + name: "mainImage", + }); - useEffect(() => { - if (watchedMainImage && watchedMainImage instanceof File) { - const url = URL.createObjectURL(watchedMainImage); - setPreviewUrl(url); + const formValues = useWatch({ control }); - return () => { - URL.revokeObjectURL(url); - }; - } else if (typeof watchedMainImage === "string") { - setPreviewUrl(watchedMainImage); - } else { - setPreviewUrl(null); + const previewUrl = useMemo(() => { + if (watchedMainImage instanceof File) { + return URL.createObjectURL(watchedMainImage); } + return null; }, [watchedMainImage]); + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -99,7 +99,7 @@ export default function StepStoreInfo({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - setValue("mainImage", null, { shouldValidate: true }); + setValue("mainImage", undefined, { shouldValidate: true }); }; useEffect(() => { @@ -123,20 +123,11 @@ export default function StepStoreInfo({ loadKakaoMapSdk().catch((err) => console.error("카카오맵 로드 실패:", err)); }, []); - const onChangeRef = useRef(onChange); - onChangeRef.current = onChange; - useEffect(() => { - const subscription = watch((value) => { - onChangeRef.current(isValid, value as StoreInfoFormValues); - }); - - onChangeRef.current(isValid, getValues() as StoreInfoFormValues); + onChange(isValid, formValues as StoreInfoFormValues); + }, [formValues, isValid, onChange]); - return () => subscription.unsubscribe(); - }, [watch, isValid, getValues]); - - const handleAddressComplete = (data: any) => { + const handleAddressComplete = (data: AddressSearchResult) => { let fullAddress = data.address; let extraAddress = ""; @@ -155,23 +146,30 @@ export default function StepStoreInfo({ setValue("bname", data.bname); if (window.kakao?.maps?.services) { + const maps = window.kakao.maps; const geocoder = new window.kakao.maps.services.Geocoder(); - geocoder.addressSearch(fullAddress, (result: any, status: any) => { - if (status === window.kakao.maps.services.Status.OK) { - const lat = parseFloat(result[0].y); - const lng = parseFloat(result[0].x); - - setValue("latitude", lat, { shouldValidate: true }); - setValue("longitude", lng, { shouldValidate: true }); - - trigger("address"); - } else { - setValue("latitude", 0, { shouldValidate: true }); - setValue("longitude", 0, { shouldValidate: true }); - trigger("address"); - } - }); + geocoder.addressSearch( + fullAddress, + ( + result: KakaoAddressSearchResult[], + status: KakaoAddressSearchStatus, + ) => { + if (status === maps.services.Status.OK) { + const lat = parseFloat(result[0].y); + const lng = parseFloat(result[0].x); + + setValue("latitude", lat, { shouldValidate: true }); + setValue("longitude", lng, { shouldValidate: true }); + + trigger("address"); + } else { + setValue("latitude", 0, { shouldValidate: true }); + setValue("longitude", 0, { shouldValidate: true }); + trigger("address"); + } + }, + ); } else { alert("지도 서비스 로딩에 실패했습니다. 잠시 후 다시 시도해주세요."); setValue("latitude", 0, { shouldValidate: true }); @@ -182,6 +180,12 @@ export default function StepStoreInfo({ setIsOpenPostcode(false); }; + const mainImageError = errors.mainImage; + const mainImageErrorMessage = + typeof mainImageError?.message === "string" + ? mainImageError.message + : undefined; + return (
@@ -465,9 +469,9 @@ export default function StepStoreInfo({

• 최대 용량: 1MB

• 형식: JPG(JPEG), PNG

- {errors.mainImage && ( + {mainImageErrorMessage && (

- • {(errors.mainImage as any).message} + • {mainImageErrorMessage}

)}
@@ -499,7 +503,7 @@ export default function StepStoreInfo({ aria-modal="true" >
e.stopPropagation()} >