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
3 changes: 3 additions & 0 deletions apps/nowait-admin/src/assets/off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion apps/nowait-admin/src/components/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const AdminSidebar = () => {
<aside
className={`
h-screen flex flex-col justify-between bg-white px-4 py-6 fixed
${isCompact ? "w-[60px] items-center" : "w-[220px]"}
${isCompact ? "w-[60px] items-center" : "w-[210px]"}
`}
>
{/* 상단: 로고 & 메뉴 */}
Expand Down
43 changes: 43 additions & 0 deletions apps/nowait-admin/src/components/ConfirmRemoveModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";

interface ConfirmRemoveModalProps {
onCancel: () => void;
onConfirm: () => void;
}

const ConfirmRemoveModal = ({
onCancel,
onConfirm,
}: ConfirmRemoveModalProps) => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
<div
className="bg-white rounded-[20px] p-6 w-[305px] h-[170px]
md:w-[372px] text-center"
>
<h2 className="text-[17px] font-semibold text-black mb-2">
대기열에서 제거할까요?
</h2>
<p className="text-sm text-gray-500 mb-6">
삭제 후에는 대기 정보가 복구되지 않아요.
</p>
<div className="flex gap-2">
<button
onClick={onCancel}
className="flex-1 py-2 rounded-md border border-gray-300 text-gray-600 bg-white"
>
취소
</button>
<button
onClick={onConfirm}
className="flex-1 py-2 rounded-md bg-[#FF3B30] text-white font-medium"
>
삭제
</button>
</div>
</div>
</div>
);
};

export default ConfirmRemoveModal;
21 changes: 13 additions & 8 deletions apps/nowait-admin/src/layout/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,25 @@ const AdminLayout = () => {
const isCompact = width < 1024;
const isMobile = width <= 431;

// 사이드바 너비 계산
const getSidebarWidth = () => {
if (isMobile || width <= 768) return 0;
return isCompact ? 60 : 220;
return isCompact ? 60 : 210;
};

return (
<div className="flex [@media(max-width:431px)]:flex-col">
{width <= 431 ? <MobileMenuBar /> : <AdminSidebar />}
<div className="flex w-screen">
{isMobile ? <MobileMenuBar /> : <AdminSidebar />}
<main
className={`flex bg-[#F2F6F9] min-h-screen py-5 w-full ${
width <= 431 ? "px-5" : "px-7.5"
}`}
style={{ marginLeft: `${getSidebarWidth()}px` }}
className={`
flex bg-[#F2F6F9] min-h-screen py-5
${
isMobile || width <= 768
? "w-full px-5"
: isCompact
? "w-[calc(100vw-60px)] ml-[60px] px-7.5"
: "w-[calc(100vw-210px)] ml-[210px] px-7.5"
}
`}
>
<Outlet />
</main>
Expand Down
94 changes: 45 additions & 49 deletions apps/nowait-admin/src/pages/AdminHome/AdminHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import refreshIcon from "../../assets/refresh.svg";
import { WaitingCard } from "./components/WaitingCard";
import { useGetReservationList } from "../../hooks/useGetReservationList";
import on from "../../assets/on.svg";
import off from "../../assets/off.svg";
import onIcon from "../../assets/toggleOn.svg"; // 켜짐 상태 이미지
import offIcon from "../../assets/toggleOFF.svg";
import { useWindowWidth } from "../../hooks/useWindowWidth";
import { useUpdateReservationStatus } from "../../hooks/useUpdateReservationStatus";
import ConfirmRemoveModal from "../../components/ConfirmRemoveModal";
type WaitingStatus = "WAITING" | "CALLING" | "CONFIRMED" | "CANCELLED";

interface Reservation {
Expand All @@ -27,32 +29,42 @@ const AdminHome = () => {
const width = useWindowWidth();
const [noShowIds, setNoShowIds] = useState<number[]>([]);
const { mutate: updateStatus } = useUpdateReservationStatus();
const [showModal, setShowModal] = useState(false);

console.log(width);

const isCompact = width < 1024;

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 waitingCount = reservations.filter(
(res) => res.status === "WAITING"
).length;
//호출 중 카드 개수
const callingCount = reservations.filter(
(res) => res.status === "CALLING"
).length;
//완료 카드 개수
const confirmedCount = reservations.filter(
(res) => res.status === "CONFIRMED"
).length;
//최소된 카드 개수
const cancelledCount = reservations.filter(
(res) => res.status === "CANCELLED"
).length;

const tabLabels = [
{ label: "전체 보기" },
{ label: "대기 중", count: waitingCount },
{ label: "호출 중", count: callingCount },
{ label: "입장 완료", count: confirmedCount },
{ label: "대기 취소", count: cancelledCount },
];

const statusMap = {
WAITING: "대기 중",
CALLING: "호출 중",
Expand Down Expand Up @@ -130,24 +142,11 @@ const AdminHome = () => {
};

const handleNoShow = (id: number) => {
const target = reservations.find((res) => res.id === id);
if (!target || target.status === "CANCELLED") return;

updateStatus(
{ reservationId: id, status: "CANCELLED" },
{
onSuccess: () => {
setReservations((prev) =>
prev.map((res) =>
res.id === id ? { ...res, status: "CANCELLED" } : res
)
);
setNoShowIds((prev) => [...prev, id]);
},
}
);
setNoShowIds((prev) => {
if (!prev.includes(id)) return [...prev, id];
return prev;
});
};

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

Expand Down Expand Up @@ -178,18 +177,18 @@ const AdminHome = () => {

return (
<div
className={`w-full max-w-[804px] flex flex-col items-center mx-auto space-y-6 min-[375px]:px-[20px] lg:px-[30px]`}
className={`w-full md:w-[752px] max-w-[804px] flex flex-col items-center mx-auto space-y-6`}
>
<section
id="대기 현황"
className="flex w-full [@media(min-width:375px)_and_(max-width:431px)]:justify-center"
className="flex w-full [@media(min-width:375px)_and_(max-width:431px)]:justify-center m-0"
>
<div className="flex flex-col w-full">
<div className="flex justify-between mb-5">
<div className="flex justify-between mb-[40px]">
<div className="flex items-center">
<h1 className="title-20-bold">대기 현황</h1>&nbsp;
<h1 className="text-title-20-bold">대기 접수</h1>&nbsp;
<span>
<img src={on} />
<img src={isOn ? on : off} />
</span>
</div>
<button onClick={toggle}>
Expand All @@ -200,35 +199,22 @@ const AdminHome = () => {
/>
</button>
</div>
<div className="flex [@media(min-width:375px)_and_(max-width:431px)]:justify-between">
<CardBox
title="대기 팀 수"
count={waitingCount}
bottomLabel={`호출 중 ${callingCount}팀`}
/>
<CardBox
title="입장 완료"
count={confirmedCount}
bottomLabel={`대기 취소 ${cancelledCount}팀`}
/>
</div>
</div>
</section>

<section id="대기자 목록" className="flex flex-col w-full">
<h1 className="title-20-bold mb-5">대기자 목록</h1>
{/* <h1 className="title-20-bold mb-5">대기자 목록</h1> */}
<div className="flex justify-between items-center">
<div className="flex flex-wrap gap-2 overflow-x-auto scrollbar-hide [@media(max-width:431px)]:flex-nowrap">
{["전체 보기", "대기 중", "호출 중", "입장 완료", "대기 취소"].map(
(label) => (
<RoundTabButton
key={label}
label={label}
active={activeTab === label}
onClick={() => setActiveTab(label)}
/>
)
)}
{tabLabels.map(({ label, count }) => (
<RoundTabButton
key={label}
label={label}
active={activeTab === label}
onClick={() => setActiveTab(label)}
count={label === "전체 보기" ? undefined : count}
/>
))}
</div>
<div className="hover:rotate-90 transition-transform duration-500 cursor-pointer">
<img
Expand All @@ -239,7 +225,7 @@ const AdminHome = () => {
</div>
</section>

<div className="w-full grid grid-cols-1 gap-[10px] md:grid-cols-2 [@media(max-width:431px)]:place-items-center">
<div className="w-full grid grid-cols-1 gap-[10px] md:grid-cols-2 md:gap-[8px] [@media(max-width:431px)]:place-items-center">
{filteredReservations.map((res) => {
const requested = new Date(res.requestedAt);

Expand All @@ -264,11 +250,21 @@ const AdminHome = () => {
onCall={() => handleCall(res.id)}
onEnter={() => handleEnter(res.id)}
onClose={() => handleClose(res.id)}
onDelete={() => setShowModal(true)}
onNoShow={() => handleNoShow(res.id)}
/>
);
})}
</div>
{showModal && (
<ConfirmRemoveModal
onCancel={() => setShowModal(false)}
onConfirm={() => {
// handleDelete(); // 삭제 처리 로직
setShowModal(false);
}}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ interface RoundTabButtonProps {
label: string;
active?: boolean;
onClick?: () => void;
count?: number;
}

const RoundTabButton: React.FC<RoundTabButtonProps> = ({
label,
active = false,
onClick,
count,
}) => {
return (
<button
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"
active ? "text-white bg-navy-80" : "bg-white text-navy-30"
)}
>
{label}
{count !== undefined && count > 0 && <span>&nbsp;{count}</span>}
</button>
);
};
Expand Down
46 changes: 26 additions & 20 deletions apps/nowait-admin/src/pages/AdminHome/components/WaitingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface WaitingCardProps {
onEnter: () => void;
onClose: () => void;
onNoShow: () => void;
onDelete: () => void;
}
const truncateName = (name: string, maxLength: number = 3) => {
return name.length > maxLength ? name.slice(0, maxLength) + "..." : name;
Expand All @@ -38,14 +39,15 @@ export function WaitingCard({
onEnter,
onClose,
onNoShow,
onDelete,
}: WaitingCardProps) {
const [elapsed, setElapsed] = useState("10:00");

useEffect(() => {
if (status === "CALLING") {
const timer = setTimeout(() => {
onNoShow();
}, 10 * 1 * 1000);
}, 10 * 1 * 1000); // 테스트용 10초

return () => clearTimeout(timer);
}
Expand Down Expand Up @@ -80,7 +82,7 @@ export function WaitingCard({
<div className="flex items-center gap-2 text-13-medium text-black-50">
<span>{time}</span>
<span>· {waitMinutes}분 대기 중</span>
{<CloseButton onClick={onClose} />}
{<CloseButton onClick={onDelete} />}
</div>
</div>

Expand Down Expand Up @@ -128,30 +130,34 @@ export function WaitingCard({
</>
)}

{status === "CALLING" && (
<>
<div className="w-[60%] bg-black-15 text-black-60 py-2 rounded-[8px] flex justify-center items-center gap-1">
⏱ {elapsed}
</div>
<button
onClick={onEnter}
className="w-[35%] bg-[#E8F3FF] text-[#2C7CF6] py-2 rounded-[8px] flex justify-center items-center gap-1"
>
입장
</button>
</>
)}
{status === "CANCELLED" &&
{status === "CALLING" &&
(isNoShow === true ? (
<div className="w-full bg-black-5 text-black-40 rounded-[8px] flex justify-center items-center py-2">
<div
className="w-full bg-black-5 text-black-40 rounded-[8px] flex justify-center items-center py-2"
onClick={onClose}
>
미입장
</div>
) : (
<div className="w-full bg-black-5 text-black-40 rounded-[8px] flex justify-center items-center py-2">
취소된 입장
</div>
<>
<div className="w-[60%] bg-black-15 text-black-60 py-2 rounded-[8px] flex justify-center items-center gap-1">
⏱ {elapsed}
</div>
<button
onClick={onEnter}
className="w-[35%] bg-[#E8F3FF] text-[#2C7CF6] py-2 rounded-[8px] flex justify-center items-center gap-1"
>
입장
</button>
</>
))}

{status === "CANCELLED" && (
<div className="w-full bg-black-5 text-black-40 rounded-[8px] flex justify-center items-center py-2">
취소된 입장
</div>
)}

{status === "CONFIRMED" && (
<div className="w-full bg-black-5 text-black-40 rounded-[8px] flex justify-center items-center py-2">
완료된 입장
Expand Down