Skip to content

Feat/order history#57

Merged
Seoje1405 merged 18 commits intodevelopfrom
feat/order-history
Feb 11, 2026
Merged

Feat/order history#57
Seoje1405 merged 18 commits intodevelopfrom
feat/order-history

Conversation

@Seoje1405
Copy link
Copy Markdown
Contributor

📝 개요

  • 작업한 내용의 핵심을 간단히 적어주세요.
  • 관련 이슈 번호: #이슈번호

🚀 주요 변경 사항

  • 구체적인 변경 사항 1
  • 구체적인 변경 사항 2

📸 스크린샷 (선택)

기능 구현 화면
사진을 여기에 드래그하세요

✅ 체크리스트

  • 빌드 테스트를 완료했나요?
  • 코드 컨벤션을 준수했나요?
  • 불필요한 console.log는 제거했나요?

이슈 해결 여부

PR 본문에 Closes #이슈번호 이라고 적으면, PR이 머지될 때 해당 이슈가 자동으로 닫힙니다

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 11, 2026

Walkthrough

주문 취소·삭제 기능과 관련 UI·타입·유틸을 추가합니다. 서버 액션(getRefundInfo, cancelOrder, deleteOrder), 다단계 취소 폼(CancelForm)과 페이지, 주문 목록·카드·검색 모달, 삭제 버튼, Calendar/Popover/CloseXButton UI primitives, 날짜 유틸 및 관련 타입·enum 변경이 포함됩니다.

Changes

Cohort / File(s) Summary
Server Actions
src/app/actions/order.ts
getRefundInfo, cancelOrder, deleteOrder 추가. API 호출, 에러 로깅/재던짐; cancelOrder 성공 시 orders 페이지 revalidate 호출. 타입 임포트(OrderCancelRequest, OrderCancelResponse, OrderRefundInfoResponse) 추가.
Cancel Flow
src/app/orders/[orderId]/cancel/_components/cancel-form.tsx, src/app/orders/[orderId]/cancel/page.tsx
클라이언트 다단계 취소 폼(사유→확인→완료)과 라우트 페이지 추가. 환불정보 조회, 취소 API 호출, 확인 모달, addToCart 옵션, 결과 렌더링 및 리디렉션 처리.
Order Pages
src/app/orders/page.tsx, src/app/orders/[orderId]/page.tsx, src/app/payment/page.tsx
주문 목록 페이지 추가(force-dynamic) 및 메타데이터; 주문 상세 레이아웃 개편(카드 기반, CloseXButton 교체, DeleteOrderButton 삽입); payment 페이지에 dynamic = 'force-dynamic' 추가.
Order List & Card
src/components/orders/order-list.tsx, src/components/orders/order-list-card.tsx, src/components/orders/order-search-modal.tsx
주문 목록 컴포넌트(페이징·더보기), 주문 카드 렌더링, 날짜/키워드 기반 검색 모달(프리셋·직접입력) 추가. 초기 데이터 주입 및 검색 로직 포함.
Delete Button
src/components/orders/delete-order-button.tsx
주문 삭제 UI 컴포넌트 추가: 확인 모달 → deleteOrder 호출 → 성공 시 /orders로 네비게이션, 에러 처리 및 로딩 상태.
UI Primitives
src/components/ui/calendar.tsx, src/components/ui/popover.tsx, src/components/ui/close-button.tsx
react-day-picker 기반 Calendar 및 DayButton, Radix Popover 래퍼들, CloseXButton(작은 X 아이콘) 추가. 포털 사용·포커스 관리·데이터 슬롯 적용.
Domain Types & Enums
src/types/domain/order.ts, src/types/enums.ts
취소/환불 타입 추가(OrderCancelRequest, OrderCancelResponse, RefundInfo, OrderRefundInfoResponse). OrderStatusCANCELLED 추가.
Date Utils
src/lib/date-utils.ts
기간 프리셋 타입과 getDefaultDateRange 추가(1개월/6개월/1년). 날짜 포맷 헬퍼 포함.
Imports/Consumers
src/components/..., src/app/...
새 타입·액션을 소비하도록 여러 컴포넌트/페이지에서 임포트 확장. 해당 참조 지점 검토 필요.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client as CancelForm (Client)
    participant Server as ServerActions
    participant API as Order API

    User->>Client: 취소 사유 선택 및 확인 요청
    Client->>Server: getRefundInfo(orderId)
    Server->>API: GET /orders/{id}/refund-info
    API-->>Server: RefundInfo
    Server-->>Client: RefundInfo
    User->>Client: 최종 확인 (addToCart 선택 가능)
    Client->>Server: cancelOrder(orderId, {cancelReason, cancelDetail, addToCart})
    Server->>API: POST /orders/{id}/cancel
    API-->>Server: OrderCancelResponse
    Server->>Server: revalidate /orders
    Server-->>Client: OrderCancelResponse
    Client->>User: 취소 완료 화면 표시 / 리디렉션
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

✨ FEATURE

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR 설명은 템플릿 형식만 제공하고 구체적인 변경 사항, 개요, 스크린샷이 채워지지 않아 의미 있는 정보를 전달하지 못합니다. PR 본문의 '📝 개요'와 '🚀 주요 변경 사항' 섹션을 실제 구현 내용으로 채워주세요. 예: 주문 조회 페이지, 주문 취소 기능, 관련 컴포넌트 추가 내용 기술.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Feat/order history'는 변경 사항의 주요 내용(주문 이력 기능 추가)을 명확하게 나타내며, 광범위한 파일 변경(주문 페이지, 취소 폼, 컴포넌트 추가 등)을 적절히 요약합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/order-history

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/app/orders/[orderId]/cancel/_components/cancel-form.tsx (5)

