diff --git a/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx index 75a3f49..70f79f1 100644 --- a/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx +++ b/apps/owner/src/app/(tabs)/order/_components/modal/RejectOrderModal.tsx @@ -16,12 +16,20 @@ export default function RejectOrderModal({ }: RejectOrderModalProps) { const [reason, setReason] = useState(""); + const trimmedReason = reason.trim(); + const isValidReason = trimmedReason.length >= 10; + useEffect(() => { if (!open) { setReason(""); } }, [open]); + const handleConfirm = () => { + if (!isValidReason) return; + onConfirm(trimmedReason); + }; + return ( onConfirm(reason)} + onClick={handleConfirm} + disabled={!isValidReason} > 거절하기 @@ -58,7 +67,7 @@ export default function RejectOrderModal({ setReason(e.target.value)} - placeholder="거절 사유를 입력해주세요." + placeholder="거절 사유를 입력해주세요. (10자 이상)" className=" w-full rounded-[8px] border border-primary px-[1rem] py-[0.6rem] @@ -67,6 +76,12 @@ export default function RejectOrderModal({ outline-none " /> + + {!isValidReason && reason.length > 0 && ( +

+ 거절 사유는 10자 이상 입력해주세요. +

+ )}
); diff --git a/apps/owner/src/app/(tabs)/order/_types/order.ts b/apps/owner/src/app/(tabs)/order/_types/order.ts index 2d48126..e69633d 100644 --- a/apps/owner/src/app/(tabs)/order/_types/order.ts +++ b/apps/owner/src/app/(tabs)/order/_types/order.ts @@ -1,6 +1,10 @@ export type OrderTabKey = "reservation" | "order"; -export type ReservationStatus = "pending" | "completed" | "cancelled"; +export type ReservationStatus = + | "pending" + | "completed" + | "cancelled" + | "refunded"; export interface ReservationItem { id: number; @@ -10,6 +14,7 @@ export interface ReservationItem { quantity: string; status: ReservationStatus; processedAt?: string; + rejectReason?: string; } export interface AcceptModalState { diff --git a/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts index d5bf8a4..bd99808 100644 --- a/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts +++ b/apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts @@ -2,26 +2,30 @@ import type { ReservationStatus } from "../_types/order"; export const getStatusLabel = (status: ReservationStatus) => { switch (status) { - case "pending": - return "확인 대기중"; - case "completed": - return "거래완료"; - case "cancelled": - return "거래취소"; - default: - return ""; + case "pending": + return "확인 대기중"; + case "completed": + return "거래완료"; + case "cancelled": + return "거래취소"; + case "refunded": + 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 ""; + case "pending": + return "body1-m text-secondary"; + case "completed": + return "body1-m text-primary"; + case "cancelled": + return "body1-m text-gray-500"; + case "refunded": + return "body1-m text-secondary"; + 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 index 0bd8071..d84c112 100644 --- a/apps/owner/src/app/(tabs)/order/page.tsx +++ b/apps/owner/src/app/(tabs)/order/page.tsx @@ -5,20 +5,51 @@ 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"; +import { usePendingReservationsQuery } from "@/shared/queries/query/owner/usePendingReservationsQuery"; +import { useProcessedReservationsQuery } from "@/shared/queries/query/owner/useProcessedReservationsQuery"; +import { useApproveReservationMutation } from "@/shared/queries/mutation/owner/useApproveReservationMutation"; +import { useRejectReservationMutation } from "@/shared/queries/mutation/owner/useRejectReservationMutation"; +import type { ReservationDTO } from "@compasser/api"; + +const formatPrice = (price: number) => `${price.toLocaleString()}원`; + +const mapReservationStatus = ( + status: ReservationDTO["status"], +): ReservationItem["status"] => { + switch (status) { + case "REQUESTED": + return "pending"; + case "APPROVED": + return "completed"; + case "REJECTED": + return "cancelled"; + case "CANCELED": + return "refunded"; + default: + return "pending"; + } +}; + +const mapReservationToItem = ( + reservation: ReservationDTO, +): ReservationItem => ({ + id: reservation.reservationId, + customerName: reservation.customerName, + orderDetail: reservation.randomBoxName, + price: formatPrice(reservation.totalPrice), + quantity: `${reservation.requestedQuantity}개`, + status: mapReservationStatus(reservation.status), + rejectReason: reservation.rejectReason, +}); 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, @@ -30,18 +61,44 @@ export default function OrderStatusPage() { orderId: null, }); + const { + data: pendingReservationData, + isLoading: isPendingLoading, + isError: isPendingError, + } = usePendingReservationsQuery(); + + const { + data: processedReservationData, + isLoading: isProcessedLoading, + isError: isProcessedError, + } = useProcessedReservationsQuery(); + + const approveMutation = useApproveReservationMutation(); + const rejectMutation = useRejectReservationMutation(); + + const isOrderTabKey = (key: string): key is OrderTabKey => + key === "reservation" || key === "order"; + const reservationOrders = useMemo( - () => orders.filter((order) => order.status === "pending"), - [orders] + () => + pendingReservationData?.reservations.map(mapReservationToItem) ?? [], + [pendingReservationData], ); - const completedOrders = useMemo( - () => orders.filter((order) => order.status !== "pending"), - [orders] + const processedOrders = useMemo( + () => + processedReservationData?.reservations.map(mapReservationToItem) ?? [], + [processedReservationData], ); const currentOrders = - activeTab === "reservation" ? reservationOrders : completedOrders; + activeTab === "reservation" ? reservationOrders : processedOrders; + + const isLoading = + activeTab === "reservation" ? isPendingLoading : isProcessedLoading; + + const isError = + activeTab === "reservation" ? isPendingError : isProcessedError; const openAcceptModal = (orderId: number) => { setAcceptModal({ @@ -74,41 +131,31 @@ export default function OrderStatusPage() { 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 - ) + approveMutation.mutate( + { + reservationId: acceptModal.orderId, + }, + { + onSuccess: closeAcceptModal, + }, ); - - closeAcceptModal(); }; - const handleRejectOrder = (_reason: string) => { + 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 - ) + rejectMutation.mutate( + { + reservationId: rejectModal.orderId, + body: { + status: "REQUESTED", + rejectReason: reason, + }, + }, + { + onSuccess: closeRejectModal, + }, ); - - closeRejectModal(); }; return ( @@ -128,12 +175,24 @@ export default function OrderStatusPage() { />
- + {isLoading ? ( +
+

불러오는 중이에요.

+
+ ) : isError ? ( +
+

+ 주문 내역을 불러오지 못했어요. +

+
+ ) : ( + + )}
diff --git a/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts b/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts new file mode 100644 index 0000000..1c67ce1 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/owner/useApproveReservationMutation.ts @@ -0,0 +1,8 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useApproveReservationMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(ownerModule.mutations.approveReservation(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts b/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts new file mode 100644 index 0000000..2960b00 --- /dev/null +++ b/apps/owner/src/shared/queries/mutation/owner/useRejectReservationMutation.ts @@ -0,0 +1,8 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useRejectReservationMutation = () => { + const queryClient = useQueryClient(); + + return useMutation(ownerModule.mutations.rejectReservation(queryClient)); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts b/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts new file mode 100644 index 0000000..fb1800a --- /dev/null +++ b/apps/owner/src/shared/queries/query/owner/usePendingReservationsQuery.ts @@ -0,0 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const usePendingReservationsQuery = () => { + return useQuery(ownerModule.queries.pendingReservations()); +}; \ No newline at end of file diff --git a/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts b/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts new file mode 100644 index 0000000..52fd953 --- /dev/null +++ b/apps/owner/src/shared/queries/query/owner/useProcessedReservationsQuery.ts @@ -0,0 +1,6 @@ +import { useQuery } from "@tanstack/react-query"; +import { ownerModule } from "@/shared/api/api"; + +export const useProcessedReservationsQuery = () => { + return useQuery(ownerModule.queries.processedReservations()); +}; \ No newline at end of file diff --git a/packages/api/src/domains/owner.ts b/packages/api/src/domains/owner.ts index 11102ad..2a14494 100644 --- a/packages/api/src/domains/owner.ts +++ b/packages/api/src/domains/owner.ts @@ -6,14 +6,14 @@ import type { BusinessLicenseVerifyReqDTO, OwnerUpgradeRespDTO, ReservationListResponse, - ReservationReqDTO, + ReservationRejectReqDTO, ReservationResponse, SettlementPreviewDTO, } from "../models/owner"; export interface ReservationDecisionParams { reservationId: number; - body?: ReservationReqDTO; + body?: ReservationRejectReqDTO; } export const createOwnerModule = (api: CompasserApi) => { diff --git a/packages/api/src/models/owner.ts b/packages/api/src/models/owner.ts index c9a1c24..6c5ce80 100644 --- a/packages/api/src/models/owner.ts +++ b/packages/api/src/models/owner.ts @@ -13,30 +13,27 @@ export interface OwnerUpgradeRespDTO { alreadyUpgraded: boolean; } -export interface ReservationReqDTO { +export interface ReservationRejectReqDTO { status?: ReservationStatus; - rejectReason?: string; + rejectReason: string; } export interface ReservationDTO { reservationId: number; memberId: number; - nickName: string; + customerName: string; storeId: number; storeName: string; randomBoxId: number; randomBoxName: string; - price: number; + totalPrice: number; status: ReservationStatus; requestedQuantity: number; rejectReason?: string; - createdAt: string; - updatedAt: string; } export interface ReservationListDTO { reservations: ReservationDTO[]; - count: number; } export interface SettlementPreviewReservationDTO {