diff --git a/apps/nowait-admin/src/components/NewOrderToast.tsx b/apps/nowait-admin/src/components/NewOrderToast.tsx index 8b16c3a1..470237a9 100644 --- a/apps/nowait-admin/src/components/NewOrderToast.tsx +++ b/apps/nowait-admin/src/components/NewOrderToast.tsx @@ -154,10 +154,16 @@ export default function NewOrderToast() { onTap={() => { const params = new URLSearchParams(); params.set("order", String(t.orderId)); - navigate({ - pathname: `/admin/orders/${storeId}`, - search: params.toString(), - }); + params.set("status", t.status); + navigate( + { + pathname: `/admin/orders/${storeId}`, + search: params.toString(), + }, + { replace: true } + ); + + removeToast(t.id); }} >
{ ); return editMode ? ( - // ✅ 편집 모드: SwipeableRow 사용 안 함 (충돌 차단)
{ + setShowModal(false); + onSuccess?.(); + onClose(); // 디테일 화면도 닫기 + }; + // 타입에 따른 설정 const getConfig = () => { if (type === "payment") { @@ -168,27 +174,15 @@ const DetailCard = ({ className="fixed inset-0 bg-black/30 flex items-center justify-center z-50" onClick={handleCloseModal} > - {type === "cooking" ? ( - - ) : ( - - )} +
)} diff --git a/apps/nowait-admin/src/pages/AdminOrders/OrderCard.tsx b/apps/nowait-admin/src/pages/AdminOrders/OrderCard.tsx index 5ed07875..97720579 100644 --- a/apps/nowait-admin/src/pages/AdminOrders/OrderCard.tsx +++ b/apps/nowait-admin/src/pages/AdminOrders/OrderCard.tsx @@ -125,7 +125,8 @@ const CookCard = ({ return 7; // sm }; - const handleCookCompleteClick = () => { + const handleCookCompleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); // 부모 onClick 이벤트 방지 setShowCookCompleteModal(true); }; diff --git a/apps/nowait-user/src/components/common/modal/SearchModal.tsx b/apps/nowait-user/src/components/common/modal/SearchModal.tsx index 96804479..92960cd4 100644 --- a/apps/nowait-user/src/components/common/modal/SearchModal.tsx +++ b/apps/nowait-user/src/components/common/modal/SearchModal.tsx @@ -121,7 +121,7 @@ const SearchModal = ({ isOpen, onClose }: SearchModalProps) => { {store.profileImage ? ( {`${store.name} ) : ( diff --git a/apps/nowait-user/src/pages/home/components/MainCard.tsx b/apps/nowait-user/src/pages/home/components/MainCard.tsx index 16f6b922..fad670b4 100644 --- a/apps/nowait-user/src/pages/home/components/MainCard.tsx +++ b/apps/nowait-user/src/pages/home/components/MainCard.tsx @@ -16,7 +16,7 @@ interface WaitingCardProps { interface StoreCardProps { type: "store"; storeId: number; - publicCode : string; + publicCode: string; name: string; departmentName: string; profileImageUrl: string; @@ -319,7 +319,7 @@ const StoreCardComponent = ({
-
+
{name}
{status === "open" && (waitingCount || 0) > 0 ? ( diff --git a/apps/nowait-user/src/pages/home/components/MyWaitingCard.tsx b/apps/nowait-user/src/pages/home/components/MyWaitingCard.tsx index 7756bcb2..a26a3956 100644 --- a/apps/nowait-user/src/pages/home/components/MyWaitingCard.tsx +++ b/apps/nowait-user/src/pages/home/components/MyWaitingCard.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect, memo, useCallback, useMemo } from "react"; +import { useState, useEffect, memo, useCallback } from "react"; import { MemoizedRefresh } from "../../../components/icons/MemoizedIcons"; +import { useTimerStore } from "../../../stores/timerStore"; // 대기 중인 주점 데이터 타입 interface WaitingStoreData { @@ -35,15 +36,12 @@ const MyWaitingCard = memo( }: MyWaitingCardProps) => { const [startX, setStartX] = useState(0); const [isDragging, setIsDragging] = useState(false); - const [timeLeft, setTimeLeft] = useState(0); // 현재 남은 시간 (초, CALLING 상태일 때만 설정) - const totalSlides = waitingStores?.length || 0; + // 전역 타이머 상태 사용 + const { setTimer, updateTimer, getTimeLeft, isTimerActive } = + useTimerStore(); - // 슬라이드별 초기 시간 (초 단위) - 모두 10분으로 통일 - const initialTimes = useMemo( - () => Array(totalSlides).fill(600), - [totalSlides] - ); + const totalSlides = waitingStores?.length || 0; // 시간을 MM:SS 형식으로 포맷팅 const formatTime = useCallback((seconds: number) => { @@ -57,31 +55,37 @@ const MyWaitingCard = memo( // 현재 슬라이드의 데이터 가져오기 const currentStore = waitingStores[currentSlide]; - // 슬라이드가 변경될 때 타이머 리셋 (CALLING 상태일 때만) + // 현재 store의 남은 시간 가져오기 + const timeLeft = currentStore ? getTimeLeft(currentStore.storeId) : 0; + + // 슬라이드가 변경될 때 타이머 초기화 (CALLING 상태일 때만) useEffect(() => { - if (currentStore?.status === "CALLING") { - setTimeLeft(initialTimes[currentSlide] || 600); + if (currentStore?.status === "CALLING" && currentStore?.storeId) { + // 타이머가 없거나 비활성화된 경우에만 새로 설정 + if (!isTimerActive(currentStore.storeId)) { + setTimer(currentStore.storeId, 600); // 10분으로 설정 + } } - }, [currentSlide, initialTimes, currentStore?.status]); - - // 타이머 로직 (CALLING 상태일 때만 작동) + }, [ + currentSlide, + currentStore?.status, + currentStore?.storeId, + setTimer, + isTimerActive, + ]); + + // 타이머 업데이트 로직 (CALLING 상태일 때만 작동) useEffect(() => { - // CALLING 상태가 아니면 타이머 비활성화 - if (currentStore?.status !== "CALLING" || timeLeft <= 0) { + if (currentStore?.status !== "CALLING" || !currentStore?.storeId) { return; } const timer = setInterval(() => { - setTimeLeft((prev: number) => { - if (prev <= 1) { - return 0; - } - return prev - 1; - }); + updateTimer(currentStore.storeId); }, 1000); return () => clearInterval(timer); - }, [timeLeft, currentStore?.status]); + }, [currentStore?.status, currentStore?.storeId, updateTimer]); // 터치 시작 const handleTouchStart = useCallback((e: React.TouchEvent) => { diff --git a/apps/nowait-user/src/stores/timerStore.ts b/apps/nowait-user/src/stores/timerStore.ts new file mode 100644 index 00000000..e3cac4ea --- /dev/null +++ b/apps/nowait-user/src/stores/timerStore.ts @@ -0,0 +1,117 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface TimerState { + // 각 store별 타이머 상태를 저장 + timers: Record< + number, + { + timeLeft: number; + startTime: number; // 타이머가 시작된 시간 (timestamp) + isActive: boolean; + } + >; + + // 타이머 설정 (10분 = 600초) + setTimer: (storeId: number, timeLeft: number) => void; + + // 타이머 업데이트 (1초씩 감소) + updateTimer: (storeId: number) => void; + + // 타이머 초기화 + resetTimer: (storeId: number) => void; + + // 특정 store의 남은 시간 가져오기 + getTimeLeft: (storeId: number) => number; + + // 타이머가 활성화되어 있는지 확인 + isTimerActive: (storeId: number) => boolean; + + // 모든 타이머 정리 + clearAllTimers: () => void; +} + +export const useTimerStore = create()( + persist( + (set, get) => ({ + timers: {}, + + setTimer: (storeId: number, timeLeft: number) => { + set((state) => ({ + timers: { + ...state.timers, + [storeId]: { + timeLeft, + startTime: Date.now(), + isActive: true, + }, + }, + })); + }, + + updateTimer: (storeId: number) => { + set((state) => { + const timer = state.timers[storeId]; + if (!timer || !timer.isActive) return state; + + const elapsed = Math.floor((Date.now() - timer.startTime) / 1000); + const newTimeLeft = Math.max(0, timer.timeLeft - elapsed); + + return { + timers: { + ...state.timers, + [storeId]: { + ...timer, + timeLeft: newTimeLeft, + startTime: Date.now(), // 새로운 기준점 설정 + isActive: newTimeLeft > 0, + }, + }, + }; + }); + }, + + resetTimer: (storeId: number) => { + set((state) => ({ + timers: { + ...state.timers, + [storeId]: { + timeLeft: 600, // 10분으로 초기화 + startTime: Date.now(), + isActive: true, + }, + }, + })); + }, + + getTimeLeft: (storeId: number) => { + const timer = get().timers[storeId]; + if (!timer || !timer.isActive) return 0; + + const elapsed = Math.floor((Date.now() - timer.startTime) / 1000); + return Math.max(0, timer.timeLeft - elapsed); + }, + + isTimerActive: (storeId: number) => { + const timer = get().timers[storeId]; + return timer?.isActive && timer.timeLeft > 0; + }, + + clearAllTimers: () => { + set({ timers: {} }); + }, + }), + { + name: "timer-storage", + // 타이머는 새로고침 시에도 유지되지만, 너무 오래된 타이머는 정리 + partialize: (state) => ({ + timers: Object.fromEntries( + Object.entries(state.timers).filter(([_, timer]) => { + const elapsed = Math.floor((Date.now() - timer.startTime) / 1000); + return timer.timeLeft - elapsed > 0; + }) + ), + }), + } + ) +); diff --git a/apps/nowait-user/src/types/search.ts b/apps/nowait-user/src/types/search.ts index 54ef39d5..c4f045da 100644 --- a/apps/nowait-user/src/types/search.ts +++ b/apps/nowait-user/src/types/search.ts @@ -1,13 +1,18 @@ +export interface ProfileImage { + id: number; + storeId: number; + imageUrl: string; + imageType: string; +} + export interface SearchStore { storeId: number; - publicCode : string; + publicCode: string; departmentId: number; departmentName: string; name: string; - location: string; - description: string; - profileImage: string | null; - bannerImages: any[]; + openTime: string; + profileImage: ProfileImage | null; isActive: boolean; deleted: boolean; createdAt: string;