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
5 changes: 5 additions & 0 deletions apps/nowait-admin/src/assets/Menu.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/nowait-admin/src/assets/favicon_admin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 7 additions & 3 deletions apps/nowait-admin/src/components/AdminSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { NavLink } from "react-router-dom";
import { NavLink, useNavigate } from "react-router-dom";
import { Clock, Layers, BarChart2 } from "lucide-react";
import { useWindowWidth } from "../hooks/useWindowWidth";
import nwIcon from "../assets/nwLogo.svg";
Expand All @@ -8,6 +8,7 @@ import profile from "../assets/profile.png";

const AdminSidebar = () => {
const width = useWindowWidth();
const navigate = useNavigate();

// 375px 이하에서는 사이드바 완전히 숨김
if (width <= 375) return null;
Expand All @@ -27,12 +28,15 @@ const AdminSidebar = () => {
{/* 로고 */}
<div className="mb-8">
{isCompact ? (
<div className="flex justify-center">
<div
className="flex justify-center"
onClick={() => navigate("/admin")}
>
<img src={nwIcon} />
</div>
) : (
<div className="flex">
<img src={nwTextIcon} />
<img src={nwTextIcon} onClick={() => navigate("/admin")} />
</div>
)}
</div>
Expand Down
7 changes: 3 additions & 4 deletions apps/nowait-admin/src/components/MobileMenuBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import nwIcon from "../assets/nwLogo.svg";
import nwTextIcon from "../assets/nw_text_logo.svg";
import menuIcon from "../assets/Menu.svg";

const MobileMenuBar = () => {
return (
Expand All @@ -15,10 +16,8 @@ const MobileMenuBar = () => {
</div>

{/* 우측: 햄버거 메뉴 */}
<button className="w-[24px] h-[24px] flex flex-col justify-between items-center cursor-pointer">
<span className="block w-full h-[2px] bg-black-90"></span>
<span className="block w-full h-[2px] bg-black-90"></span>
<span className="block w-full h-[2px] bg-black-90"></span>
<button className="cursor-pointer">
<img src={menuIcon} />
</button>
</div>
);
Expand Down
8 changes: 7 additions & 1 deletion apps/nowait-admin/src/components/closeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ interface CloseButtonProps {
const CloseButton = ({ onClick }: CloseButtonProps) => {
return (
<button onClick={onClick}>
<img src={closeIcon} width="11px" height="11px" />
<img
src={closeIcon}
width="11px"
height="11px"
alt="닫기 아이콘"
className="hover:brightness-[110%] cursor-pointer"
/>
</button>
);
};
Expand Down
8 changes: 8 additions & 0 deletions apps/nowait-admin/src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ body,
width: var(--sidebar-width, 280px);
height: var(--admin-sidebar-height, calc(100vh - 4rem));
}

.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
26 changes: 26 additions & 0 deletions apps/nowait-admin/src/hooks/useUpdateReservationStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMutation } from "@tanstack/react-query";
import UserApi from "../utils/UserApi";
export const useUpdateReservationStatus = () => {
const token = localStorage.getItem("adminToken");

return useMutation({
mutationFn: async ({
reservationId,
status,
}: {
reservationId: number;
status: "WAITING" | "CALLING" | "CONFIRMED" | "CANCELLED" | "NO_SHOW";
}) => {
const res = await UserApi.patch(
`/reservations/admin/updates/${reservationId}`,
{ status },
{
headers: {
Authorization: `Bearer ${token}`,
},
}
);
return res.data;
},
});
};
185 changes: 146 additions & 39 deletions apps/nowait-admin/src/pages/AdminHome/AdminHome.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import CardBox from "./components/CardBox";
import RoundTabButton from "./components/RoundTabButton";
import refreshIcon from "../../assets/refresh.svg";
Expand All @@ -8,25 +8,29 @@ import on from "../../assets/on.svg";
import onIcon from "../../assets/toggleOn.svg"; // 켜짐 상태 이미지
import offIcon from "../../assets/toggleOFF.svg";
import { useWindowWidth } from "../../hooks/useWindowWidth";
type WaitingStatus = "대기 중" | "호출 중" | "입장 완료" | "대기 취소";
import { useUpdateReservationStatus } from "../../hooks/useUpdateReservationStatus";
type WaitingStatus =
| "WAITING"
| "CALLING"
| "CONFIRMED"
| "CANCELLED"
| "NO_SHOW";

interface Reservation {
id: number;
number: number;
time: string;
requestedAt: string;
waitMinutes: number;
peopleCount: number;
name: string;
phone: string;
status: WaitingStatus;
calledAt?: string;
}

const AdminHome = () => {
const handleCall = () => alert("🔔 고객 호출");
const handleEnter = () => alert("🏢 고객 입장 처리");
const handleClose = () => alert("❌ 카드 닫기");

const width = useWindowWidth();
const { mutate: updateStatus } = useUpdateReservationStatus();

console.log(width);

Expand All @@ -35,6 +39,8 @@ const AdminHome = () => {
const [activeTab, setActiveTab] = useState("전체 보기");
const storeId = 1; //현재는 임시로 mockdata씀
const [isOn, setIsOn] = useState(false);
const [reservations, setReservations] = useState<Reservation[]>([]);
const { data, isLoading, isError } = useGetReservationList(storeId);

const toggle = () => setIsOn((prev) => !prev);
const statusMap = {
Expand All @@ -43,28 +49,119 @@ const AdminHome = () => {
CONFIRMED: "입장 완료",
CANCELLED: "대기 취소",
};
const { data, isLoading, isError } = useGetReservationList(storeId);
console.log(data, "ReservationList");

// 전체 목록 예약순 예약 번호 부여
const numberedReservations = useMemo(() => {
if (!data) return [];
return data.reservationList.map((res, idx) => ({
...res,
number: idx + 1,
}));
}, [data]);

const filteredReservations = useMemo(() => {
if (activeTab === "전체 보기") return numberedReservations;
if (activeTab === "전체 보기") return reservations;

const targetStatus = Object.entries(statusMap).find(
([, label]) => label === activeTab
)?.[0];

if (!targetStatus) return [];
return numberedReservations.filter((res) => res.status === targetStatus);
}, [numberedReservations, activeTab]);

return reservations.filter((res) => res.status === targetStatus);
}, [reservations, activeTab]);

// 호출 버튼 클릭 이벤트
const handleCall = (id: number) => {
// 상태 변화 api 호출 --> 성공시 --> reservation status 변경(호출 시간 calledAt추가해야 됨)
updateStatus(
{ reservationId: id, status: "CALLING" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id
? {
...res,
status: "CALLING",
calledAt: new Date().toISOString(),
}
: res
)
);
},
onError: () => {
alert("호출 상태 변경 실패");
},
}
);
};

const handleEnter = (id: number) => {
updateStatus(
{ reservationId: id, status: "CONFIRMED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id ? { ...res, status: "CONFIRMED" } : res
)
);
},
}
);
};

