Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 71 additions & 62 deletions apps/nowait-admin/src/pages/AdminHome/AdminHome.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import RoundTabButton from "./components/RoundTabButton";
import refreshIcon from "../../assets/refresh.svg";
import { WaitingCard } from "./components/WaitingCard";
Expand Down Expand Up @@ -104,74 +104,83 @@ const AdminHome = () => {
}, [reservations, activeTab]);

// 호출 버튼 클릭 이벤트
const handleCall = (id: number, reservationNumber: number) => {
updateStatus(
{ storeId, reservationNumber, status: "CALLING" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id
? (() => {
// 입장/취소 기록은 흐름상 초기화하는게 보통 자연스러움
return {
...res,
status: "CALLING",
calledAt: new Date().toISOString(),
confirmedAt: undefined,
cancelledAt: undefined,
};
})()
: res
)
);
},
}
);
};
const handleCall = useCallback(
(id: number, reservationNumber: number) => {
updateStatus(
{ storeId, reservationNumber, status: "CALLING" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id
? (() => {
// 입장/취소 기록은 흐름상 초기화하는게 보통 자연스러움
return {
...res,
status: "CALLING",
calledAt: new Date().toISOString(),
confirmedAt: undefined,
cancelledAt: undefined,
};
})()
: res
)
);
},
}
);
},
[storeId, updateStatus]
);

const handleEnter = (id: number, reservationNumber: number) => {
const now = new Date().toISOString();
updateStatus(
{ storeId, reservationNumber, status: "CONFIRMED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) => {
if (res.id !== id) return res;
return { ...res, status: "CONFIRMED", confirmedAt: now };
})
);
},
}
);
};
const handleEnter = useCallback(
(id: number, reservationNumber: number) => {
const now = new Date().toISOString();
updateStatus(
{ storeId, reservationNumber, status: "CONFIRMED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) => {
if (res.id !== id) return res;
return { ...res, status: "CONFIRMED", confirmedAt: now };
})
);
},
}
);
},
[storeId, updateStatus]
);

const handleClose = (id: number, reservationNumber: number) => {
const now = new Date().toISOString();
updateStatus(
{ storeId, reservationNumber, status: "CANCELLED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) => {
if (res.id !== id) return res;
return { ...res, status: "CANCELLED", cancelledAt: now };
})
);
},
}
);
};
const handleClose = useCallback(
(id: number, reservationNumber: number) => {
const now = new Date().toISOString();
updateStatus(
{ storeId, reservationNumber, status: "CANCELLED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) => {
if (res.id !== id) return res;
return { ...res, status: "CANCELLED", cancelledAt: now };
})
);
},
}
);
},
[storeId, updateStatus]
);

const handleNoShow = (id: number) => {
const handleNoShow = useCallback((id: number) => {
setNoShowIds((prev) => {
if (!prev.includes(id)) return [...prev, id];
return prev;
});
};
}, []);

