Conversation
Walkthrough주문 취소·삭제 기능과 관련 UI·타입·유틸을 추가합니다. 서버 액션(getRefundInfo, cancelOrder, deleteOrder), 다단계 취소 폼(CancelForm)과 페이지, 주문 목록·카드·검색 모달, 삭제 버튼, Calendar/Popover/CloseXButton UI primitives, 날짜 유틸 및 관련 타입·enum 변경이 포함됩니다. Changes
Sequence DiagramsequenceDiagram
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: 취소 완료 화면 표시 / 리디렉션
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
There was a problem hiding this comment.
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가 빈 배열이면repItem이undefined— 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포지셔닝은 부모header가relative가 아니면 의도와 다르게 배치됩니다.header에relative를 추가하거나 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에서startDate가endDate보다 이후인 경우에 대한 검증이 없습니다. 사용자가 시작일을 종료일보다 나중으로 선택하면 빈 결과나 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.tsxLine 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)
componentsprop에 전달되는Root(line 135),Chevron(line 145),WeekNumber(line 166)가 인라인 화살표 함수로 정의되어 있습니다. 렌더링마다 새 함수 참조가 생성되어 DayPicker 내부에서 불필요한 리렌더링을 유발할 수 있습니다.shadcn/ui 생성 코드의 표준 패턴이므로 당장은 문제없으나, 성능 이슈가 관측되면
CalendarDayButton처럼 별도 함수로 분리하는 것을 권장합니다.
182-218:CalendarDayButton구현 — 전반적으로 양호포커스 관리(
useEffect+ ref)와 range/selection 상태에 따른 data attribute 매핑이 적절합니다. 몇 가지 참고 사항:
- Line 188:
getDefaultClassNames()가 매 렌더마다 호출됩니다. 반환값이 순수하므로 모듈 스코프로 호이스팅하면 불필요한 호출을 줄일 수 있습니다.- Line 200:
day.date.toLocaleDateString()은 로케일에 따라 다른 문자열을 반환합니다.data-day속성이 테스트나 선택 로직에 사용된다면toISOString()등 로케일 무관 포맷이 더 안전합니다.- 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>
| 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'), | ||
| ); |
There was a problem hiding this comment.
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.
| const initialOrders = response.content.filter( | ||
| (order) => !String(order.orderStatus).includes('CANCEL'), | ||
| ); |
There was a problem hiding this comment.
문자열 기반 필터링 → 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.
| 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.
| const filtered = response.content.filter( | ||
| (order) => !String(order.orderStatus).includes('CANCEL'), | ||
| ); |
There was a problem hiding this comment.
클라이언트 사이드 취소 주문 필터링 — 페이지네이션 불일치 발생
서버 응답의 content에서 CANCEL 상태를 클라이언트에서 필터링하면, totalPages와 currentPage는 필터링 전 데이터 기준이므로 실제 표시 항목 수와 불일치가 발생합니다. 예를 들어, 한 페이지에 취소 주문만 있으면 빈 목록이 표시되지만 "더보기" 버튼은 남아있을 수 있습니다.
또한 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.
There was a problem hiding this comment.
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.replacevsrouter.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); |
There was a problem hiding this comment.
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.
| 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.
| .catch((err) => { | ||
| console.error(err); | ||
| setAlertMessage( | ||
| err instanceof Error | ||
| ? err.message | ||
| : '환불 정보를 불러오지 못했습니다.', | ||
| ); | ||
| setStep('reason'); |
There was a problem hiding this comment.
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)).
| import { notFound } from 'next/navigation'; | ||
| import { CloseXButton } from '@/components/ui/close-button'; | ||
| import { CancelForm } from './_components/cancel-form'; | ||
| import { auth } from '/auth'; |
There was a problem hiding this comment.
임포트 경로 오류 — /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.
| 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).
| <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> |
There was a problem hiding this comment.
absolute right-5 — relative 부모 없이 위치 지정
header에 relative가 빠져 있어 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.
| <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".
📝 개요
🚀 주요 변경 사항
📸 스크린샷 (선택)
✅ 체크리스트
이슈 해결 여부
PR 본문에 Closes #이슈번호 이라고 적으면, PR이 머지될 때 해당 이슈가 자동으로 닫힙니다