const handleClose = (id: number) => {
updateStatus(
{ reservationId: id, status: "CANCELLED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id ? { ...res, status: "CANCELLED" } : res
)
);
},
}
);
};

const handleNoShow = (id: number) => {
const target = reservations.find((res) => res.id === id);
if (target?.status === "NO_SHOW") return;
updateStatus(
{ reservationId: id, status: "NO_SHOW" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id ? { ...res, status: "NO_SHOW" } : res
)
);
},
}
);
};

useEffect(() => {
if (!data?.reservationList) return;

const now = Date.now();

setReservations(
data.reservationList.map((res, idx) => {
const requested = new Date(res.requestedAt);
return {
id: res.id,
requestedAt: res.requestedAt, //서버 데이터 문자열 그대로 사용("2025-06-24T12:33:26")
time: requested.toLocaleTimeString("ko-KR", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
}),
waitMinutes: Math.floor((now - requested.getTime()) / 60000),
peopleCount: res.partySize,
name: res.userName,
phone: "010-****-****",
status: res.status,
calledAt:
res.status === "CALLING" ? requested.toISOString() : undefined,
};
})
);
}, [data]);

return (
<div
Expand Down Expand Up @@ -100,7 +197,7 @@ const AdminHome = () => {
<section id="대기자 목록" className="flex flex-col w-full">
<h1 className="title-20-bold mb-5">대기자 목록</h1>
<div className="flex justify-between items-center">
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 overflow-x-auto scrollbar-hide [@media(max-width:431px)]:flex-nowrap">
{["전체 보기", "대기 중", "호출 중", "입장 완료", "대기 취소"].map(
(label) => (
<RoundTabButton
Expand All @@ -122,23 +219,33 @@ const AdminHome = () => {
</section>

<div className="w-full grid grid-cols-1 gap-[10px] md:grid-cols-2 [@media(max-width:431px)]:place-items-center">
{filteredReservations.map((res) => (
<WaitingCard
key={res.id}
number={res.number}
time={new Date(res.requestedAt).toLocaleTimeString("ko-KR", {
hour: "2-digit",
minute: "2-digit",
})}
waitMinutes={3}
peopleCount={res.partySize}
name={res.userName}
phone="010-****-****"
onCall={handleCall}
onEnter={handleEnter}
onClose={handleClose}
/>
))}
{filteredReservations.map((res) => {
const requested = new Date(res.requestedAt);

return (
<WaitingCard
key={res.id}
number={res.id}
time={requested.toLocaleTimeString("ko-KR", {
hour: "2-digit",
minute: "2-digit",
hour12: true,
})}
waitMinutes={Math.floor(
(Date.now() - requested.getTime()) / 60000
)}
peopleCount={res.peopleCount}
name={res.name}
phone="010-****-****"
status={res.status}
calledAt={res.calledAt}
onCall={() => handleCall(res.id)}
onEnter={() => handleEnter(res.id)}
onClose={() => handleClose(res.id)}
onNoShow={() => handleNoShow(res.id)}
/>
);
})}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const RoundTabButton: React.FC<RoundTabButtonProps> = ({
onClick={onClick}
className={clsx(
"px-4 h-[33px] rounded-full text-14-medium font-semibold transition cursor-pointer",
"whitespace-nowrap",
active ? "text-white bg-navy-80" : "bg-white text-black"
)}
>
Expand Down
Loading