const handleRefresh = async () => {
const handleRefresh = useCallback(async () => {
if (isRefreshing) return; // 중복 클릭 방지
setIsRefreshing(true);
try {
Expand All @@ -180,7 +189,7 @@ const AdminHome = () => {
// 살짝 딜레이를 주면 회전이 끊기지 않고 보여짐 (선택)
setTimeout(() => setIsRefreshing(false), 300);
}
};
}, [isRefreshing, refetchWaiting, refetchCompleted]);

useEffect(() => {
if (!Array.isArray(waitingList) || !Array.isArray(completedList)) return;
Expand Down
23 changes: 20 additions & 3 deletions apps/nowait-admin/src/pages/AdminHome/components/WaitingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import CloseButton from "../../../components/closeButton";
import callIcon from "../../../assets/Call.svg";
import openDoorIcon from "../../../assets/door_open.svg";
import alarmIcon from "../../../assets/alarm.svg";
import { useEffect, useState } from "react";
import { memo, useEffect, useState } from "react";

const totalDurationSec = 10; // 10초, 10분은 600

Expand Down Expand Up @@ -37,7 +37,24 @@ const diffMinutes = (start?: string, end?: string) => {
);
};

export function WaitingCard({
const areEqual = (prev: WaitingCardProps, next: WaitingCardProps) => {
return (
prev.number === next.number &&
prev.time === next.time &&
prev.waitMinutes === next.waitMinutes &&
prev.peopleCount === next.peopleCount &&
prev.name === next.name &&
prev.phoneNumber === next.phoneNumber &&
prev.status === next.status &&
prev.requestedAt === next.requestedAt &&
prev.calledAt === next.calledAt &&
prev.confirmedAt === next.confirmedAt &&
prev.cancelledAt === next.cancelledAt &&
prev.isNoShow === next.isNoShow
);
};

export const WaitingCard = memo(function WaitingCard({
number,
time,
waitMinutes,
Expand Down Expand Up @@ -222,4 +239,4 @@ export function WaitingCard({
</div>
</div>
);
}
}, areEqual);
23 changes: 23 additions & 0 deletions apps/nowait-user/src/hooks/order/useStoreMenus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery } from "@tanstack/react-query";
import { getStoreMenus } from "../../api/menu";

export const storeMenusQuery = (storeId?: string) => ({
queryKey: ["storeMenus", storeId],
queryFn: () => getStoreMenus(storeId!),
});

export const useStoreMenus = (storeId?: string) => {
return useQuery({
...storeMenusQuery(storeId!),
enabled: !!storeId,
select: (data) => data?.response,
});
};

export const useStoreMenuList = (storeId?: string) => {
return useQuery({
...storeMenusQuery(storeId!),
enabled: !!storeId,
select: (data) => data?.response?.menuReadDto,
});
};
100 changes: 45 additions & 55 deletions apps/nowait-user/src/pages/order/addMenu/AddMenuPage.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,77 @@
import { useState } from "react";
import QuantitySelector from "../../../components/common/QuantitySelector";
import { useState, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import PageFooterButton from "../../../components/order/PageFooterButton";
import FullPageLoader from "../../../components/FullPageLoader";
import { Button } from "@repo/ui";
import type { CartType } from "../../../types/order/cart";
import { useCartStore } from "../../../stores/cartStore";
import NumberFlow from "@number-flow/react";
import defaultMenuImageLg from "../../../assets/default-image-lg.png";
import { useQuery } from "@tanstack/react-query";
import { getStoreMenu } from "../../../api/menu";
import FullPageLoader from "../../../components/FullPageLoader";
import { useCartStore } from "../../../stores/cartStore";
import type { CartType } from "../../../types/order/cart";
import MenuInfoSection from "./components/MenuInfoSection";
import MenuOrderSection from "./components/MenuOrderSection";


const AddMenuPage = () => {
const navigate = useNavigate();
const { storeId, menuId } = useParams();
const { storeId, menuId } = useParams<{ storeId: string; menuId: string }>();

const [quantity, setQuantity] = useState(1);
const { addToCart } = useCartStore();

const { data: menu, isLoading } = useQuery({
queryKey: ["menu", menuId],
queryFn: () => getStoreMenu(storeId!, Number(menuId!)),
select: (data) => data?.response,
queryKey: ["menu", storeId, menuId],
queryFn: () => getStoreMenu(storeId!, Number(menuId)),
select: (data) => data.response,
enabled: !!storeId && !!menuId,
});

const [quantity, setQuantity] = useState(1);
const { addToCart } = useCartStore();
const totalPrice = useMemo(
() => (menu ? menu.price * quantity : 0),
[menu, quantity]
);

const handleAddToCart = () => {
if (!menu) return;

const addToCartButton = () => {
const item: CartType = {
menuId: menu!.menuId,
image: menu!.images[0]?.imageUrl,
name: menu!.name,
menuId: menu.menuId,
image: menu.images?.[0]?.imageUrl,
name: menu.name,
quantity,
originPrice: menu!.price,
price: menu!.price * quantity,
originPrice: menu.price,
price: totalPrice,
};

addToCart(item);

navigate(`/${storeId}`, {
state: { added: true, addedPrice: menu!.price * quantity, isBack: true },
replace: false,
state: {
added: true,
addedPrice: totalPrice,
isBack: true,
},
});
};

if (isLoading) return <FullPageLoader />;
if (isLoading || !menu) return <FullPageLoader />;

return (
<div className="flex flex-col min-h-dvh bg-white">
<div className="flex flex-col grow px-5">
<h1 className="-mx-5 h-[246px] object-cover">
<img
className="w-full h-full object-cover"
src={menu!.images[0]?.imageUrl || defaultMenuImageLg}
alt="음식 메뉴 이미지"
/>
</h1>
<div className="py-8">
<h1 className="text-headline-22-bold mb-2 break-keep">
{menu!.name}
</h1>
<h2 className="text-16-regular text-black-70 break-keep">
{menu!.description}
</h2>
</div>
</div>
{/* 메뉴 가격 및 수량 컨트롤 */}
<div className="fixed bottom-28 w-full max-w-[430px] min-w-[320px] bg-white">
<div className="w-full flex justify-between items-center px-5">
<h1 className="text-[24px] font-semibold">
<NumberFlow value={menu!.price * quantity} suffix="원" />
</h1>
<QuantitySelector
mode="state"
quantity={quantity}
setQuantity={setQuantity}
/>
</div>
</div>
<MenuInfoSection menu={menu} />

<MenuOrderSection
price={menu.price}
quantity={quantity}
onChangeQuantity={setQuantity}
/>

<PageFooterButton>
<Button textColor="white" onClick={addToCartButton}>
<Button textColor="white" onClick={handleAddToCart}>
추가하기
</Button>
</PageFooterButton>
</div>
);
};

export default AddMenuPage;
export default AddMenuPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import defaultMenuImageLg from "../../../../assets/default-image-lg.png";

interface Props {
menu: {
name: string;
description?: string;
images?: { imageUrl: string }[];
};
}

const MenuInfoSection = ({ menu }: Props) => {
const image = menu.images?.[0]?.imageUrl ?? defaultMenuImageLg;

return (
<section className="flex flex-col grow px-5">
<div className="-mx-5 h-[246px]">
<img
className="w-full h-full object-cover"
src={image}
alt="음식 메뉴 이미지"
/>
</div>

<div className="py-8">
<h1 className="text-headline-22-bold mb-2 break-keep">{menu.name}</h1>
{menu.description && (
<p className="text-16-regular text-black-70 break-keep">
{menu.description}
</p>
)}
</div>
</section>
);
};

export default MenuInfoSection;
Loading