39-74: useFocusTrap: Escape 키 핸들링 및 focusable selector 보완 필요

WAI-ARIA dialog 패턴에서 Escape 키로 모달을 닫는 것은 필수입니다. 현재 훅에는 Escape 핸들러가 없고, focusable 요소 selector에 a[href], textarea, select 등이 누락되어 있습니다.

♻️ 개선 제안
-function useFocusTrap(active: boolean) {
+function useFocusTrap(active: boolean, onEscape?: () => void) {
   const ref = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
     if (!active || !ref.current) return;
     const el = ref.current;
     const prev = document.activeElement as HTMLElement | null;
     const firstBtn = el.querySelector<HTMLElement>('button:not([disabled])');
     firstBtn?.focus();
 
+    const FOCUSABLE =
+      'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
+
     const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') {
+        onEscape?.();
+        return;
+      }
       if (e.key !== 'Tab') return;
-      const focusable = el.querySelectorAll<HTMLElement>(
-        'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])',
-      );
+      const focusable = el.querySelectorAll<HTMLElement>(FOCUSABLE);
       if (focusable.length === 0) return;

219-224: window.location.replacerouter.replace 통일

cancelOrder 서버 액션에서 이미 revalidatePath('/orders')를 호출하므로, window.location.replace로 풀 리로드할 필요가 없습니다. 다른 곳에서는 router.replace를 사용하고 있어 일관성도 떨어집니다.

        <button
          className="bg-ongil-teal sticky bottom-20 mt-8 w-full rounded-xl py-4 text-2xl text-white"
-          onClick={() => window.location.replace('/orders')}
+          onClick={() => router.replace('/orders')}
        >

248-250: key={idx} 대신 고유 식별자 사용 권장

배열 인덱스를 key로 사용하면 리스트 변경 시 불필요한 리렌더링이 발생할 수 있습니다. OrderItem에 고유 ID가 있다면 활용하세요.

-          {refundItems.map((item, idx) => (
-            <div
-              key={idx}
+          {refundItems.map((item) => (
+            <div
+              key={`${item.productId}-${item.selectedColor}-${item.selectedSize}`}

365-379: 사유 선택 버튼에 aria-pressed 누락

시각적으로는 선택 상태가 표시되지만, 스크린 리더 사용자는 어떤 사유가 선택되었는지 알 수 없습니다.

          <button
            key={reason.id}
            onClick={() => handleReasonSelect(reason.id)}
+           aria-pressed={selectedReason === reason.id}
            className={`rounded-xl border-2 p-5 text-left transition-all duration-200 outline-none ${

76-96: 상태 변수가 많아 복잡도가 높음 — 추후 useReducer 전환 고려

10개의 useState가 서로 의존적으로 업데이트됩니다 (step, alertMessage, showModal, submitting 등). 현재 동작에는 문제 없지만, 상태 전이가 복잡해지면 useReducer로 묶는 것이 상태 불일치 방지에 유리합니다.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@src/app/orders/`[orderId]/cancel/page.tsx:
- Around line 16-22: Add an authentication guard to CancelReasonPage: call
auth() at the top of the component and if it returns no user (or
unauthenticated) call redirect('/login') before resolving params and fetching
data; also wrap the getOrderDetail(numericId) call in a try/catch and on a 401
response perform redirect('/login') (or rethrow for other errors) so
unauthenticated users are sent to the login page instead of seeing an error
screen. Reference: CancelReasonPage, getOrderDetail, auth(), redirect('/login').

In `@src/app/orders/`[orderId]/page.tsx:
- Around line 117-154: Extract the repeated reduce calculation into a local
variable before the JSX render (e.g., const itemsTotal =
order.orderItems.reduce((sum, item) => sum + item.priceAtOrder * item.quantity,
0)) and use that variable in the three places where order.orderItems.reduce(...)
is currently used (for 상품 금액 display, 할인 금액 calculation/conditional, and any
other reuse), and compute discount = itemsTotal - order.totalAmount to drive the
conditional rendering and discount display instead of re-running the reduce each
time.

In `@src/app/orders/page.tsx`:
- Around line 15-26: The page filters out canceled orders on the client after
fetching (OrderListPage uses getOrders then computes initialOrders by
filtering), which makes response.totalPages (response.totalPages) inconsistent
with the visible items; fix by moving cancel-filtering into the API call: update
calls to getOrders to include a filter parameter (e.g., excludeCancelled /
status filter) so the server returns already-filtered content and totalPages, or
if the API cannot be changed adjust pagination using the filtered result by
recomputing totalPages from response.totalElements minus canceled items before
rendering; locate OrderListPage, getOrders, initialOrders and
response.totalPages to implement the chosen approach.
- Line 15: Add an authentication guard at the start of OrderListPage: call
auth() (await if needed) and if it returns no user, perform a redirect('/login')
using Next's redirect from next/navigation; update the top of OrderListPage (the
async function OrderListPage) to import and use auth() and redirect so
unauthenticated requests are sent to /login before rendering order data.
- Around line 24-26: The filter using
String(order.orderStatus).includes('CANCEL') is too broad; change it to compare
the order.orderStatus value directly against the enum (OrderStatus) so only the
exact CANCEL status is excluded. Update the initialOrders assignment to use
response.content.filter(order => order.orderStatus !== OrderStatus.CANCEL) (or
an explicit set of allowed statuses) and import/reference the OrderStatus enum
so the comparison is type-safe and not string-based.

In `@src/components/orders/delete-order-button.tsx`:
- Line 11: Change the named export to the required default function pattern by
replacing the named export of DeleteOrderButton with a default function export
(export default function DeleteOrderButton(...)) in the component; keep the
function name DeleteOrderButton and its parameter type DeleteOrderButtonProps
and ensure you do not use an arrow function so the file conforms to the
src/components/**/*.tsx "export default function" guideline.

In `@src/components/orders/order-list-card.tsx`:
- Around line 27-29: Replace raw enum display with a localized label: add a
STATUS_LABEL mapping (e.g., const STATUS_LABEL: Record<string,string> = {
ORDER_RECEIVED: '주문 완료', CANCELLED: '주문 취소', ... }) near the top of the
OrderListCard component file and update the JSX that renders order.orderStatus
to render STATUS_LABEL[order.orderStatus] || order.orderStatus (or a default
like '알 수 없음') instead of the raw enum; reference order.orderStatus in the span
and ensure the mapping covers all expected enum keys.
- Line 13: Change the named export to a default function declaration to follow
the "export default function" pattern: replace "export function OrderListCard({
order }: OrderListCardProps) { … }" with "export default function
OrderListCard({ order }: OrderListCardProps) { … }" and ensure any call
sites/imports are updated from named imports to default imports for
OrderListCard.

In `@src/components/orders/order-list.tsx`:
- Around line 56-58: Client-side filtering of CANCEL orders (the filtered
variable derived from response.content in order-list) causes pagination/count
mismatches and uses a fragile String(...).includes check; instead, push the
filter to the server by adding a status filter parameter to the API request that
returns only non-CANCEL orders and remove the client-side filter, or if server
changes are impossible: replace String(order.orderStatus).includes('CANCEL')
with a strict enum/string comparison (e.g., order.orderStatus === 'CANCEL' or
OrderStatus.CANCEL) and recompute/adjust pagination metadata
(totalPages/currentPage/hasMore) from the post-filtered result so the "load
more" UI reflects the actual displayed items; update the API call site in the
order-list component and remove or adapt the filtered variable accordingly.
- Line 105: In order-list.tsx update the Image src to use an absolute path so
Next.js will load the static asset from the public folder; replace the relative
"icons/search.svg" reference used in the OrderList component (the Image element
in src/components/orders/order-list.tsx) with the absolute path
"/icons/search.svg" to match how order-search-modal.tsx references the same
icon.

In `@src/lib/date-utils.ts`:
- Around line 3-12: getDefaultDateRange currently uses Date#setMonth and
toISOString which cause month-end rollover bugs (e.g., Mar 31 -> Mar 3) and
UTC-based date shifts; replace the manual arithmetic with date-fns utilities:
compute start by calling subMonths(today, 1/6/12) based on the preset and format
both dates with format(date, 'yyyy-MM-dd') to produce local YYYY-MM-DD strings;
update the function getDefaultDateRange to import and use subMonths and format
(or equivalent date-fns functions) instead of setMonth and toISOString to
prevent overflow and UTC offset issues.
🟡 Minor comments (12)
src/components/ui/popover.tsx-58-66 (1)

58-66: ⚠️ Potential issue | 🟡 Minor

PopoverTitle: <h2> props를 받으면서 <div>으로 렌더링 — 시맨틱/접근성 불일치

React.ComponentProps<"h2">를 타입으로 사용하지만 실제 렌더링은 <div>입니다. 스크린 리더가 heading으로 인식하지 못하며, 소비자가 <h2> 전용 속성을 전달할 경우 예상과 다르게 동작합니다.

🛡️ 제안: 렌더 엘리먼트와 타입을 일치시키기
-function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
-  return (
-    <div
-      data-slot="popover-title"
-      className={cn("font-medium", className)}
-      {...props}
-    />
-  )
-}
+function PopoverTitle({ className, ...props }: React.ComponentProps<"div">) {
+  return (
+    <div
+      data-slot="popover-title"
+      className={cn("font-medium", className)}
+      {...props}
+    />
+  )
+}

또는, heading 시맨틱이 필요하다면 엘리먼트를 <h2>로 변경하세요.

src/components/orders/delete-order-button.tsx-40-64 (1)

40-64: ⚠️ Potential issue | 🟡 Minor

모달 접근성 부족 — focus trap, Esc 키, aria 속성 누락

현재 모달은 div로만 구성되어 있어 스크린 리더와 키보드 사용자가 제대로 인식·조작할 수 없습니다. 최소한:

  • role="dialog", aria-modal="true", aria-labelledby 추가
  • Esc 키로 닫기
  • 열릴 때 포커스를 모달 내부로 이동

또는 이미 PR에 추가된 shadcn Dialog 컴포넌트를 활용하면 이 부분을 자동 처리할 수 있습니다.

src/components/orders/order-list-card.tsx-62-68 (1)

62-68: ⚠️ Potential issue | 🟡 Minor

취소된 주문에도 "상품 취소하기" 버튼 노출

이미 취소된 주문(CANCELLED)에 대해 다시 취소 페이지로 이동할 수 있습니다. 상태에 따라 버튼을 조건부 렌더링하세요.

src/components/ui/close-button.tsx-23-28 (1)

23-28: ⚠️ Potential issue | 🟡 Minor

접근성: <button>aria-label 누락

아이콘만 있는 버튼이므로 스크린 리더 사용자를 위해 aria-label을 추가하세요.

 <button
   onClick={() => router.back()}
+  aria-label="닫기"
   className="..."
 >
src/components/orders/order-list-card.tsx-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

order.items가 빈 배열이면 repItemundefined — UI에 "undefined" 노출

optional chaining으로 크래시는 막았지만, Line 44에서 undefined / undefined, Line 46에서 undefined개가 그대로 렌더링됩니다. 빈 상태 처리가 필요합니다.

 const repItem = order.items[0];
+if (!repItem) {
+  return (
+    <Card className="rounded-lg border-black p-5">
+      <CardContent className="p-0">
+        <p className="text-center text-gray-500">주문 상품 정보가 없습니다.</p>
+      </CardContent>
+    </Card>
+  );
+}
src/app/orders/[orderId]/cancel/page.tsx-26-30 (1)

26-30: ⚠️ Potential issue | 🟡 Minor

absolute right-5 — 레이아웃 깨짐 가능

absolute 포지셔닝은 부모 headerrelative가 아니면 의도와 다르게 배치됩니다. headerrelative를 추가하거나 flexbox로 정렬하는 것이 안정적입니다.

-      <header className="flex items-center justify-center py-8">
+      <header className="relative flex items-center justify-center py-8">
src/app/orders/[orderId]/cancel/_components/cancel-form.tsx-40-40 (1)

40-40: ⚠️ Potential issue | 🟡 Minor

useRouter 선언 후 미사용 — window.location.replace 대신 router 사용 권장

Line 40에서 useRouter()를 초기화했지만, Line 126에서 window.location.replace('/orders')를 사용하고 있습니다. 이는 풀 페이지 리로드를 발생시켜 클라이언트 사이드 네비게이션의 이점을 잃습니다.

🔧 수정 제안
-          onClick={() => window.location.replace('/orders')}
+          onClick={() => router.replace('/orders')}

Also applies to: 126-126

src/app/orders/[orderId]/page.tsx-43-47 (1)

43-47: ⚠️ Potential issue | 🟡 Minor

absolute 포지셔닝 — relative 부모 누락 가능

Line 45의 absolute right-5는 가장 가까운 positioned ancestor 기준으로 배치됩니다. <header>relative가 없으면 CloseXButton이 의도치 않은 위치에 렌더링될 수 있습니다.

🔧 수정 제안
-      <header className="flex items-center justify-center py-8">
+      <header className="relative flex items-center justify-center py-8">
src/components/orders/order-search-modal.tsx-52-54 (1)

52-54: ⚠️ Potential issue | 🟡 Minor

startDate > endDate 검증 누락

handleSearch에서 startDateendDate보다 이후인 경우에 대한 검증이 없습니다. 사용자가 시작일을 종료일보다 나중으로 선택하면 빈 결과나 API 에러가 발생할 수 있습니다.

🛡️ 수정 제안
 const handleSearch = () => {
+  if (startDate && endDate && startDate > endDate) {
+    alert('시작일은 종료일보다 이전이어야 합니다.');
+    return;
+  }
   onSearch({ keyword, startDate, endDate });
 };
src/app/orders/[orderId]/page.tsx-67-73 (1)

67-73: ⚠️ Potential issue | 🟡 Minor

이미지 fallback 누락 — 다른 파일과 불일치

cancel-form.tsx Line 159에서는 item.imageUrl || '/placeholder.png' 패턴으로 fallback을 제공하지만, 여기서는 src={item.imageUrl}으로 직접 사용합니다. imageUrl이 빈 문자열이나 null이면 Next.js Image 컴포넌트가 에러를 발생시킵니다.

🛡️ 수정 제안
-                src={item.imageUrl}
+                src={item.imageUrl || '/placeholder.png'}
src/components/orders/order-list.tsx-22-22 (1)

22-22: ⚠️ Potential issue | 🟡 Minor

export default function 패턴 위반

🔧 수정 제안
-export function OrderList({
+export default function OrderList({

As per coding guidelines, src/components/**/*.tsx: export default function 패턴 (화살표 함수 금지).

src/components/orders/order-search-modal.tsx-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

export default function 패턴 위반

코딩 가이드라인에 따라 src/components/**/*.tsx 파일은 export default function 패턴을 사용해야 합니다.

🔧 수정 제안
-export function OrderSearchModal({
+export default function OrderSearchModal({

As per coding guidelines, src/components/**/*.tsx: export default function 패턴 (화살표 함수 금지).

🧹 Nitpick comments (10)
src/components/ui/popover.tsx (1)

81-89: named export 사용 — 코딩 가이드라인 export default function 패턴과 불일치

가이드라인은 export default function 패턴을 요구하지만, 이 파일은 7개 컴포넌트를 named export합니다. shadcn/ui 생성 파일 특성상 단일 default export는 비현실적이므로 팀 컨벤션 확인이 필요합니다. UI primitive 유틸리티 파일은 가이드라인 예외로 둘지 논의해 주세요.

As per coding guidelines, src/components/**/*.tsx: "export default function 패턴 (화살표 함수 금지)".

src/components/ui/calendar.tsx (2)

134-174: 인라인 화살표 함수 컴포넌트 (Root, Chevron, WeekNumber)

components prop에 전달되는 Root (line 135), Chevron (line 145), WeekNumber (line 166)가 인라인 화살표 함수로 정의되어 있습니다. 렌더링마다 새 함수 참조가 생성되어 DayPicker 내부에서 불필요한 리렌더링을 유발할 수 있습니다.

shadcn/ui 생성 코드의 표준 패턴이므로 당장은 문제없으나, 성능 이슈가 관측되면 CalendarDayButton처럼 별도 함수로 분리하는 것을 권장합니다.


182-218: CalendarDayButton 구현 — 전반적으로 양호

포커스 관리(useEffect + ref)와 range/selection 상태에 따른 data attribute 매핑이 적절합니다. 몇 가지 참고 사항:

  1. Line 188: getDefaultClassNames()가 매 렌더마다 호출됩니다. 반환값이 순수하므로 모듈 스코프로 호이스팅하면 불필요한 호출을 줄일 수 있습니다.
  2. Line 200: day.date.toLocaleDateString()은 로케일에 따라 다른 문자열을 반환합니다. data-day 속성이 테스트나 선택 로직에 사용된다면 toISOString() 등 로케일 무관 포맷이 더 안전합니다.
  3. Line 211: className 문자열이 매우 길어 가독성이 낮습니다. 기능상 문제는 없으나, 유지보수 시 주의가 필요합니다.
♻️ getDefaultClassNames 호이스팅 예시

Calendar 함수 내부(line 30)와 CalendarDayButton(line 188) 모두에서 호출하고 있으므로, 모듈 최상위로 추출할 수 있습니다:

+const defaultClassNames = getDefaultClassNames()
+
 function Calendar({
   className,
   classNames,
   ...
 }) {
-  const defaultClassNames = getDefaultClassNames()
   ...
 }

 function CalendarDayButton({
   ...
 }) {
-  const defaultClassNames = getDefaultClassNames()
   ...
 }
src/components/ui/close-button.tsx (1)

19-30: CloseButton과 거의 동일한 코드 중복

아이콘 src/크기만 다르고 나머지 로직·스타일이 완전히 동일합니다. 하나의 컴포넌트로 통합하면 유지보수가 편해집니다.

♻️ 통합 예시
-export function CloseButton() {
-  const router = useRouter();
-  return (
-    <button
-      onClick={() => router.back()}
-      className="-ml-2 flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
-    >
-      <Image src="/icons/arrow.svg" width={37} height={37} alt="뒤로가기" />
-    </button>
-  );
-}
-
-export function CloseXButton() {
-  const router = useRouter();
-  return (
-    <button
-      onClick={() => router.back()}
-      className="-ml-2 flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
-    >
-      <Image src="/icons/X.svg" width={23} height={23} alt="닫기" />
-    </button>
-  );
-}
+interface CloseButtonProps {
+  variant?: 'arrow' | 'x';
+}
+
+const config = {
+  arrow: { src: '/icons/arrow.svg', size: 37, alt: '뒤로가기' },
+  x:     { src: '/icons/X.svg',     size: 23, alt: '닫기' },
+} as const;
+
+export function CloseButton({ variant = 'arrow' }: CloseButtonProps) {
+  const router = useRouter();
+  const { src, size, alt } = config[variant];
+  return (
+    <button
+      onClick={() => router.back()}
+      className="-ml-2 flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
+    >
+      <Image src={src} width={size} height={size} alt={alt} />
+    </button>
+  );
+}
src/app/payment/page.tsx (1)

19-19: force-dynamic은 이 페이지에선 사실상 불필요

auth()cookies()를 사용하므로 이미 dynamic으로 판정됩니다. 명시적으로 두는 것 자체는 해롭지 않지만, 실질적인 효과는 없습니다 — 의도적이라면 무시해도 됩니다.

src/components/orders/order-search-modal.tsx (2)

57-58: 모달 접근성(a11y) 속성 누락

풀스크린 모달에 role="dialog", aria-modal="true", aria-label 등의 ARIA 속성이 없습니다. 스크린 리더 사용자가 모달 컨텍스트를 인식할 수 없고, 포커스 트랩도 구현되어 있지 않아 키보드 네비게이션에 문제가 됩니다.

♿ 최소 수정 제안
-    <div className="fixed inset-0 z-50 flex flex-col bg-white">
+    <div
+      role="dialog"
+      aria-modal="true"
+      aria-label="조회 조건 설정"
+      className="fixed inset-0 z-50 flex flex-col bg-white"
+    >

14-19: 컴포넌트 파일에서 유틸리티 re-export

getDefaultDateRange를 컴포넌트 파일에서 re-export하면 소비자가 유틸 함수만 필요할 때도 이 컴포넌트 모듈에 의존하게 됩니다. 소비자가 @/lib/date-utils에서 직접 import하는 것이 더 깔끔합니다.

src/app/orders/[orderId]/cancel/_components/cancel-form.tsx (2)

62-62: 프로덕션 코드에 console.log 디버그 로그 잔존

console.log('환불 정보 응답:', data) — 디버그용 로그가 남아있습니다. 제거하거나, 필요하다면 console.debug로 변경하세요.

🧹 수정 제안
-        console.log('환불 정보 응답:', data);

216-249: 취소 확인 모달 접근성(a11y) 개선 필요

확인 모달에 role="dialog", aria-modal="true" 속성이 없습니다. 또한 ESC 키로 모달 닫기, 포커스 트랩 등이 구현되어 있지 않습니다.

♿ 최소 수정 제안
-          <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
-            <div className="mx-5 w-full max-w-md rounded-2xl bg-white p-6">
+          <div
+            className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
+            role="dialog"
+            aria-modal="true"
+            aria-label="주문 취소 확인"
+          >
+            <div className="mx-5 w-full max-w-md rounded-2xl bg-white p-6">
src/components/orders/order-list.tsx (1)

97-110: 클릭 가능한 <div> — 접근성 문제

검색 트리거가 <div onClick>으로 구현되어 있어 키보드 사용자가 Tab으로 포커스하거나 Enter/Space로 활성화할 수 없습니다. <button> 또는 role="button" + tabIndex={0} + onKeyDown 처리가 필요합니다.

♿ 수정 제안
-        <div
+        <button
+          type="button"
           onClick={() => setIsSearchOpen(true)}
-          className="bg-secondary-gray text-muted-foreground mb-12 flex h-12 w-full cursor-pointer items-center justify-between rounded-md border border-[`#5E5D5D`] px-5 py-2"
+          className="bg-secondary-gray text-muted-foreground mb-12 flex h-12 w-full cursor-pointer items-center justify-between rounded-md border border-[`#5E5D5D`] px-5 py-2 text-left"
         >
           ...
-        </div>
+        </button>

Comment thread src/app/orders/[orderId]/cancel/page.tsx
Comment thread src/app/orders/[orderId]/page.tsx
Comment thread src/app/orders/page.tsx Outdated
Comment on lines +15 to +26
export default async function OrderListPage() {
const defaults = getDefaultDateRange('1m');
const response = await getOrders({
startDate: defaults.startDate,
endDate: defaults.endDate,
page: 0,
size: 10,
});

const initialOrders = response.content.filter(
(order) => !String(order.orderStatus).includes('CANCEL'),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

totalPages 불일치 — 클라이언트 필터링 후 페이지네이션 깨짐

서버에서 받은 totalPages는 취소 주문 포함 기준인데, Line 24에서 취소 주문을 클라이언트에서 제거합니다. 결과적으로 표시되는 아이템 수와 페이지 수가 맞지 않습니다.

취소 주문 제외는 서버 API 파라미터로 처리하거나, 서버에서 필터링된 응답을 받는 것이 올바른 접근입니다. 현재 구조에서는 마지막 페이지에 아이템이 0개인 상황도 발생할 수 있습니다.

🤖 Prompt for AI Agents
In `@src/app/orders/page.tsx` around lines 15 - 26, The page filters out canceled
orders on the client after fetching (OrderListPage uses getOrders then computes
initialOrders by filtering), which makes response.totalPages
(response.totalPages) inconsistent with the visible items; fix by moving
cancel-filtering into the API call: update calls to getOrders to include a
filter parameter (e.g., excludeCancelled / status filter) so the server returns
already-filtered content and totalPages, or if the API cannot be changed adjust
pagination using the filtered result by recomputing totalPages from
response.totalElements minus canceled items before rendering; locate
OrderListPage, getOrders, initialOrders and response.totalPages to implement the
chosen approach.

Comment thread src/app/orders/page.tsx
Comment thread src/app/orders/page.tsx Outdated
Comment on lines +24 to +26
const initialOrders = response.content.filter(
(order) => !String(order.orderStatus).includes('CANCEL'),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

문자열 기반 필터링 → enum 직접 비교로 변경 필요

String(order.orderStatus).includes('CANCEL')은 향후 CANCEL_REQUESTED 등 유사 상태가 추가되면 의도치 않게 필터링됩니다. OrderStatus enum을 직접 비교하세요.

+import { OrderStatus } from '@/types/enums';
+
 const initialOrders = response.content.filter(
-  (order) => !String(order.orderStatus).includes('CANCEL'),
+  (order) => order.orderStatus !== OrderStatus.CANCELLED,
 );
📝 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
const initialOrders = response.content.filter(
(order) => !String(order.orderStatus).includes('CANCEL'),
);
import { OrderStatus } from '@/types/enums';
const initialOrders = response.content.filter(
(order) => order.orderStatus !== OrderStatus.CANCELLED,
);
🤖 Prompt for AI Agents
In `@src/app/orders/page.tsx` around lines 24 - 26, The filter using
String(order.orderStatus).includes('CANCEL') is too broad; change it to compare
the order.orderStatus value directly against the enum (OrderStatus) so only the
exact CANCEL status is excluded. Update the initialOrders assignment to use
response.content.filter(order => order.orderStatus !== OrderStatus.CANCEL) (or
an explicit set of allowed statuses) and import/reference the OrderStatus enum
so the comparison is type-safe and not string-based.

Comment thread src/components/orders/order-list-card.tsx Outdated
Comment thread src/components/orders/order-list-card.tsx
Comment thread src/components/orders/order-list.tsx Outdated
Comment on lines +56 to +58
const filtered = response.content.filter(
(order) => !String(order.orderStatus).includes('CANCEL'),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

클라이언트 사이드 취소 주문 필터링 — 페이지네이션 불일치 발생

서버 응답의 content에서 CANCEL 상태를 클라이언트에서 필터링하면, totalPagescurrentPage는 필터링 전 데이터 기준이므로 실제 표시 항목 수와 불일치가 발생합니다. 예를 들어, 한 페이지에 취소 주문만 있으면 빈 목록이 표시되지만 "더보기" 버튼은 남아있을 수 있습니다.

또한 String(order.orderStatus).includes('CANCEL')은 fragile한 패턴입니다. 서버 API에 status 필터 파라미터를 추가하여 서버 사이드에서 필터링하는 것을 권장합니다.

🤖 Prompt for AI Agents
In `@src/components/orders/order-list.tsx` around lines 56 - 58, Client-side
filtering of CANCEL orders (the filtered variable derived from response.content
in order-list) causes pagination/count mismatches and uses a fragile
String(...).includes check; instead, push the filter to the server by adding a
status filter parameter to the API request that returns only non-CANCEL orders
and remove the client-side filter, or if server changes are impossible: replace
String(order.orderStatus).includes('CANCEL') with a strict enum/string
comparison (e.g., order.orderStatus === 'CANCEL' or OrderStatus.CANCEL) and
recompute/adjust pagination metadata (totalPages/currentPage/hasMore) from the
post-filtered result so the "load more" UI reflects the actual displayed items;
update the API call site in the order-list component and remove or adapt the
filtered variable accordingly.

Comment thread src/components/orders/order-list.tsx Outdated
Comment thread src/lib/date-utils.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/orders/[orderId]/page.tsx (1)

18-24: ⚠️ Potential issue | 🟠 Major

인증 체크 누락 — 다른 주문 페이지와 불일치

orders/page.tsx, orders/[orderId]/cancel/page.tsx는 모두 auth() 체크 후 미인증 시 /login으로 리다이렉트하지만, 이 상세 페이지에는 인증 가드가 없습니다. getOrderDetail()이 인증 실패 시 에러가 노출되며, 보호되지 않은 접근이 가능합니다.

🤖 Fix all issues with AI agents
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx:
- Around line 66-73: The catch block currently calls setAlertMessage(...) and
setStep('reason') together, but because the alert modal (rendered when
alertMessage is truthy) takes precedence the UI never shows the reason step;
remove the immediate setStep('reason') from the catch and instead add a "다시 시도"
(Retry) action to the alert modal component which, when clicked, calls
setAlertMessage(null) and then setStep('reason') (or optionally retries the
original request); update the alert modal's click handler to perform
setAlertMessage(null) followed by setStep('reason') so the reason step is
rendered after the modal is dismissed (reference: setAlertMessage, setStep, and
the alert modal rendering logic that checks if (alertMessage)).
- Around line 104-118: The alert modal rendered when alertMessage is truthy
lacks accessibility attributes and focus management: add role="dialog",
aria-modal="true" and aria-labelledby pointing to the modal title element (the
<p> showing {alertMessage}) by giving that <p> a stable id; implement a focus
trap so keyboard focus cannot escape the modal while open and move initial focus
to the primary button (the button that calls router.replace('/orders')), and
ensure focus is returned to the element that opened the modal when it closes;
apply the same fixes to the other modal rendered around lines 239-272.
- Line 63: Remove the debugging console.log that prints refund details (the line
console.log('환불 정보 응답:', data)) in the cancel form component (e.g., CancelForm
or the refund-handling function) so sensitive refund data is not exposed in
production; if you need auditable logs, replace it with a call to the app's
secure logger (e.g., processLogger or a centralized logging utility) that
redacts PII and includes contextual metadata, otherwise just delete the
console.log line.

In `@src/app/orders/`[orderId]/cancel/page.tsx:
- Line 6: The import is using an absolute filesystem root path; change the
import in page.tsx to use the project alias so the module resolves correctly —
replace the current "import { auth } from '/auth';" with the aliased import
"import { auth } from '@/auth';" (update the import statement that references
auth in src/app/orders/[orderId]/cancel/page.tsx).
- Around line 30-34: The header element wrapping the page title and CloseXButton
lacks positioning context so the absolutely positioned CloseXButton can
misrender; update the header in src/app/orders/[orderId]/cancel/page.tsx to
include a relative positioning class (e.g., add "relative" to the header's
className) so CloseXButton (component CloseXButton) is positioned correctly
within that container and mirrors the layout used on the order details page.

In `@src/app/orders/`[orderId]/page.tsx:
- Around line 48-53: The header's absolutely positioned CloseXButton is missing
a positioned parent, so add positioning to the header element (e.g., include
"relative" in the header's className) so CloseXButton (the component inside the
header with className container "absolute right-5") is positioned relative to
the header; update the header element that contains CloseXButton (the <header>
with className "flex items-center justify-center py-8") to include "relative".
🧹 Nitpick comments (3)
src/app/orders/[orderId]/page.tsx (1)

67-69: key={index} 대신 안정적인 식별자 사용 권장

주문 아이템에 고유 ID가 있다면 key={item.productId} 등으로 교체하는 것이 React 재조정(reconciliation) 안정성에 유리합니다. 동일 상품이 다른 옵션으로 복수 존재할 수 있으므로 복합 키(${item.productId}-${item.selectedColor}-${item.selectedSize})도 고려하세요.

src/app/orders/[orderId]/cancel/_components/cancel-form.tsx (2)

147-149: window.location.replace vs router.replace — 의도적 하드 네비게이션 확인

완료 단계에서 window.location.replace('/orders')로 전체 페이지를 리로드합니다. 알림 모달(Line 112)에서는 router.replace('/orders')를 사용하여 클라이언트 네비게이션을 합니다. 동일 목적지에 대해 동작이 다릅니다. 클라이언트 상태 초기화가 목적이라면 둘 다 window.location.replace로 통일하거나, SPA 경험 유지가 목적이라면 router.replace로 통일하세요.


39-56: 10개의 useState — 상태 복잡도 높음

Step, 선택 사유, 상세 입력, 제출 상태, 장바구니 추가, 모달, 알림, 취소 결과, 환불 정보, 로딩까지 10개의 독립 상태가 있습니다. 서로 연관된 상태 전이가 많으므로 useReducer로 통합하면 상태 불일치(예: alertMessage와 step 간 비동기)를 방지할 수 있습니다. 즉시 필수는 아니지만, 향후 로직 추가 시 유지보수성이 크게 향상됩니다.

setRefundLoading(true);
getRefundInfo(orderId)
.then((data) => {
console.log('환불 정보 응답:', data);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

console.log 제거 필요

프로덕션 코드에 디버그 로그가 남아 있습니다. 환불 정보가 콘솔에 노출됩니다.

🔧 수정 제안
      .then((data) => {
-        console.log('환불 정보 응답:', data);
        setRefundInfo(data);
      })
📝 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
console.log('환불 정보 응답:', data);
🤖 Prompt for AI Agents
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx at line 63,
Remove the debugging console.log that prints refund details (the line
console.log('환불 정보 응답:', data)) in the cancel form component (e.g., CancelForm
or the refund-handling function) so sensitive refund data is not exposed in
production; if you need auditable logs, replace it with a call to the app's
secure logger (e.g., processLogger or a centralized logging utility) that
redacts PII and includes contextual metadata, otherwise just delete the
console.log line.

Comment on lines +66 to +73
.catch((err) => {
console.error(err);
setAlertMessage(
err instanceof Error
? err.message
: '환불 정보를 불러오지 못했습니다.',
);
setStep('reason');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

setStep('reason') — alertMessage가 우선 렌더링되어 사실상 무효

Line 68에서 setAlertMessage를 설정하면 Line 105의 if (alertMessage) 분기가 먼저 렌더링되므로 setStep('reason')(Line 73)은 실행되지만 화면에 반영되지 않습니다. alertMessage를 닫는 동작이 router.replace('/orders')뿐이라 reason 단계로 돌아가는 의도가 실현되지 않습니다.

의도가 에러 시 사유 선택으로 복귀라면, 알림 모달에 "다시 시도" 버튼을 추가하고 setAlertMessage(null)로 초기화하는 방식을 고려하세요.

🤖 Prompt for AI Agents
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx around lines 66
- 73, The catch block currently calls setAlertMessage(...) and setStep('reason')
together, but because the alert modal (rendered when alertMessage is truthy)
takes precedence the UI never shows the reason step; remove the immediate
setStep('reason') from the catch and instead add a "다시 시도" (Retry) action to the
alert modal component which, when clicked, calls setAlertMessage(null) and then
setStep('reason') (or optionally retries the original request); update the alert
modal's click handler to perform setAlertMessage(null) followed by
setStep('reason') so the reason step is rendered after the modal is dismissed
(reference: setAlertMessage, setStep, and the alert modal rendering logic that
checks if (alertMessage)).

Comment thread src/app/orders/[orderId]/cancel/_components/cancel-form.tsx
import { notFound } from 'next/navigation';
import { CloseXButton } from '@/components/ui/close-button';
import { CancelForm } from './_components/cancel-form';
import { auth } from '/auth';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

임포트 경로 오류 — /auth@/auth여야 합니다

'/auth'는 파일시스템 루트로 해석됩니다. 프로젝트의 다른 파일에서 사용하는 @/auth alias 패턴과 일치하지 않으며, 빌드 시 모듈을 찾지 못할 가능성이 높습니다.

🐛 수정 제안
-import { auth } from '/auth';
+import { auth } from '@/auth';
📝 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
import { auth } from '/auth';
import { auth } from '@/auth';
🤖 Prompt for AI Agents
In `@src/app/orders/`[orderId]/cancel/page.tsx at line 6, The import is using an
absolute filesystem root path; change the import in page.tsx to use the project
alias so the module resolves correctly — replace the current "import { auth }
from '/auth';" with the aliased import "import { auth } from '@/auth';" (update
the import statement that references auth in
src/app/orders/[orderId]/cancel/page.tsx).

Comment thread src/app/orders/[orderId]/cancel/page.tsx Outdated
Comment on lines +48 to 53
<header className="flex items-center justify-center py-8">
<h1 className="text-3xl font-semibold">주문 상세</h1>
<div className="absolute right-5">
<CloseXButton />
</div>
<h1 className="text-3xl leading-[18px] font-semibold">주문 상세</h1>
</header>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

absolute right-5relative 부모 없이 위치 지정

headerrelative가 빠져 있어 CloseXButton이 헤더가 아닌 가장 가까운 positioned ancestor 기준으로 배치됩니다. 의도와 다른 레이아웃이 나올 수 있습니다.

🔧 수정 제안
-      <header className="flex items-center justify-center py-8">
+      <header className="relative flex items-center justify-center py-8">
📝 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
<header className="flex items-center justify-center py-8">
<h1 className="text-3xl font-semibold">주문 상세</h1>
<div className="absolute right-5">
<CloseXButton />
</div>
<h1 className="text-3xl leading-[18px] font-semibold">주문 상세</h1>
</header>
<header className="relative flex items-center justify-center py-8">
<h1 className="text-3xl font-semibold">주문 상세</h1>
<div className="absolute right-5">
<CloseXButton />
</div>
</header>
🤖 Prompt for AI Agents
In `@src/app/orders/`[orderId]/page.tsx around lines 48 - 53, The header's
absolutely positioned CloseXButton is missing a positioned parent, so add
positioning to the header element (e.g., include "relative" in the header's
className) so CloseXButton (the component inside the header with className
container "absolute right-5") is positioned relative to the header; update the
header element that contains CloseXButton (the <header> with className "flex
items-center justify-center py-8") to include "relative".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant