From de8c55f1cd6b95b23b4c45c2787eb6fd4e3848cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:56:13 +0900 Subject: [PATCH 1/3] feat/owner-orderList/modal --- .../_components/modal/AcceptOrderModal.tsx | 49 +++++++++++++ .../_components/modal/RejectOrderModal.tsx | 73 +++++++++++++++++++ .../app/(tabs)/order/_constants/mockOrders.ts | 36 +++++++++ .../src/app/(tabs)/order/_types/order.ts | 23 ++++++ 4 files changed, 181 insertions(+) create mode 100644 apps/owner/src/app/(tabs)/order/_components/modal/AcceptOrderModal.tsx create mode 100644 apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx create mode 100644 apps/owner/src/app/(tabs)/order/_constants/mockOrders.ts create mode 100644 apps/owner/src/app/(tabs)/order/_types/order.ts diff --git a/apps/owner/src/app/(tabs)/order/_components/modal/AcceptOrderModal.tsx b/apps/owner/src/app/(tabs)/order/_components/modal/AcceptOrderModal.tsx new file mode 100644 index 0000000..9f02132 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_components/modal/AcceptOrderModal.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Button, Modal } from "@compasser/design-system"; + +interface AcceptOrderModalProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export default function AcceptOrderModal({ + open, + onClose, + onConfirm, +}: AcceptOrderModalProps) { + return ( + + + + + + } + /> + ); +} \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx new file mode 100644 index 0000000..75a3f49 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button, Modal } from "@compasser/design-system"; + +interface RejectOrderModalProps { + open: boolean; + onClose: () => void; + onConfirm: (reason: string) => void; +} + +export default function RejectOrderModal({ + open, + onClose, + onConfirm, +}: RejectOrderModalProps) { + const [reason, setReason] = useState(""); + + useEffect(() => { + if (!open) { + setReason(""); + } + }, [open]); + + return ( + + + + + + } + > +
+ setReason(e.target.value)} + placeholder="거절 사유를 입력해주세요." + className=" + w-full rounded-[8px] border border-primary + px-[1rem] py-[0.6rem] + body2-r text-default + placeholder:body2-r placeholder:text-gray-300 + outline-none + " + /> +
+
+ ); +} \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_constants/mockOrders.ts b/apps/owner/src/app/(tabs)/order/_constants/mockOrders.ts new file mode 100644 index 0000000..3a72761 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_constants/mockOrders.ts @@ -0,0 +1,36 @@ +import type { ReservationItem } from "../_types/order"; + +export const INITIAL_RESERVATIONS: ReservationItem[] = [ + { + id: 1, + customerName: "김00", + orderDetail: "랜덤박스 1레벨", + price: "6,000원", + quantity: "1개", + status: "pending", + }, + { + id: 2, + customerName: "고00", + orderDetail: "랜덤박스 1레벨", + price: "6,000원", + quantity: "1개", + status: "pending", + }, + { + id: 3, + customerName: "이00", + orderDetail: "랜덤박스 3레벨", + price: "12,000원", + quantity: "2개", + status: "pending", + }, + { + id: 4, + customerName: "박00", + orderDetail: "랜덤박스 2레벨", + price: "9,000원", + quantity: "3개", + status: "pending", + } +]; \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_types/order.ts b/apps/owner/src/app/(tabs)/order/_types/order.ts new file mode 100644 index 0000000..2d48126 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_types/order.ts @@ -0,0 +1,23 @@ +export type OrderTabKey = "reservation" | "order"; + +export type ReservationStatus = "pending" | "completed" | "cancelled"; + +export interface ReservationItem { + id: number; + customerName: string; + orderDetail: string; + price: string; + quantity: string; + status: ReservationStatus; + processedAt?: string; +} + +export interface AcceptModalState { + isOpen: boolean; + orderId: number | null; +} + +export interface RejectModalState { + isOpen: boolean; + orderId: number | null; +} \ No newline at end of file From 768579774716af826104c0eaad32aa37a91f5c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:56:27 +0900 Subject: [PATCH 2/3] feat/owner-orderList --- .../order/_components/OrderHistoryCard.tsx | 38 +++++ .../(tabs)/order/_components/OrderList.tsx | 44 ++++++ .../order/_components/ReservationCard.tsx | 70 ++++++++ .../(tabs)/order/_utils/formatProcessAt.ts | 7 + .../app/(tabs)/order/_utils/orderStatus.ts | 27 ++++ apps/owner/src/app/(tabs)/order/page.tsx | 149 ++++++++++++++++++ 6 files changed, 335 insertions(+) create mode 100644 apps/owner/src/app/(tabs)/order/_components/OrderHistoryCard.tsx create mode 100644 apps/owner/src/app/(tabs)/order/_components/OrderList.tsx create mode 100644 apps/owner/src/app/(tabs)/order/_components/ReservationCard.tsx create mode 100644 apps/owner/src/app/(tabs)/order/_utils/formatProcessAt.ts create mode 100644 apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts create mode 100644 apps/owner/src/app/(tabs)/order/page.tsx diff --git a/apps/owner/src/app/(tabs)/order/_components/OrderHistoryCard.tsx b/apps/owner/src/app/(tabs)/order/_components/OrderHistoryCard.tsx new file mode 100644 index 0000000..50161f8 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_components/OrderHistoryCard.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Card } from "@compasser/design-system"; +import type { ReservationItem } from "../_types/order"; +import { getStatusClassName, getStatusLabel } from "../_utils/orderStatus"; + +interface OrderHistoryCardProps { + order: ReservationItem; +} + +export default function OrderHistoryCard({ order }: OrderHistoryCardProps) { + return ( + +
+
+ 주문자 + {order.customerName} + + {getStatusLabel(order.status)} + + + 주문내역 + {order.orderDetail} + + 가격 + {order.price} + + 수량 + {order.quantity} +
+ +
+ {order.processedAt} +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_components/OrderList.tsx b/apps/owner/src/app/(tabs)/order/_components/OrderList.tsx new file mode 100644 index 0000000..3426408 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_components/OrderList.tsx @@ -0,0 +1,44 @@ +"use client"; + +import ReservationCard from "./ReservationCard"; +import OrderHistoryCard from "./OrderHistoryCard"; +import type { OrderTabKey, ReservationItem } from "../_types/order"; + +interface OrderListProps { + activeTab: OrderTabKey; + orders: ReservationItem[]; + onAccept: (orderId: number) => void; + onReject: (orderId: number) => void; +} + +export default function OrderList({ + activeTab, + orders, + onAccept, + onReject, +}: OrderListProps) { + if (orders.length === 0) { + return ( +
+

주문 내역이 없어요.

+
+ ); + } + + return ( +
+ {activeTab === "reservation" + ? orders.map((order) => ( + + )) + : orders.map((order) => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_components/ReservationCard.tsx b/apps/owner/src/app/(tabs)/order/_components/ReservationCard.tsx new file mode 100644 index 0000000..3798d41 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_components/ReservationCard.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { Card, Button } from "@compasser/design-system"; +import type { ReservationItem } from "../_types/order"; +import { getStatusClassName, getStatusLabel } from "../_utils/orderStatus"; + +interface ReservationCardProps { + order: ReservationItem; + onAccept: (orderId: number) => void; + onReject: (orderId: number) => void; +} + +export default function ReservationCard({ + order, + onAccept, + onReject, +}: ReservationCardProps) { + const isPending = order.status === "pending"; + + return ( + +
+
+ 주문자 + {order.customerName} + + {getStatusLabel(order.status)} + + + 주문내역 + {order.orderDetail} + + 가격 + {order.price} + + 수량 + {order.quantity} +
+ + {isPending ? ( +
+ + + +
+ ) : ( +
+ {order.processedAt} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_utils/formatProcessAt.ts b/apps/owner/src/app/(tabs)/order/_utils/formatProcessAt.ts new file mode 100644 index 0000000..b74de73 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_utils/formatProcessAt.ts @@ -0,0 +1,7 @@ +export const formatProcessedAt = (date: Date) => { + const year = String(date.getFullYear()).slice(2); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + + return `${year}/${month}/${day}`; +}; \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts new file mode 100644 index 0000000..d5bf8a4 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts @@ -0,0 +1,27 @@ +import type { ReservationStatus } from "../_types/order"; + +export const getStatusLabel = (status: ReservationStatus) => { + switch (status) { + case "pending": + return "확인 대기중"; + case "completed": + return "거래완료"; + case "cancelled": + return "거래취소"; + default: + return ""; + } +}; + +export const getStatusClassName = (status: ReservationStatus) => { + switch (status) { + case "pending": + return "body1-m text-secondary"; + case "completed": + return "body1-m text-primary"; + case "cancelled": + return "body1-m text-gray-500"; + default: + return ""; + } +}; \ No newline at end of file diff --git a/apps/owner/src/app/(tabs)/order/page.tsx b/apps/owner/src/app/(tabs)/order/page.tsx new file mode 100644 index 0000000..be64df4 --- /dev/null +++ b/apps/owner/src/app/(tabs)/order/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { Header, TopTabBar } from "@compasser/design-system"; +import OrderList from "./_components/OrderList"; +import AcceptOrderModal from "./_components/modal/AcceptOrderModal"; +import RejectOrderModal from "./_components/modal/RejectOrderModal"; +import { INITIAL_RESERVATIONS } from "./_constants/mockOrders"; +import { formatProcessedAt } from "./_utils/formatProcessAt"; +import type { + AcceptModalState, + OrderTabKey, + RejectModalState, + ReservationItem, +} from "./_types/order"; + +export default function OrderStatusPage() { + const [activeTab, setActiveTab] = useState("reservation"); + const [orders, setOrders] = useState(INITIAL_RESERVATIONS); + + const [acceptModal, setAcceptModal] = useState({ + isOpen: false, + orderId: null, + }); + + const [rejectModal, setRejectModal] = useState({ + isOpen: false, + orderId: null, + }); + + const reservationOrders = useMemo( + () => orders.filter((order) => order.status === "pending"), + [orders] + ); + + const completedOrders = useMemo( + () => orders.filter((order) => order.status !== "pending"), + [orders] + ); + + const currentOrders = + activeTab === "reservation" ? reservationOrders : completedOrders; + + const openAcceptModal = (orderId: number) => { + setAcceptModal({ + isOpen: true, + orderId, + }); + }; + + const closeAcceptModal = () => { + setAcceptModal({ + isOpen: false, + orderId: null, + }); + }; + + const openRejectModal = (orderId: number) => { + setRejectModal({ + isOpen: true, + orderId, + }); + }; + + const closeRejectModal = () => { + setRejectModal({ + isOpen: false, + orderId: null, + }); + }; + + const handleAcceptOrder = () => { + if (acceptModal.orderId === null) return; + + const now = formatProcessedAt(new Date()); + + setOrders((prev) => + prev.map((order) => + order.id === acceptModal.orderId + ? { + ...order, + status: "completed", + processedAt: now, + } + : order + ) + ); + + closeAcceptModal(); + }; + + const handleRejectOrder = (_reason: string) => { + if (rejectModal.orderId === null) return; + + const now = formatProcessedAt(new Date()); + + setOrders((prev) => + prev.map((order) => + order.id === rejectModal.orderId + ? { + ...order, + status: "cancelled", + processedAt: now, + } + : order + ) + ); + + closeRejectModal(); + }; + + return ( + <> +
+
+ + setActiveTab(key as OrderTabKey)} + /> + +
+ +
+
+ + + + + + ); +} \ No newline at end of file From 7572263e4232412a98ade4b3d605f5f68aa3de7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B3=A0=EB=AF=BC=EA=B7=A0?= <97932282+skyblue1232@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:10:38 +0900 Subject: [PATCH 3/3] chore/tab-order --- apps/owner/src/app/(tabs)/order/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/owner/src/app/(tabs)/order/page.tsx b/apps/owner/src/app/(tabs)/order/page.tsx index be64df4..0bd8071 100644 --- a/apps/owner/src/app/(tabs)/order/page.tsx +++ b/apps/owner/src/app/(tabs)/order/page.tsx @@ -17,6 +17,8 @@ import type { export default function OrderStatusPage() { const [activeTab, setActiveTab] = useState("reservation"); const [orders, setOrders] = useState(INITIAL_RESERVATIONS); + const isOrderTabKey = (key: string): key is OrderTabKey => + key === "reservation" || key === "order"; const [acceptModal, setAcceptModal] = useState({ isOpen: false, @@ -120,7 +122,9 @@ export default function OrderStatusPage() { { key: "order", label: "주문" }, ]} activeKey={activeTab} - onTabChange={(key) => setActiveTab(key as OrderTabKey)} + onTabChange={(key) => { + if (isOrderTabKey(key)) setActiveTab(key); + }} />