Skip to content

Commit 4ced190

Browse files
authored
Merge branch 'develop' into feature/user/order-style
2 parents 425d751 + 7a3b5ff commit 4ced190

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1058
-305
lines changed
Lines changed: 5 additions & 0 deletions
Loading
1.2 KB
Loading

apps/nowait-admin/src/components/AdminSidebar.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { NavLink } from "react-router-dom";
2+
import { NavLink, useNavigate } from "react-router-dom";
33
import { Clock, Layers, BarChart2 } from "lucide-react";
44
import { useWindowWidth } from "../hooks/useWindowWidth";
55
import nwIcon from "../assets/nwLogo.svg";
@@ -8,6 +8,7 @@ import profile from "../assets/profile.png";
88

99
const AdminSidebar = () => {
1010
const width = useWindowWidth();
11+
const navigate = useNavigate();
1112

1213
// 375px 이하에서는 사이드바 완전히 숨김
1314
if (width <= 375) return null;
@@ -27,12 +28,15 @@ const AdminSidebar = () => {
2728
{/* 로고 */}
2829
<div className="mb-8">
2930
{isCompact ? (
30-
<div className="flex justify-center">
31+
<div
32+
className="flex justify-center"
33+
onClick={() => navigate("/admin")}
34+
>
3135
<img src={nwIcon} />
3236
</div>
3337
) : (
3438
<div className="flex">
35-
<img src={nwTextIcon} />
39+
<img src={nwTextIcon} onClick={() => navigate("/admin")} />
3640
</div>
3741
)}
3842
</div>

apps/nowait-admin/src/components/MobileMenuBar.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import nwIcon from "../assets/nwLogo.svg";
22
import nwTextIcon from "../assets/nw_text_logo.svg";
3+
import menuIcon from "../assets/Menu.svg";
34

45
const MobileMenuBar = () => {
56
return (
@@ -15,10 +16,8 @@ const MobileMenuBar = () => {
1516
</div>
1617

1718
{/* 우측: 햄버거 메뉴 */}
18-
<button className="w-[24px] h-[24px] flex flex-col justify-between items-center cursor-pointer">
19-
<span className="block w-full h-[2px] bg-black-90"></span>
20-
<span className="block w-full h-[2px] bg-black-90"></span>
21-
<span className="block w-full h-[2px] bg-black-90"></span>
19+
<button className="cursor-pointer">
20+
<img src={menuIcon} />
2221
</button>
2322
</div>
2423
);

apps/nowait-admin/src/components/closeButton.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ interface CloseButtonProps {
66
const CloseButton = ({ onClick }: CloseButtonProps) => {
77
return (
88
<button onClick={onClick}>
9-
<img src={closeIcon} width="11px" height="11px" />
9+
<img
10+
src={closeIcon}
11+
width="11px"
12+
height="11px"
13+
alt="닫기 아이콘"
14+
className="hover:brightness-[110%] cursor-pointer"
15+
/>
1016
</button>
1117
);
1218
};

apps/nowait-admin/src/global.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ body,
5757
width: var(--sidebar-width, 280px);
5858
height: var(--admin-sidebar-height, calc(100vh - 4rem));
5959
}
60+
61+
.scrollbar-hide::-webkit-scrollbar {
62+
display: none;
63+
}
64+
.scrollbar-hide {
65+
-ms-overflow-style: none; /* IE and Edge */
66+
scrollbar-width: none; /* Firefox */
67+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { useMutation } from "@tanstack/react-query";
2+
import UserApi from "../utils/UserApi";
3+
export const useUpdateReservationStatus = () => {
4+
const token = localStorage.getItem("adminToken");
5+
6+
return useMutation({
7+
mutationFn: async ({
8+
reservationId,
9+
status,
10+
}: {
11+
reservationId: number;
12+
status: "WAITING" | "CALLING" | "CONFIRMED" | "CANCELLED" | "NO_SHOW";
13+
}) => {
14+
const res = await UserApi.patch(
15+
`/reservations/admin/updates/${reservationId}`,
16+
{ status },
17+
{
18+
headers: {
19+
Authorization: `Bearer ${token}`,
20+
},
21+
}
22+
);
23+
return res.data;
24+
},
25+
});
26+
};

apps/nowait-admin/src/layout/AdminLayout.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@ const AdminLayout = () => {
88

99
const isCompact = width < 1024;
1010
const isMobile = width <= 431;
11+
12+
// 사이드바 너비 계산
13+
const getSidebarWidth = () => {
14+
if (isMobile || width <= 768) return 0;
15+
return isCompact ? 60 : 220;
16+
};
17+
1118
return (
1219
<div className="flex [@media(max-width:431px)]:flex-col">
1320
{width <= 431 ? <MobileMenuBar /> : <AdminSidebar />}
14-
<main className="flex bg-[#F2F6F9] min-h-screen py-5 md:px-7.5 w-full">
21+
<main
22+
className={`flex bg-[#F2F6F9] min-h-screen py-5 w-full ${
23+
width <= 431 ? "px-5" : "px-7.5"
24+
}`}
25+
style={{ marginLeft: `${getSidebarWidth()}px` }}
26+
>
1527
<Outlet />
1628
</main>
1729
</div>

apps/nowait-admin/src/pages/AdminHome/AdminHome.tsx

Lines changed: 146 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMemo, useState } from "react";
1+
import { useEffect, useMemo, useState } from "react";
22
import CardBox from "./components/CardBox";
33
import RoundTabButton from "./components/RoundTabButton";
44
import refreshIcon from "../../assets/refresh.svg";
@@ -8,25 +8,29 @@ import on from "../../assets/on.svg";
88
import onIcon from "../../assets/toggleOn.svg"; // 켜짐 상태 이미지
99
import offIcon from "../../assets/toggleOFF.svg";
1010
import { useWindowWidth } from "../../hooks/useWindowWidth";
11-
type WaitingStatus = "대기 중" | "호출 중" | "입장 완료" | "대기 취소";
11+
import { useUpdateReservationStatus } from "../../hooks/useUpdateReservationStatus";
12+
type WaitingStatus =
13+
| "WAITING"
14+
| "CALLING"
15+
| "CONFIRMED"
16+
| "CANCELLED"
17+
| "NO_SHOW";
1218

1319
interface Reservation {
1420
id: number;
15-
number: number;
1621
time: string;
22+
requestedAt: string;
1723
waitMinutes: number;
1824
peopleCount: number;
1925
name: string;
2026
phone: string;
2127
status: WaitingStatus;
28+
calledAt?: string;
2229
}
2330

2431
const AdminHome = () => {
25-
const handleCall = () => alert("🔔 고객 호출");
26-
const handleEnter = () => alert("🏢 고객 입장 처리");
27-
const handleClose = () => alert("❌ 카드 닫기");
28-
2932
const width = useWindowWidth();
33+
const { mutate: updateStatus } = useUpdateReservationStatus();
3034

3135
console.log(width);
3236

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

3945
const toggle = () => setIsOn((prev) => !prev);
4046
const statusMap = {
@@ -43,28 +49,119 @@ const AdminHome = () => {
4349
CONFIRMED: "입장 완료",
4450
CANCELLED: "대기 취소",
4551
};
46-
const { data, isLoading, isError } = useGetReservationList(storeId);
47-
console.log(data, "ReservationList");
48-
49-
// 전체 목록 예약순 예약 번호 부여
50-
const numberedReservations = useMemo(() => {
51-
if (!data) return [];
52-
return data.reservationList.map((res, idx) => ({
53-
...res,
54-
number: idx + 1,
55-
}));
56-
}, [data]);
5752

5853
const filteredReservations = useMemo(() => {
59-
if (activeTab === "전체 보기") return numberedReservations;
54+
if (activeTab === "전체 보기") return reservations;
6055

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

6560
if (!targetStatus) return [];
66-
return numberedReservations.filter((res) => res.status === targetStatus);
67-
}, [numberedReservations, activeTab]);
61+
62+
return reservations.filter((res) => res.status === targetStatus);
63+
}, [reservations, activeTab]);
64+
65+
// 호출 버튼 클릭 이벤트
66+
const handleCall = (id: number) => {
67+
// 상태 변화 api 호출 --> 성공시 --> reservation status 변경(호출 시간 calledAt추가해야 됨)
68+
updateStatus(
69+
{ reservationId: id, status: "CALLING" },
70+
{
71+
onSuccess: () => {
72+
setReservations((prev) =>
73+
prev.map((res) =>
74+
res.id === id
75+
? {
76+
...res,
77+
status: "CALLING",
78+
calledAt: new Date().toISOString(),
79+
}
80+
: res
81+
)
82+
);
83+
},
84+
onError: () => {
85+
alert("호출 상태 변경 실패");
86+
},
87+
}
88+
);
89+
};
90+
91+
const handleEnter = (id: number) => {
92+
updateStatus(
93+
{ reservationId: id, status: "CONFIRMED" },
94+
{
95+
onSuccess: () => {
96+
setReservations((prev) =>
97+
prev.map((res) =>
98+
res.id === id ? { ...res, status: "CONFIRMED" } : res
99+
)
100+
);
101+
},
102+
}
103+
);
104+
};
105+
106+
const handleClose = (id: number) => {
107+
updateStatus(
108+
{ reservationId: id, status: "CANCELLED" },
109+
{
110+
onSuccess: () => {
111+
setReservations((prev) =>
112+
prev.map((res) =>
113+
res.id === id ? { ...res, status: "CANCELLED" } : res
114+
)
115+
);
116+
},
117+
}
118+
);
119+
};
120+
121+
const handleNoShow = (id: number) => {
122+
const target = reservations.find((res) => res.id === id);
123+
if (target?.status === "NO_SHOW") return;
124+
updateStatus(
125+
{ reservationId: id, status: "NO_SHOW" },
126+
{
127+
onSuccess: () => {
128+
setReservations((prev) =>
129+
prev.map((res) =>
130+
res.id === id ? { ...res, status: "NO_SHOW" } : res
131+
)
132+
);
133+
},
134+
}
135+
);
136+
};
137+
138+
useEffect(() => {
139+
if (!data?.reservationList) return;
140+
141+
const now = Date.now();
142+
143+
setReservations(
144+
data.reservationList.map((res, idx) => {
145+
const requested = new Date(res.requestedAt);
146+
return {
147+
id: res.id,
148+
requestedAt: res.requestedAt, //서버 데이터 문자열 그대로 사용("2025-06-24T12:33:26")
149+
time: requested.toLocaleTimeString("ko-KR", {
150+
hour: "2-digit",
151+
minute: "2-digit",
152+
hour12: true,
153+
}),
154+
waitMinutes: Math.floor((now - requested.getTime()) / 60000),
155+
peopleCount: res.partySize,
156+
name: res.userName,
157+
phone: "010-****-****",
158+
status: res.status,
159+
calledAt:
160+
res.status === "CALLING" ? requested.toISOString() : undefined,
161+
};
162+
})
163+
);
164+
}, [data]);
68165

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

124221
<div className="w-full grid grid-cols-1 gap-[10px] md:grid-cols-2 [@media(max-width:431px)]:place-items-center">
125-
{filteredReservations.map((res) => (
126-
<WaitingCard
127-
key={res.id}
128-
number={res.number}
129-
time={new Date(res.requestedAt).toLocaleTimeString("ko-KR", {
130-
hour: "2-digit",
131-
minute: "2-digit",
132-
})}
133-
waitMinutes={3}
134-
peopleCount={res.partySize}
135-
name={res.userName}
136-
phone="010-****-****"
137-
onCall={handleCall}
138-
onEnter={handleEnter}
139-
onClose={handleClose}
140-
/>
141-
))}
222+
{filteredReservations.map((res) => {
223+
const requested = new Date(res.requestedAt);
224+
225+
return (
226+
<WaitingCard
227+
key={res.id}
228+
number={res.id}
229+
time={requested.toLocaleTimeString("ko-KR", {
230+
hour: "2-digit",
231+
minute: "2-digit",
232+
hour12: true,
233+
})}
234+
waitMinutes={Math.floor(
235+
(Date.now() - requested.getTime()) / 60000
236+
)}
237+
peopleCount={res.peopleCount}
238+
name={res.name}
239+
phone="010-****-****"
240+
status={res.status}
241+
calledAt={res.calledAt}
242+
onCall={() => handleCall(res.id)}
243+
onEnter={() => handleEnter(res.id)}
244+
onClose={() => handleClose(res.id)}
245+
onNoShow={() => handleNoShow(res.id)}
246+
/>
247+
);
248+
})}
142249
</div>
143250
</div>
144251
);

apps/nowait-admin/src/pages/AdminHome/components/RoundTabButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const RoundTabButton: React.FC<RoundTabButtonProps> = ({
1818
onClick={onClick}
1919
className={clsx(
2020
"px-4 h-[33px] rounded-full text-14-medium font-semibold transition cursor-pointer",
21+
"whitespace-nowrap",
2122
active ? "text-white bg-navy-80" : "bg-white text-black"
2223
)}
2324
>

0 commit comments

Comments
 (0)