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
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Modal
open={open}
Expand All @@ -47,7 +55,8 @@ export default function RejectOrderModal({
variant="primary"
fullWidth={false}
className="px-[2.2rem]"
onClick={() => onConfirm(reason)}
onClick={handleConfirm}
disabled={!isValidReason}
>
거절하기
</Button>
Expand All @@ -58,7 +67,7 @@ export default function RejectOrderModal({
<input
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="거절 사유를 입력해주세요."
placeholder="거절 사유를 입력해주세요. (10자 이상)"
className="
w-full rounded-[8px] border border-primary
px-[1rem] py-[0.6rem]
Expand All @@ -67,6 +76,12 @@ export default function RejectOrderModal({
outline-none
"
/>

{!isValidReason && reason.length > 0 && (
<p className="caption-m mt-[0.6rem] text-secondary">
거절 사유는 10자 이상 입력해주세요.
</p>
)}
</div>
</Modal>
);
Expand Down
7 changes: 6 additions & 1 deletion apps/owner/src/app/(tabs)/order/_types/order.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +14,7 @@ export interface ReservationItem {
quantity: string;
status: ReservationStatus;
processedAt?: string;
rejectReason?: string;
}

export interface AcceptModalState {
Expand Down
36 changes: 20 additions & 16 deletions apps/owner/src/app/(tabs)/order/_utils/orderStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
};
151 changes: 105 additions & 46 deletions apps/owner/src/app/(tabs)/order/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderTabKey>("reservation");
const [orders, setOrders] = useState<ReservationItem[]>(INITIAL_RESERVATIONS);
const isOrderTabKey = (key: string): key is OrderTabKey =>
key === "reservation" || key === "order";

const [acceptModal, setAcceptModal] = useState<AcceptModalState>({
isOpen: false,
Expand All @@ -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({
Expand Down Expand Up @@ -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,
},
Comment on lines +147 to +153

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

거절 요청 payload의 상태값이 잘못되어 있습니다.

Line [151]에서 status: "REQUESTED"를 보내면 거절 요청 의미와 충돌합니다. 이 값은 제거하거나, 서버가 필요로 하면 REJECTED로 맞춰야 합니다.

수정 예시
     rejectMutation.mutate(
       {
         reservationId: rejectModal.orderId,
         body: {
-          status: "REQUESTED",
-          rejectReason: reason,
+          rejectReason: reason.trim(),
         },
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rejectMutation.mutate(
{
reservationId: rejectModal.orderId,
body: {
status: "REQUESTED",
rejectReason: reason,
},
rejectMutation.mutate(
{
reservationId: rejectModal.orderId,
body: {
rejectReason: reason.trim(),
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/owner/src/app/`(tabs)/order/page.tsx around lines 147 - 153, The payload
sent to rejectMutation.mutate is using an incorrect status ("REQUESTED"); update
the mutation payload in the rejectMutation.mutate call so that the body either
omits the status field entirely or sets it to the server-expected value
("REJECTED") instead of "REQUESTED" (refer to the call using
rejectMutation.mutate, the payload object with reservationId:
rejectModal.orderId, and body: { rejectReason: reason }).

},
{
onSuccess: closeRejectModal,
},
);

closeRejectModal();
};

return (
Expand All @@ -128,12 +175,24 @@ export default function OrderStatusPage() {
/>

<section className="flex-1 overflow-y-auto px-[1.6rem] pt-[2.8rem] pb-[10rem]">
<OrderList
activeTab={activeTab}
orders={currentOrders}
onAccept={openAcceptModal}
onReject={openRejectModal}
/>
{isLoading ? (
<div className="flex h-full items-center justify-center">
<p className="body1-m text-gray-600">불러오는 중이에요.</p>
</div>
) : isError ? (
<div className="flex h-full items-center justify-center">
<p className="body1-m text-gray-600">
주문 내역을 불러오지 못했어요.
</p>
</div>
) : (
<OrderList
activeTab={activeTab}
orders={currentOrders}
onAccept={openAcceptModal}
onReject={openRejectModal}
/>
)}
</section>
</main>

Expand Down
Original file line number Diff line number Diff line change
@@ -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));
};
Original file line number Diff line number Diff line change
@@ -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));
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { ownerModule } from "@/shared/api/api";

export const usePendingReservationsQuery = () => {
return useQuery(ownerModule.queries.pendingReservations());
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { ownerModule } from "@/shared/api/api";

export const useProcessedReservationsQuery = () => {
return useQuery(ownerModule.queries.processedReservations());
};
4 changes: 2 additions & 2 deletions packages/api/src/domains/owner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines 14 to 17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

rejectReason 필수 계약을 타입에서 강제하지 못하고 있습니다.

ReservationRejectReqDTOrejectReason가 필수인데 body가 optional이라 reject 호출이 컴파일 단계에서 통과할 수 있습니다. 현재 rejectReservationbody ?? {}를 전송하므로 런타임 4xx를 유발할 수 있습니다.

🐛 제안 diff
-export interface ReservationDecisionParams {
+export interface ApproveReservationParams {
   reservationId: number;
-  body?: ReservationRejectReqDTO;
+}
+
+export interface RejectReservationParams {
+  reservationId: number;
+  body: ReservationRejectReqDTO;
 }
@@
-    approveReservation: async ({ reservationId }: ReservationDecisionParams) => {
+    approveReservation: async ({ reservationId }: ApproveReservationParams) => {
@@
-    }: ReservationDecisionParams) => {
+    }: RejectReservationParams) => {
       const { data } = await api.privateClient.patch<ReservationResponse>(
         `/owners/my-store/reservations/${reservationId}/reject`,
-        body ?? {},
+        body,
       );
       return data;
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/domains/owner.ts` around lines 14 - 17, The
ReservationDecisionParams type allows body to be optional even though
ReservationRejectReqDTO requires rejectReason, causing potential runtime 4xx
when rejectReservation sends body ?? {}; change ReservationDecisionParams to
require body (replace body?: ReservationRejectReqDTO with body:
ReservationRejectReqDTO) and remove the fallback usage of body ?? {} in
rejectReservation so callers must pass a valid ReservationRejectReqDTO (with
rejectReason) and the compiler will enforce it.


export const createOwnerModule = (api: CompasserApi) => {
Expand Down
Loading
Loading