Conversation
Walkthrough결제 흐름 전체를 새로 구현했습니다. 사용자 인증, 상품 상세 조회, 배송 정보 입력, 결제 정보 처리를 포함한 서버 액션과 클라이언트 컴포넌트를 추가하고, 주문 완료 페이지를 제공합니다. Changes
Sequence DiagramsequenceDiagram
participant User
participant PaymentPage
participant PaymentProvider
participant ShippingForm
participant PaymentForm
participant OrderAction
participant API
participant CompletePage
User->>PaymentPage: 배송정보/결제정보 입력
PaymentPage->>PaymentProvider: 컨텍스트 초기화
User->>ShippingForm: 배송정보 입력
ShippingForm->>ShippingForm: Daum 우편번호 API
ShippingForm->>PaymentProvider: 배송정보 업데이트
User->>PaymentForm: 포인트 입력
PaymentForm->>PaymentProvider: 포인트 업데이트
PaymentProvider->>PaymentProvider: 최종 가격 계산
User->>PaymentProvider: 결제 버튼 클릭
PaymentProvider->>PaymentProvider: 배송정보 검증
PaymentProvider->>OrderAction: 주문 생성 요청 (cart/direct)
OrderAction->>API: POST /orders
API-->>OrderAction: orderId 반환
OrderAction-->>PaymentProvider: 주문 완료
PaymentProvider->>CompletePage: /payment/complete?orderId=... 리다이렉트
CompletePage->>OrderAction: getOrderDetail(orderId)
OrderAction->>API: GET /orders/{orderId}
API-->>CompletePage: 주문 상세 정보
CompletePage-->>User: 주문 완료 화면 표시
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ✏️ Tip: You can disable in-progress messages and the fortune message in your review settings. Tip CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.Add a .trivyignore file to your project to customize which findings Trivy reports. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
2 similar comments
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ✏️ Tip: You can disable in-progress messages and the fortune message in your review settings. Tip CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.Add a .trivyignore file to your project to customize which findings Trivy reports. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ✏️ Tip: You can disable in-progress messages and the fortune message in your review settings. Tip CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.Add a .trivyignore file to your project to customize which findings Trivy reports. ✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
|
Caution Review failedFailed to post review comments Walkthrough결제 플로우 엔드-투-엔드 구현 추가. 사용자 인증, 주문 항목 로드(카트/직구), PaymentContext 기반 상태 관리, 스크롤 스파이 네비게이션, 배송/결제 정보 입력, 주문 생성 및 완료 페이지로 구성. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant PaymentPage as PaymentPage
participant PaymentProvider as PaymentProvider<br/>(Context)
participant OrderAPI as 서버 액션<br/>(order/product/user)
participant ShippingSection as ShippingSection
participant PaymentSection as PaymentSection
participant OrderAPI2 as 주문 생성 API<br/>(createOrder)
participant CompletePage as CompletePage
User->>PaymentPage: /payment 접속
PaymentPage->>PaymentPage: 사용자 인증 확인
rect rgba(100, 150, 200, 0.5)
Note over PaymentPage: 초기 데이터 로드
par 병렬 로드
PaymentPage->>OrderAPI: getUserInfo()
PaymentPage->>OrderAPI: fetchCartOrderItems() 또는<br/>fetchDirectOrderItems()
end
end
PaymentPage->>PaymentProvider: items, user 전달
PaymentProvider->>PaymentProvider: totalPrice 계산<br/>shippingInfo 초기화
User->>ShippingSection: 배송 정보 입력
ShippingSection->>PaymentProvider: onChange(shippingInfo)
PaymentProvider->>PaymentProvider: shippingInfo 상태 갱신
User->>PaymentSection: 포인트 사용 입력
PaymentSection->>PaymentProvider: onPointsChange(usedPoints)
PaymentProvider->>PaymentProvider: finalPrice 재계산
rect rgba(100, 150, 200, 0.5)
Note over User,PaymentProvider: 스크롤 네비게이션
User->>ShippingSection: 섹션 스크롤
ShippingSection->>PaymentProvider: usePaymentScrollSpy<br/>activeStep 갱신
end
User->>PaymentProvider: 결제 버튼 클릭
rect rgba(150, 100, 200, 0.5)
Note over PaymentProvider: 결제 검증 및 주문 생성
PaymentProvider->>PaymentProvider: shippingInfo 필수 검증
PaymentProvider->>OrderAPI2: createOrderFromCart() 또는<br/>createOrderFromProduct()
end
OrderAPI2->>OrderAPI2: 주문 데이터 생성
OrderAPI2-->>PaymentProvider: orderId 반환
PaymentProvider->>CompletePage: /payment/complete?orderId={id}<br/>네비게이션
CompletePage->>OrderAPI: getOrderDetail(orderId)
CompletePage-->>User: 완료 화면 렌더링
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Reasoning: 13개 신규 파일 추가로 광범위한 결제 플로우 구현. PaymentContext의 복잡한 상태 관리(가격 계산, 검증, 부작용), usePaymentScrollSpy의 이벤트 핸들링 및 타이밍 로직, Daum Postcode 통합, 다중 서버 액션의 에러 처리 및 데이터 변환 등 여러 도메인에 걸친 이질적 변경. 포인트 정규화, 배열 검증, 스크롤 동작 정확성 확인 필수. A11y 고려사항: ShippingInfoSection의 Daum Postcode 팝업이 포커스/ARIA 레이블 없음. StepNavigator의 버튼 의미 전달 강화 필요. PaymentInfoSection의 "모두 사용" 버튼이 접근 가능한가 확인. Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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: 17
🤖 Fix all issues with AI agents
In `@src/app/actions/order.ts`:
- Around line 96-101: params.selections can be a string[] but the code
force-casts it with as string before JSON.parse, causing Array.toString() and
invalid JSON; fix by normalizing the raw value before parsing: read
params.selections (reference the selections variable and params.selections), if
Array.isArray(params.selections) use the first element (or join/choose an agreed
behavior) as the JSON string, otherwise use it directly or fallback to '[]',
then JSON.parse that normalized string inside the try/catch and call notFound()
only on parse failure.
- Around line 74-86: The mapping that builds PaymentDisplayItem sets
originalPrice and price to the same value because CartResponse lacks
originalPrice, breaking discount calculations; update the data flow so
originalPrice contains the product's list/was price (either by adding
originalPrice to the CartResponse from the API or by fetching the product's
MSRP/list price from the product/catalog service using productId when building
the mapped objects in the selected.map block), then assign originalPrice to that
sourced value (leave price as the current sale price) so PaymentDisplayItem and
OrderItemsSection can compute discounts correctly.
In `@src/app/payment/_components/order-items.tsx`:
- Line 67: The Tailwind class on the paragraph in the OrderItems component is
invalid ("text-red"); update the className on the <p> element in
src/app/payment/_components/order-items.tsx (the paragraph with
className="text-red text-center text-xl leading-normal font-medium") to use a
valid shade like text-red-500 (or another appropriate red-<shade> such as
text-red-600) so it conforms to Tailwind vX utility naming; keep the rest of the
classes unchanged.
In `@src/app/payment/_components/payment-context.tsx`:
- Around line 76-77: Compute finalPrice defensively in the payment context so it
never becomes negative: when computing finalPrice from totalItemPrice (derived
from items.reduce(...)) and usedPoints, clamp the result to a minimum of 0
(e.g., finalPrice = Math.max(totalItemPrice - usedPoints, 0)). Also validate
usedPoints is non-negative before the subtraction (coerce negatives to 0) so the
context-level value for finalPrice is always >= 0 for consumers like
PaymentInfoSection.
- Around line 96-101: orderId creation uses items.map(item => item.cartItemId!)
which force-asserts an optional cartItemId and can send undefined to
createOrderFromCart; update the block around orderType === 'cart' (in
payment-context.tsx) to first filter items for those with a defined cartItemId
(e.g., items.filter(i => i.cartItemId != null)) and then map to the numeric ids,
or alternatively validate and throw/log if any cartItemId is missing before
calling createOrderFromCart (reference: orderType, items, cartItemId,
createOrderFromCart).
- Around line 83-90: handlePayment currently only checks
shippingInfo.deliveryAddress and shippingInfo.postalCode; add validation for
shippingInfo.recipient and shippingInfo.recipientPhone so orders cannot be
created with empty recipient or contact. Inside handlePayment (alongside
existing deliveryAddress/postalCode checks) verify recipient and recipientPhone
are non-empty (and optionally basic phone format), show an alert with a clear
message and call scrollToId(SECTIONS.SHIPPING) before returning if validation
fails; use the same isSubmitting guard and error flow so behavior remains
consistent.
In `@src/app/payment/_components/payment-info.tsx`:
- Around line 33-38: The Input component (Input with props value={usedPoints}
and onChange={handlePointChange}) is missing an accessible label and the
placeholder never shows when usedPoints is 0; add an accessible label by either
supplying aria-label="적립금 사용 금액" (or connecting a <label> to the input) and
update the value handling so that when usedPoints is 0 you pass an empty string
("" ) to the Input or implement conditional display logic (e.g., display "" for
value prop and show placeholder "0원" visually) so screen readers and visual
users both get correct information.
In `@src/app/payment/_components/step-navigator.tsx`:
- Line 89: The spacer div in StepNavigator currently uses a hardcoded className
"h-52" (208px) which mismatches HEADER_HEIGHT (220px) defined in
use-payment-scroll-spy.ts causing ~12px of the first section to be hidden; fix
by replacing the hardcoded spacer with the shared HEADER_HEIGHT value (import
the constant from use-payment-scroll-spy or a central constants file) and apply
it via a style or computed class so the spacer height exactly equals
HEADER_HEIGHT; update StepNavigator (the div with className "h-52") to use the
shared HEADER_HEIGHT constant.
- Line 53: The Tailwind class "bg-" in the JSX className is invalid; locate the
className on the element in src/app/payment/_components/step-navigator.tsx (the
JSX element inside the StepNavigator component) and either remove "bg-" or
replace it with the intended full Tailwind background class (e.g., "bg-white",
"bg-gray-100", etc.) so the className becomes valid (e.g., "relative z-10 flex
flex-col items-center" or include the correct bg-* value).
In `@src/app/payment/_components/use-payment-scroll-spy.ts`:
- Around line 16-33: The scrollToId function sets isClickScrolling.current =
true unconditionally but only clears it inside the element-found branch, so when
document.getElementById(id) returns null the flag stays true; update scrollToId
(and related timeoutRef handling) to either set isClickScrolling.current = true
only after confirming element exists or ensure you reset
isClickScrolling.current = false and clear any timeoutRef when element is not
found, and keep the startTransition/setActiveId behavior unchanged; reference
scrollToId, isClickScrolling.current, timeoutRef, and HEADER_HEIGHT when making
this change.
In `@src/app/payment/complete/page.tsx`:
- Around line 39-52: The button grid div currently uses className "mt-5 grid
max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white" and is left-aligned;
add "mx-auto" to that div's className (the div wrapping the two Link elements in
the payment completion page component) so the max-w-sm grid is horizontally
centered.
- Around line 87-92: The rendering uses order.totalAmount.toLocaleString() which
will throw if totalAmount is null/undefined; update the component in page.tsx to
guard against missing values by using a safe fallback (e.g., default to 0 or
display a placeholder) before calling toLocaleString or check with a nullish
coalescing expression; ensure you reference order.totalAmount and the UI span
that displays the amount so the value is sanitized/validated (or formatted only
when typeof number) to avoid runtime TypeError.
- Around line 15-22: OrderCompletePage is missing an authentication/session
check, allowing anyone with an orderId to view PII; add a session/auth check
(e.g., call auth() or your session helper) at the top of OrderCompletePage
before calling getOrderDetail and call notFound() or redirect if no session,
then pass the authenticated user id to getOrderDetail so it can verify ownership
(or alternatively ensure getOrderDetail enforces that the order belongs to the
authenticated user and throws/notFound if not); reference OrderCompletePage,
auth(), getOrderDetail, and notFound() when implementing this change.
- Around line 60-85: The Image usage in payment/complete/page.tsx relies on
external image URLs (item.imageUrl) but next.config.ts still lists a placeholder
remotePatterns hostname ('example.com'); update next.config.ts remotePatterns to
include the actual image host(s) used by your order images (or the API/image CDN
domain) so Next.js Image can load item.imageUrl at runtime; locate the Image
component in payment/complete/page.tsx and the remotePatterns array in
next.config.ts and add the real hostname(s)/protocol(s) (or wildcard pattern if
appropriate) to resolve the runtime failures.
In `@src/app/payment/page.tsx`:
- Around line 49-51: The class "min-h-auto" on the section with id
SECTIONS.ITEMS (wrapping <OrderItemsSection items={items} />) is not a valid
Tailwind utility and has no effect; remove "min-h-auto" from that className (and
similarly from the other occurrences noted around lines 53–56) or replace it
with a valid Tailwind min-h utility (e.g., "min-h-0" or "min-h-screen") if a
specific minimum height is required—update the className on the Section elements
that reference SECTIONS.ITEMS and the other affected sections accordingly.
- Line 40: The main element in the payment page uses className "max-h-screen"
which can clip content; update the main element in src/app/payment/page.tsx to
remove "max-h-screen" or replace it with "min-h-screen" (or another suitable
height utility) so the page can grow beyond the viewport and avoid cutting off
children like sections, pb-32, and min-h-[60vh].
- Around line 27-37: The payment page lacks an app-level error boundary for
failures in getUserInfo, fetchCartOrderItems, or fetchDirectOrderItems called by
PaymentPage; create src/app/payment/error.tsx exporting an Error component that
renders a user-friendly, business-focused error UI (with optional retry/home
actions and any safe error details) so thrown exceptions from those functions
show your custom message instead of the default Next.js error UI; ensure the
file name is exactly error.tsx and that it returns a React component to be
picked up by Next.js routing for the /payment route.
🧹 Nitpick comments (16)
src/components/ui/input.tsx (1)
5-21: 코딩 가이드라인:export default function패턴 미준수.shadcn/ui에서 생성된 컴포넌트로 보이지만,
src/components/**/*.tsx경로의 가이드라인에 따르면export default function패턴을 사용해야 합니다. shadcn 컴포넌트는 예외로 둘지 팀 내 합의가 필요합니다.가이드라인 준수 시 변경 예시
-function Input({ className, type, ...props }: React.ComponentProps<"input">) { +export default function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( <input ... /> ) } - -export { Input }As per coding guidelines,
src/components/**/*.tsx: "export default function 패턴 (화살표 함수 금지)".src/app/actions/user.ts (1)
10-12: API 응답과 DTO 타입 불일치 — 런타임 타입 가드로 우회 중.
UserInfoResDto.points는number로 선언되어 있지만, 실제 API 응답이string일 수 있어 런타임에서 변환하고 있습니다. 백엔드 응답 타입이 안정화되면 이 방어 코드를 제거하거나, 별도의 raw response 타입을 정의하여 변환 레이어를 명시적으로 분리하는 것이 좋습니다.src/components/product-option-sheet/use-product-option.ts (2)
163-178: URL 쿼리 파라미터로 JSON 전달 — 몇 가지 우려 사항.
optionId누락:selections에color/size/quantity만 포함되고optionId가 빠져 있습니다. 결제 페이지에서 다시 옵션을 조회하여 매칭해야 하므로 불필요한 복잡성이 추가됩니다.- URL 길이 제한: JSON을 URL-encode하여 전달하는 방식은 선택 항목이 많아지면 URL 길이 제한에 근접할 수 있습니다. 현재 사용 패턴에선 문제없을 수 있으나, 추후 세션 스토리지나 서버 사이드 상태 관리로 전환을 고려해 볼 수 있습니다.
optionId 포함 제안
const selections = selectedItems.map((item) => ({ + optionId: item.optionId, color: item.color, size: item.size, quantity: item.quantity, }));
84-95:useEffect의존성 배열에options참조 불안정.
initialOptions가 빈 배열일 때generateDummyOptions()가 매 렌더마다 새 배열을 생성하여options참조가 변경됩니다. 이로 인해useEffect가 불필요하게 재실행될 수 있습니다.useMemo로options를 메모이제이션하는 것을 권장합니다.수정 제안
+ import { useState, useEffect, useTransition, useMemo } from 'react'; ... - const options = - initialOptions.length > 0 ? initialOptions : generateDummyOptions(); + const options = useMemo( + () => (initialOptions.length > 0 ? initialOptions : generateDummyOptions()), + [initialOptions], + );src/app/payment/_components/payment-info.tsx (1)
39-47: 접근성(a11y): 버튼 내부 구조 개선 필요.
<button>안에<div>+<span>구조는 시맨틱하게 적절하지 않습니다.<span>조합으로 "모두 사용"이 시각적으로 분리되어 있지만, 스크린 리더는 이를 하나의 텍스트로 읽을 수 있도록aria-label="적립금 모두 사용"을 추가하는 것을 권장합니다.src/app/payment/_components/use-payment-scroll-spy.ts (1)
36-62:ids배열 참조 안정성 문제 — 매 렌더마다 effect가 재등록될 수 있음
useEffect의존성에ids배열이 직접 전달되고 있는데, 호출 측에서 매 렌더마다 새 배열 참조를 생성하면 effect가 불필요하게 재실행됩니다. 현재payment-context.tsxLine 17에서const SECTION_IDS = Object.values(SECTIONS)를 모듈 스코프에서 생성하므로 참조가 안정적이지만, 향후 인라인 호출 시 문제가 될 수 있습니다.♻️ 방어적으로 내부에서 직렬화 비교 또는 useMemo 적용
+'use client'; + +import { useState, useEffect, useRef, useTransition, useMemo } from 'react'; + const HEADER_HEIGHT = 220; export default function usePaymentScrollSpy(ids: string[]) { + // 배열 내용이 같으면 참조를 유지 + const stableIds = useMemo(() => ids, [JSON.stringify(ids)]); + - const [activeId, setActiveId] = useState<string>(ids[0] || ''); + const [activeId, setActiveId] = useState<string>(stableIds[0] || ''); ... useEffect(() => { - if (ids.length === 0) return; + if (stableIds.length === 0) return; ... - }, [ids]); + }, [stableIds]);src/app/payment/_components/step-navigator.tsx (1)
46-82: 접근성: 활성 스텝에aria-current속성 부재스크린 리더 사용자가 현재 활성 단계를 인식할 수 없습니다.
button에aria-current="step"속성을 추가하세요.<button type="button" onClick={() => onStepChange(step.id)} + aria-current={isActive ? 'step' : undefined} className="group relative flex flex-col items-center justify-center" >src/app/payment/_components/shipping-info.tsx (3)
1-3:'use client'지시문 누락이 파일은
useDaumPostcodePopup훅을 사용하므로 클라이언트 컴포넌트입니다. 현재는payment-context.tsx(클라이언트)에서 import하므로 동작하지만, 다른 서버 컴포넌트에서 직접 import할 경우 런타임 에러가 발생합니다. 명시적으로'use client'를 추가하세요.+'use client'; + import { useDaumPostcodePopup } from 'react-daum-postcode';
80-85: 전화번호 입력에type="tel"누락모바일에서 숫자 키패드가 표시되도록
type="tel"을 추가하면 UX가 개선됩니다.<Input id="phone" + type="tel" value={value.recipientPhone} onChange={(e) => handleChange('recipientPhone', e.target.value)} placeholder="010-0000-0000" />
91-96: 우편번호 입력에 접근성 라벨 부재
postalCode,deliveryAddress,detailAddress입력 필드에Label또는aria-label이 없어 스크린 리더 사용자가 필드 목적을 인식할 수 없습니다.placeholder만으로는 접근성 요구사항을 충족하지 못합니다.src/app/payment/_components/order-items.tsx (2)
79-86: 할인 금액 표시에 부호 없음할인 금액이 양수로 표시되어 사용자가 추가 금액으로 오해할 수 있습니다. 일반적인 결제 UI에서는
-접두사를 붙입니다.<span className="text-2xl leading-[18px]"> - {discountTotal.toLocaleString()}원 + -{discountTotal.toLocaleString()}원 </span>
39-64: 이미지에sizesprop 누락 — 불필요한 대용량 이미지 로드 가능Next.js
Image에width/height만 지정하고sizes를 생략하면 기본적으로 뷰포트 전체 너비 기준으로 srcset이 선택됩니다. 110px 고정 표시이므로sizes="110px"를 추가하면 네트워크 최적화에 도움됩니다.<Image src={item.thumbnailImageUrl} alt={item.productName} width={110} height={110} + sizes="110px" />src/app/payment/_components/payment-context.tsx (1)
19-31:handlePayment타입이() => void로 선언되어 있지만 실제로는async현재
onClick에서만 호출되므로 즉시 문제는 없지만, 인터페이스가 실제 동작과 불일치합니다.- handlePayment: () => void; + handlePayment: () => Promise<void>;src/types/domain/order.ts (1)
42-69:OrderFromCartRequest와OrderFromProductRequest의 배송 필드 중복두 인터페이스에 동일한 배송 관련 필드(recipient, recipientPhone, deliveryAddress, detailAddress, postalCode, deliveryMessage)가 반복됩니다. 공통 인터페이스를 추출하면 유지보수성이 향상됩니다.
♻️ 공통 배송 정보 인터페이스 추출
+export interface ShippingRequest { + recipient: string; + recipientPhone: string; + deliveryAddress: string; + detailAddress: string; + postalCode: string; + deliveryMessage: string; +} + -export interface OrderFromCartRequest { - cartItemIds: number[]; - usedPoints: number; - recipient: string; - recipientPhone: string; - deliveryAddress: string; - detailAddress: string; - postalCode: string; - deliveryMessage: string; -} +export interface OrderFromCartRequest extends ShippingRequest { + cartItemIds: number[]; + usedPoints: number; +} -export interface OrderFromProductRequest { - items: OrderItemRequest[]; - usedPoints: number; - recipient: string; - recipientPhone: string; - deliveryAddress: string; - detailAddress: string; - postalCode: string; - deliveryMessage: string; -} +export interface OrderFromProductRequest extends ShippingRequest { + items: OrderItemRequest[]; + usedPoints: number; +}src/app/actions/order.ts (1)
57-67: 전체 장바구니를 조회 후 필터링 — 비효율적이지만 현재 허용 가능
getCartItems()가 모든 장바구니 아이템을 반환한 후 클라이언트에서filter로 선택된 아이템만 추출합니다. 아이템이 많아지면 불필요한 데이터 전송이 발생합니다. 향후 백엔드에 ID 기반 조회 API가 추가되면 전환을 고려하세요.src/app/payment/complete/page.tsx (1)
34-36: 빈className=""제거 권장Line 34, 35, 36에 빈
className=""이 있습니다. 스타일이 적용되지 않는다면 제거하여 코드 노이즈를 줄이세요.
| return selected.map((c) => ({ | ||
| productId: c.productId, | ||
| productName: c.productName, | ||
| brandName: c.brandName, | ||
| thumbnailImageUrl: c.thumbnailImageUrl, | ||
| selectedSize: c.selectedSize, | ||
| selectedColor: c.selectedColor, | ||
| quantity: c.quantity, | ||
| originalPrice: c.price, | ||
| price: c.price, | ||
| totalPrice: c.totalPrice, | ||
| cartItemId: c.cartId, | ||
| })); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "interface CartResponse|type CartResponse" -A 20 --type=tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 2229
🏁 Script executed:
# Also check the import statements and CartResponse usage in the file
cat -n src/app/actions/order.ts | head -30Repository: IT-Cotato/12th-OnGil-FE
Length of output: 1128
🏁 Script executed:
# Find where CartResponse is defined
fd -e ts -e tsx | xargs rg -l "CartResponse" | head -10Repository: IT-Cotato/12th-OnGil-FE
Length of output: 248
🏁 Script executed:
# Check the full function around lines 74-86
sed -n '50,95p' src/app/actions/order.tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 1352
🏁 Script executed:
# Look at the OrderFromCartRequest type definition
rg -n "interface OrderFromCartRequest|type OrderFromCartRequest" -A 30 --type=tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 1627
🏁 Script executed:
# Check the PaymentDisplayItem type to understand what's expected
rg -n "type PaymentDisplayItem|interface PaymentDisplayItem" -A 20 --type=tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 1568
CartResponse에 originalPrice 필드가 없어 할인 기능 동작 불가
CartResponse는 originalPrice 필드를 제공하지 않으므로, 82-83번 줄에서 originalPrice: c.price, price: c.price로 동일한 값을 매핑할 수밖에 없습니다. 이로 인해 PaymentDisplayItem이 기대하는 정가와 판매가의 구분이 불가능하고, OrderItemsSection의 할인 계산(originalTotal - finalTotal)이 항상 0이 되어 할인 표시가 정상 작동하지 않습니다.
API에서 CartResponse에 originalPrice 필드를 추가하거나, 별도의 정가 데이터 소스를 확보해야 합니다.
🤖 Prompt for AI Agents
In `@src/app/actions/order.ts` around lines 74 - 86, The mapping that builds
PaymentDisplayItem sets originalPrice and price to the same value because
CartResponse lacks originalPrice, breaking discount calculations; update the
data flow so originalPrice contains the product's list/was price (either by
adding originalPrice to the CartResponse from the API or by fetching the
product's MSRP/list price from the product/catalog service using productId when
building the mapped objects in the selected.map block), then assign
originalPrice to that sourced value (leave price as the current sale price) so
PaymentDisplayItem and OrderItemsSection can compute discounts correctly.
| let selections: { color: string; size: string; quantity: number }[]; | ||
| try { | ||
| selections = JSON.parse((params.selections as string) || '[]'); | ||
| } catch { | ||
| notFound(); | ||
| } |
There was a problem hiding this comment.
params.selections가 string[]일 때 as string 캐스트 위험
params 타입이 Record<string, string | string[] | undefined>이므로 selections가 string[]일 수 있습니다. 이 경우 as string 캐스트는 Array.toString()이 호출되어 유효하지 않은 JSON이 됩니다. catch에서 notFound()로 처리되지만, 의도하지 않은 실패 경로입니다.
- selections = JSON.parse((params.selections as string) || '[]');
+ const raw = Array.isArray(params.selections)
+ ? params.selections[0]
+ : params.selections;
+ selections = JSON.parse(raw || '[]');📝 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.
| let selections: { color: string; size: string; quantity: number }[]; | |
| try { | |
| selections = JSON.parse((params.selections as string) || '[]'); | |
| } catch { | |
| notFound(); | |
| } | |
| let selections: { color: string; size: string; quantity: number }[]; | |
| try { | |
| const raw = Array.isArray(params.selections) | |
| ? params.selections[0] | |
| : params.selections; | |
| selections = JSON.parse(raw || '[]'); | |
| } catch { | |
| notFound(); | |
| } |
🤖 Prompt for AI Agents
In `@src/app/actions/order.ts` around lines 96 - 101, params.selections can be a
string[] but the code force-casts it with as string before JSON.parse, causing
Array.toString() and invalid JSON; fix by normalizing the raw value before
parsing: read params.selections (reference the selections variable and
params.selections), if Array.isArray(params.selections) use the first element
(or join/choose an agreed behavior) as the JSON string, otherwise use it
directly or fallback to '[]', then JSON.parse that normalized string inside the
try/catch and call notFound() only on parse failure.
| ))} | ||
| </ul> | ||
| {hasDiscount && ( | ||
| <p className="text-red text-center text-xl leading-normal font-medium"> |
There was a problem hiding this comment.
유효하지 않은 Tailwind 클래스 text-red
text-red는 Tailwind CSS v4에서 유효한 유틸리티가 아닙니다. 의도한 색상 쉐이드를 명시해야 합니다.
- <p className="text-red text-center text-xl leading-normal font-medium">
+ <p className="text-red-500 text-center text-xl leading-normal font-medium">📝 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.
| <p className="text-red text-center text-xl leading-normal font-medium"> | |
| <p className="text-red-500 text-center text-xl leading-normal font-medium"> |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/order-items.tsx` at line 67, The Tailwind class
on the paragraph in the OrderItems component is invalid ("text-red"); update the
className on the <p> element in src/app/payment/_components/order-items.tsx (the
paragraph with className="text-red text-center text-xl leading-normal
font-medium") to use a valid shade like text-red-500 (or another appropriate
red-<shade> such as text-red-600) so it conforms to Tailwind vX utility naming;
keep the rest of the classes unchanged.
| const totalItemPrice = items.reduce((acc, item) => acc + item.totalPrice, 0); | ||
| const finalPrice = totalItemPrice - usedPoints; |
There was a problem hiding this comment.
finalPrice가 음수가 될 수 있음
usedPoints가 totalItemPrice보다 크면 finalPrice가 음수가 됩니다. PaymentInfoSection에서 입력 제한을 하더라도, context 레벨에서 방어하는 것이 안전합니다.
- const finalPrice = totalItemPrice - usedPoints;
+ const finalPrice = Math.max(0, totalItemPrice - usedPoints);📝 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 totalItemPrice = items.reduce((acc, item) => acc + item.totalPrice, 0); | |
| const finalPrice = totalItemPrice - usedPoints; | |
| const totalItemPrice = items.reduce((acc, item) => acc + item.totalPrice, 0); | |
| const finalPrice = Math.max(0, totalItemPrice - usedPoints); |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/payment-context.tsx` around lines 76 - 77,
Compute finalPrice defensively in the payment context so it never becomes
negative: when computing finalPrice from totalItemPrice (derived from
items.reduce(...)) and usedPoints, clamp the result to a minimum of 0 (e.g.,
finalPrice = Math.max(totalItemPrice - usedPoints, 0)). Also validate usedPoints
is non-negative before the subtraction (coerce negatives to 0) so the
context-level value for finalPrice is always >= 0 for consumers like
PaymentInfoSection.
| const handlePayment = async () => { | ||
| if (isSubmitting) return; | ||
|
|
||
| if (!shippingInfo.deliveryAddress || !shippingInfo.postalCode) { | ||
| alert('배송지 정보를 모두 입력해주세요.'); | ||
| scrollToId(SECTIONS.SHIPPING); | ||
| return; | ||
| } |
There was a problem hiding this comment.
배송지 유효성 검증 불완전 — 수령인, 연락처 미검증
deliveryAddress와 postalCode만 확인하고 recipient와 recipientPhone은 검증하지 않습니다. 빈 수령인/연락처로 주문이 생성될 수 있습니다.
- if (!shippingInfo.deliveryAddress || !shippingInfo.postalCode) {
+ if (
+ !shippingInfo.recipient.trim() ||
+ !shippingInfo.recipientPhone.trim() ||
+ !shippingInfo.deliveryAddress ||
+ !shippingInfo.postalCode
+ ) {
alert('배송지 정보를 모두 입력해주세요.');📝 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 handlePayment = async () => { | |
| if (isSubmitting) return; | |
| if (!shippingInfo.deliveryAddress || !shippingInfo.postalCode) { | |
| alert('배송지 정보를 모두 입력해주세요.'); | |
| scrollToId(SECTIONS.SHIPPING); | |
| return; | |
| } | |
| const handlePayment = async () => { | |
| if (isSubmitting) return; | |
| if ( | |
| !shippingInfo.recipient.trim() || | |
| !shippingInfo.recipientPhone.trim() || | |
| !shippingInfo.deliveryAddress || | |
| !shippingInfo.postalCode | |
| ) { | |
| alert('배송지 정보를 모두 입력해주세요.'); | |
| scrollToId(SECTIONS.SHIPPING); | |
| return; | |
| } |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/payment-context.tsx` around lines 83 - 90,
handlePayment currently only checks shippingInfo.deliveryAddress and
shippingInfo.postalCode; add validation for shippingInfo.recipient and
shippingInfo.recipientPhone so orders cannot be created with empty recipient or
contact. Inside handlePayment (alongside existing deliveryAddress/postalCode
checks) verify recipient and recipientPhone are non-empty (and optionally basic
phone format), show an alert with a clear message and call
scrollToId(SECTIONS.SHIPPING) before returning if validation fails; use the same
isSubmitting guard and error flow so behavior remains consistent.
| <div className="mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white"> | ||
| <Link | ||
| href={`/orders/${order.id}`} | ||
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | ||
| > | ||
| <span>주문 상세보기</span> | ||
| </Link> | ||
| <Link | ||
| href="/" | ||
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | ||
| > | ||
| <span>홈화면으로</span> | ||
| </Link> | ||
| </div> |
There was a problem hiding this comment.
버튼 그리드가 중앙 정렬되지 않음
부모 div는 text-center이지만 버튼 그리드(max-w-sm)에 mx-auto가 없어 좌측 정렬됩니다.
제안
- <div className="mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white">
+ <div className="mx-auto mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white">📝 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.
| <div className="mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white"> | |
| <Link | |
| href={`/orders/${order.id}`} | |
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | |
| > | |
| <span>주문 상세보기</span> | |
| </Link> | |
| <Link | |
| href="/" | |
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | |
| > | |
| <span>홈화면으로</span> | |
| </Link> | |
| </div> | |
| <div className="mx-auto mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl font-medium text-white"> | |
| <Link | |
| href={`/orders/${order.id}`} | |
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | |
| > | |
| <span>주문 상세보기</span> | |
| </Link> | |
| <Link | |
| href="/" | |
| className="bg-ongil-teal flex items-center justify-center rounded-md px-6 py-2" | |
| > | |
| <span>홈화면으로</span> | |
| </Link> | |
| </div> |
🤖 Prompt for AI Agents
In `@src/app/payment/complete/page.tsx` around lines 39 - 52, The button grid div
currently uses className "mt-5 grid max-w-sm grid-cols-2 gap-3 text-xl
font-medium text-white" and is left-aligned; add "mx-auto" to that div's
className (the div wrapping the two Link elements in the payment completion page
component) so the max-w-sm grid is horizontally centered.
| {order.orderItems.map((item, idx) => ( | ||
| <li key={idx} className="flex gap-4"> | ||
| <div className="flex h-full w-full items-center justify-center gap-4 rounded-2xl border border-black p-5"> | ||
| {item.imageUrl ? ( | ||
| <Image | ||
| src={item.imageUrl} | ||
| alt={item.productName} | ||
| width={110} | ||
| height={110} | ||
| /> | ||
| ) : ( | ||
| <div className="flex h-[110px] w-[110px] items-center justify-center text-xl"> | ||
| No Image | ||
| </div> | ||
| )} | ||
| <div className="flex flex-col justify-center gap-7 text-xl leading-[18px] font-medium"> | ||
| <span>{item.brandName}</span> | ||
| <p>{item.productName}</p> | ||
| <span> | ||
| {item.selectedColor} / {item.selectedSize} | ||
| </span> | ||
| <span>{item.quantity}개</span> | ||
| </div> | ||
| </div> | ||
| </li> | ||
| ))} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for next.config file and its content
find . -name "next.config.*" -type f | head -20Repository: IT-Cotato/12th-OnGil-FE
Length of output: 83
🏁 Script executed:
#!/bin/bash
# Check the complete next.config content
if [ -f "next.config.js" ]; then
cat -n next.config.js
elif [ -f "next.config.ts" ]; then
cat -n next.config.ts
elif [ -f "next.config.mjs" ]; then
cat -n next.config.mjs
fiRepository: IT-Cotato/12th-OnGil-FE
Length of output: 2521
🏁 Script executed:
#!/bin/bash
# Verify the page file to understand Image component usage
head -100 src/app/payment/complete/page.tsx | cat -nRepository: IT-Cotato/12th-OnGil-FE
Length of output: 4221
item.imageUrl의 실제 도메인을 next.config.ts의 remotePatterns에 등록 필요
현재 remotePatterns에는 example.com이 TODO로 등록되어 있습니다(17행). 실제 주문 상품 이미지를 호스팅하는 도메인으로 교체하지 않으면, item.imageUrl이 등록되지 않은 도메인의 URL일 경우 Image 컴포넌트가 런타임에 실패합니다.
next.config.ts 참고
{
protocol: 'https',
hostname: 'example.com', // TODO: 실제 API 이미지 호스트로 교체 필요
},
🤖 Prompt for AI Agents
In `@src/app/payment/complete/page.tsx` around lines 60 - 85, The Image usage in
payment/complete/page.tsx relies on external image URLs (item.imageUrl) but
next.config.ts still lists a placeholder remotePatterns hostname
('example.com'); update next.config.ts remotePatterns to include the actual
image host(s) used by your order images (or the API/image CDN domain) so Next.js
Image can load item.imageUrl at runtime; locate the Image component in
payment/complete/page.tsx and the remotePatterns array in next.config.ts and add
the real hostname(s)/protocol(s) (or wildcard pattern if appropriate) to resolve
the runtime failures.
| <div className="mt-4 flex items-center justify-between pt-3 text-lg font-bold"> | ||
| <span>총 결제금액</span> | ||
| <span className="text-xl text-blue-600"> | ||
| {order.totalAmount.toLocaleString()}원 | ||
| </span> | ||
| </div> |
There was a problem hiding this comment.
totalAmount가 undefined/null일 경우 런타임 에러 가능
order.totalAmount.toLocaleString()은 totalAmount가 falsy할 때 TypeError를 발생시킵니다. API 응답이 항상 이 필드를 보장하는지 확인하거나, 방어 코드를 추가하세요.
제안
- {order.totalAmount.toLocaleString()}원
+ {(order.totalAmount ?? 0).toLocaleString()}원📝 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.
| <div className="mt-4 flex items-center justify-between pt-3 text-lg font-bold"> | |
| <span>총 결제금액</span> | |
| <span className="text-xl text-blue-600"> | |
| {order.totalAmount.toLocaleString()}원 | |
| </span> | |
| </div> | |
| <div className="mt-4 flex items-center justify-between pt-3 text-lg font-bold"> | |
| <span>총 결제금액</span> | |
| <span className="text-xl text-blue-600"> | |
| {(order.totalAmount ?? 0).toLocaleString()}원 | |
| </span> | |
| </div> |
🤖 Prompt for AI Agents
In `@src/app/payment/complete/page.tsx` around lines 87 - 92, The rendering uses
order.totalAmount.toLocaleString() which will throw if totalAmount is
null/undefined; update the component in page.tsx to guard against missing values
by using a safe fallback (e.g., default to 0 or display a placeholder) before
calling toLocaleString or check with a nullish coalescing expression; ensure you
reference order.totalAmount and the UI span that displays the amount so the
value is sanitized/validated (or formatted only when typeof number) to avoid
runtime TypeError.
| ]); | ||
|
|
||
| return ( | ||
| <main className="max-h-screen"> |
There was a problem hiding this comment.
max-h-screen이 결제 페이지 콘텐츠를 잘라낼 수 있습니다
main에 max-h-screen을 적용하면 뷰포트 높이를 초과하는 콘텐츠가 클리핑됩니다. 하위에 pb-32, 여러 섹션, min-h-[60vh] 등이 있어 콘텐츠가 뷰포트를 초과할 가능성이 높습니다. min-h-screen으로 변경하거나 제거하는 것을 권장합니다.
제안
- <main className="max-h-screen">
+ <main className="min-h-screen">📝 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.
| <main className="max-h-screen"> | |
| <main className="min-h-screen"> |
🤖 Prompt for AI Agents
In `@src/app/payment/page.tsx` at line 40, The main element in the payment page
uses className "max-h-screen" which can clip content; update the main element in
src/app/payment/page.tsx to remove "max-h-screen" or replace it with
"min-h-screen" (or another suitable height utility) so the page can grow beyond
the viewport and avoid cutting off children like sections, pb-32, and
min-h-[60vh].
| <section id={SECTIONS.ITEMS} className="min-h-auto scroll-mt-[120px]"> | ||
| <OrderItemsSection items={items} /> | ||
| </section> |
There was a problem hiding this comment.
min-h-auto는 유효한 Tailwind 유틸리티가 아닙니다
min-h-auto는 Tailwind CSS v4에서 표준 유틸리티 클래스가 아니므로 아무 효과가 없습니다. min-height: auto는 이미 기본값이므로 제거해도 무방합니다.
제안
- <section id={SECTIONS.ITEMS} className="min-h-auto scroll-mt-[120px]">
+ <section id={SECTIONS.ITEMS} className="scroll-mt-[120px]">- className="min-h-auto scroll-mt-[120px]"
+ className="scroll-mt-[120px]"Also applies to: 53-56
🤖 Prompt for AI Agents
In `@src/app/payment/page.tsx` around lines 49 - 51, The class "min-h-auto" on the
section with id SECTIONS.ITEMS (wrapping <OrderItemsSection items={items} />) is
not a valid Tailwind utility and has no effect; remove "min-h-auto" from that
className (and similarly from the other occurrences noted around lines 53–56) or
replace it with a valid Tailwind min-h utility (e.g., "min-h-0" or
"min-h-screen") if a specific minimum height is required—update the className on
the Section elements that reference SECTIONS.ITEMS and the other affected
sections accordingly.
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
3 similar comments
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Fix all issues with AI agents
In `@src/app/payment/_components/order-items.tsx`:
- Around line 22-30: finalTotal (items.reduce(... item.totalPrice)) can diverge
from originalTotal - discountTotal (which effectively sums item.price *
item.quantity) when totalPrice includes extra adjustments (points, coupons,
shipping, rounding). Pick one canonical source of truth and use it consistently:
either compute totals from originalPrice/price/quantity and discount logic
(update originalTotal/discountTotal to mirror all adjustments) or derive
discountTotal and originalTotal from item.totalPrice so finalTotal equals
originalTotal - discountTotal. Update the functions/variables originalTotal,
discountTotal, and finalTotal and the place that sets item.totalPrice/item.price
to ensure the same calculation path is used and document which field
(item.totalPrice vs item.price*item.quantity) is authoritative.
- Around line 79-85: The discount amount is rendered without a negative sign
making it look like an extra charge; update the rendering in the component that
uses hasDiscount and discountTotal (in order-items.tsx) to prefix the displayed
value with a minus sign, e.g., render "-" + discountTotal.toLocaleString() (or
use string interpolation) so the UI shows "-{amount}원" when hasDiscount is true;
ensure this change only affects the displayed text and not the raw discountTotal
value used in calculations.
In `@src/app/payment/_components/payment-context.tsx`:
- Around line 190-206: The PaymentButton component uses <button> without an
explicit type and lacks visible disabled styling; update the PaymentButton (and
the JSX returned by the component that uses usePayment, referencing finalPrice,
isSubmitting, handlePayment) to set type="button" to avoid implicit form
submission and add a visible/accessible disabled state by including the disabled
attribute bound to isSubmitting and applying conditional styling (e.g., lower
opacity, pointer-events-none or distinct background/text classes) when
isSubmitting is true; ensure any text indicating processing (isSubmitting ?
'처리중...' : '결제하기') remains and consider adding aria-disabled when appropriate
for screen readers.
- Around line 96-119: When building the order for orderType === 'cart', guard
against sending an empty cartItemIds array: after mapping/filtering items by
cartItemId (refer to items and cartItemId) check the resulting array length and
if it's zero, set an error state or throw/return early (and reset
setIsSubmitting(false)) instead of calling createOrderFromCart; ensure you still
pass usedPoints and shippingInfo only when cartItemIds is non-empty and update
any UI error messaging accordingly.
In `@src/app/payment/_components/payment-info.tsx`:
- Around line 41-49: The button in the payment-info component that calls
onPointsChange(Math.min(userPoints, totalPrice)) lacks an explicit type, so when
rendered inside a parent <form> it will act as type="submit" and may trigger
unintended form submission; update the JSX for that button element (the one
invoking onPointsChange with userPoints and totalPrice) to include type="button"
to prevent form submission.
In `@src/app/payment/_components/shipping-info.tsx`:
- Around line 91-96: The postalCode and deliveryAddress Input components lack
accessible labels; update the Input usages that render value={value.postalCode}
and value={value.deliveryAddress} (the Input elements in shipping-info.tsx) to
include either an explicit associated <label> (connect via matching id on Input)
or add aria-label attributes (e.g., aria-label="우편번호" and aria-label="배송지 주소");
ensure the id/aria-label strings are meaningful and unique so screen readers can
identify the fields and, if using label, set the Input id to match the label's
htmlFor.
- Around line 1-3: This file is missing the 'use client' directive required
because it calls the client hook useDaumPostcodePopup; add the string directive
'use client' as the very first line of
src/app/payment/_components/shipping-info.tsx (before any imports) so the module
is treated as a client component, and verify the component(s) that use
useDaumPostcodePopup (e.g., the ShippingInfo component or any function using
that hook) do not contain server-only APIs.
In `@src/app/payment/_components/step-navigator.tsx`:
- Around line 68-77: The span rendering step.label doesn't preserve newline
characters because it lacks the whitespace-pre-line utility; update the
className on the span in step-navigator.tsx (the element that renders
{step.label}) to include "whitespace-pre-line" alongside the existing classes so
any "\n" in step.label is rendered as a line break (ensure it is applied only
when multiline labels are expected).
In `@src/app/payment/complete/page.tsx`:
- Around line 113-116: The code calls order.deliveryAddress.replace(...) which
can throw if deliveryAddress is null/undefined/other falsy; change the
expression to guard and provide a safe default before calling replace (e.g., use
optional chaining or nullish coalescing like (order.deliveryAddress ??
'').replace(...) or order.deliveryAddress?.replace(...) ?? '' ) in the JSX where
order.deliveryAddress is used so replace is only invoked on a string and the UI
shows an empty/fallback value when missing.
- Line 6: The import path for auth is using a root-absolute string ("import {
auth } from '/auth'") which can break builds; change it to the project alias
import ("@/auth") wherever this pattern appears (replace the import in page.tsx
that references auth and any similar leading-slash imports), ensuring the symbol
name auth remains unchanged and tests/exports still resolve correctly.
In `@src/app/payment/page.tsx`:
- Around line 27-37: PaymentPage awaits server data (auth(), getUserInfo(),
fetchCartOrderItems(), fetchDirectOrderItems()) so add a route-level fallback
component by creating src/app/payment/loading.tsx that renders a simple loading
UI (spinner/placeholder) to display while the PaymentPage data resolves; this
ensures the route segment shows a fallback instead of a blank screen during
awaits and provides a granular loading state for the PaymentPage route.
🧹 Nitpick comments (13)
src/app/actions/user.ts (1)
10-12: API 응답 타입 불일치를 런타임에서 보정하는 방식은 취약합니다.
points가string으로 올 수 있다면, API 응답 DTO 타입(UserInfoResDto)과 실제 응답이 다르다는 의미입니다. 가능하다면 백엔드에number타입으로 통일을 요청하거나, 중간 raw DTO를 별도로 정의하여 변환하는 것이 타입 안전성 측면에서 더 견고합니다.src/components/ui/input.tsx (1)
5-21: 코딩 가이드라인:export default function패턴 위반
src/components/**/*.tsx경로에 해당하므로export default function패턴을 사용해야 합니다. 현재 named export (export { Input })를 사용하고 있습니다.단, shadcn/ui 자동 생성 컴포넌트라면 팀 내에서 예외 허용 여부를 확인해 주세요.
수정 제안
-function Input({ className, type, ...props }: React.ComponentProps<"input">) { +export default function Input({ className, type, ...props }: React.ComponentProps<"input">) { return ( <input ... /> ) } - -export { Input }As per coding guidelines,
src/components/**/*.tsx: "export default function 패턴 (화살표 함수 금지)".src/app/payment/_components/step-navigator.tsx (1)
55-57: 스텝<button>에 접근성 레이블이 없습니다.스크린 리더 사용자에게 각 버튼의 용도를 전달하기 위해
aria-label을 추가하는 것을 권장합니다.수정 제안
<button type="button" onClick={() => onStepChange(step.id)} + aria-label={step.label.replace('\n', ' ')} className="group relative flex flex-col items-center justify-center" >src/components/product-option-sheet/use-product-option.ts (1)
163-178: URL 쿼리 파라미터에 JSON 직렬화 — 현재는 동작하나 확장성 고려 필요
selections를JSON.stringify로 URL에 넣는 방식은 소수의 옵션에서는 문제없지만, 항목이 많아지면 URL 길이 제한(브라우저별 ~2,000–8,000자)에 도달할 수 있습니다. 향후 장바구니 일괄 결제 등으로 확장 시sessionStorage나 서버 사이드 임시 저장 방식을 검토해 보세요.
price를 URL에서 제외하고 서버에서 조회하는 설계는 클라이언트 측 가격 조작 방지에 좋습니다.src/app/payment/_components/use-payment-scroll-spy.ts (1)
5-5:HEADER_HEIGHT상수가step-navigator.tsx의 스페이서(h-[220px])와 중복 관리되고 있습니다.두 곳에서 같은 값을 사용하므로, 하나가 변경될 때 다른 쪽이 누락될 수 있습니다.
constants.ts에 공유 상수로 추출하면 동기화가 보장됩니다.수정 제안 (constants.ts)
+export const HEADER_HEIGHT = 220; + export const SECTIONS = { ITEMS: 'section-items', SHIPPING: 'section-shipping', PAYMENT: 'section-payment', } as const;그 후
use-payment-scroll-spy.ts와step-navigator.tsx양쪽에서 import하여 사용:-const HEADER_HEIGHT = 220; +import { HEADER_HEIGHT } from './constants';
step-navigator.tsx에서도:+import { HEADER_HEIGHT, SECTIONS } from './constants'; ... -<div className="h-[220px] w-full" aria-hidden="true" /> +<div style={{ height: HEADER_HEIGHT }} className="w-full" aria-hidden="true" />src/app/payment/_components/shipping-info.tsx (1)
80-84: 전화번호 입력 유효성 검증 부재
recipientPhone필드에 어떤 문자열이든 입력 가능합니다. 최소한type="tel"과pattern속성을 추가하거나,onChange에서 숫자/하이픈만 허용하는 로직을 고려하세요.src/types/domain/order.ts (1)
5-14: 이미지 필드명 불일치:productImagevsimageUrl
OrderListItem은productImage를,OrderItem은imageUrl을 사용합니다. 동일한 도메인에서 이미지 URL 필드명이 다르면 매핑 시 혼동이 발생합니다. 백엔드 API 응답과 일치시키되, 프론트엔드 내부에서는 통일된 네이밍을 고려하세요.Also applies to: 71-80
src/app/payment/page.tsx (1)
34-37:Promise.all실패 시 부분 데이터 유실
getUserInfo()와 아이템 페칭 중 하나가 실패하면Promise.all전체가 reject됩니다. 두 호출의 중요도가 다르다면Promise.allSettled로 변경하거나, 개별 에러 처리를 고려하세요. 현재는error.tsx경계가 없어 unhandled rejection이 됩니다.src/app/payment/complete/page.tsx (1)
62-87: 리스트key로idx대신 고유 식별자 사용 권장
item.productId가 고유하다면key={item.productId}로, 동일 상품이 여러 번 올 수 있다면 복합 키(${item.productId}-${item.selectedSize}-${item.selectedColor})를 사용하세요. 인덱스 키는 리스트 변경 시 불필요한 리렌더링을 유발합니다.src/app/actions/order.ts (2)
89-122:fetchDirectOrderItems—getProductDetail실패 시 에러 전파 미처리
getProductDetail이 throw하면 이 함수에서 catch 없이 에러가 그대로 전파됩니다. 서버 컴포넌트에서 호출 시error.tsx바운더리로 처리되지만, 사용자에게 보여줄 메시지가 제어되지 않습니다. 의도적이라면 무방하지만, 확인 부탁드립니다.
124-139:params as Record<...>타입 단언 —OrderListParams필드가Date등으로 확장되면 깨짐현재
OrderListParams는string | number | undefined필드만 있어 안전하지만, 향후 필드 타입이 변경되면 이 단언이 조용히 무효화됩니다.api.get의params타입이 이 패턴을 지원하는지도 확인해 주세요.src/app/payment/_components/payment-context.tsx (2)
1-17:SECTION_IDS가constants.ts에서 이미 export됨 — 중복 파생
constants.ts에서SECTION_IDS를 export하고 있으므로 직접 import하는 게 DRY합니다.♻️ 수정 제안
-import { SECTIONS } from './constants'; - -const SECTION_IDS = Object.values(SECTIONS); +import { SECTIONS, SECTION_IDS } from './constants';
38-42:usePaymenthook이 export되지 않음
ConnectedStepNavigator등 내부 컴포넌트에서만 사용 중이므로 현재는 문제 없지만, 외부에서 context 접근이 필요한 경우export가 필요합니다. 의도적 은닉이라면 현재 상태가 맞습니다.
| const originalTotal = items.reduce( | ||
| (acc, item) => acc + item.originalPrice * item.quantity, | ||
| 0, | ||
| ); | ||
| const discountTotal = items.reduce( | ||
| (acc, item) => acc + (item.originalPrice - item.price) * item.quantity, | ||
| 0, | ||
| ); | ||
| const finalTotal = items.reduce((acc, item) => acc + item.totalPrice, 0); |
There was a problem hiding this comment.
finalTotal과 originalTotal - discountTotal 불일치 가능성
finalTotal은 item.totalPrice의 합이고, originalTotal - discountTotal은 item.price * item.quantity의 합입니다. totalPrice !== price * quantity인 경우(예: 포인트 적용, 쿠폰 등) 표시 금액이 맞지 않을 수 있습니다. totalPrice의 산출 기준을 명확히 하거나, 한 가지 계산 방식으로 통일하세요.
🤖 Prompt for AI Agents
In `@src/app/payment/_components/order-items.tsx` around lines 22 - 30, finalTotal
(items.reduce(... item.totalPrice)) can diverge from originalTotal -
discountTotal (which effectively sums item.price * item.quantity) when
totalPrice includes extra adjustments (points, coupons, shipping, rounding).
Pick one canonical source of truth and use it consistently: either compute
totals from originalPrice/price/quantity and discount logic (update
originalTotal/discountTotal to mirror all adjustments) or derive discountTotal
and originalTotal from item.totalPrice so finalTotal equals originalTotal -
discountTotal. Update the functions/variables originalTotal, discountTotal, and
finalTotal and the place that sets item.totalPrice/item.price to ensure the same
calculation path is used and document which field (item.totalPrice vs
item.price*item.quantity) is authoritative.
| {hasDiscount && ( | ||
| <div className="flex items-center justify-between"> | ||
| <span>할인 금액</span> | ||
| <span className="text-2xl leading-[18px]"> | ||
| {discountTotal.toLocaleString()}원 | ||
| </span> | ||
| </div> |
There was a problem hiding this comment.
할인 금액에 부호 표시 누락
할인 금액이 양수로 표시되어 추가 비용처럼 보일 수 있습니다. - 접두사를 추가하세요.
수정 제안
<span className="text-2xl leading-[18px]">
- {discountTotal.toLocaleString()}원
+ -{discountTotal.toLocaleString()}원
</span>📝 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.
| {hasDiscount && ( | |
| <div className="flex items-center justify-between"> | |
| <span>할인 금액</span> | |
| <span className="text-2xl leading-[18px]"> | |
| {discountTotal.toLocaleString()}원 | |
| </span> | |
| </div> | |
| {hasDiscount && ( | |
| <div className="flex items-center justify-between"> | |
| <span>할인 금액</span> | |
| <span className="text-2xl leading-[18px]"> | |
| -{discountTotal.toLocaleString()}원 | |
| </span> | |
| </div> |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/order-items.tsx` around lines 79 - 85, The
discount amount is rendered without a negative sign making it look like an extra
charge; update the rendering in the component that uses hasDiscount and
discountTotal (in order-items.tsx) to prefix the displayed value with a minus
sign, e.g., render "-" + discountTotal.toLocaleString() (or use string
interpolation) so the UI shows "-{amount}원" when hasDiscount is true; ensure
this change only affects the displayed text and not the raw discountTotal value
used in calculations.
| try { | ||
| setIsSubmitting(true); | ||
| let orderId: number; | ||
|
|
||
| if (orderType === 'cart') { | ||
| orderId = await createOrderFromCart({ | ||
| cartItemIds: items | ||
| .map((item) => item.cartItemId) | ||
| .filter((id): id is number => id != null), | ||
| usedPoints, | ||
| ...shippingInfo, | ||
| }); | ||
| } else { | ||
| orderId = await createOrderFromProduct({ | ||
| items: items.map((item) => ({ | ||
| productId: item.productId, | ||
| selectedSize: item.selectedSize, | ||
| selectedColor: item.selectedColor, | ||
| quantity: item.quantity, | ||
| })), | ||
| usedPoints, | ||
| ...shippingInfo, | ||
| }); | ||
| } |
There was a problem hiding this comment.
cartItemIds 필터링 후 빈 배열이 API로 전송될 수 있음
items에 cartItemId가 모두 undefined인 경우 (데이터 불일치 시), 빈 cartItemIds: []가 createOrderFromCart로 전달됩니다. API에서 빈 배열을 거부하지 않으면 빈 주문이 생성될 수 있습니다.
🛡️ 방어 코드 제안
if (orderType === 'cart') {
+ const cartItemIds = items
+ .map((item) => item.cartItemId)
+ .filter((id): id is number => id != null);
+ if (cartItemIds.length === 0) {
+ throw new Error('유효한 장바구니 아이템이 없습니다.');
+ }
orderId = await createOrderFromCart({
- cartItemIds: items
- .map((item) => item.cartItemId)
- .filter((id): id is number => id != null),
+ cartItemIds,
usedPoints,
...shippingInfo,
});🤖 Prompt for AI Agents
In `@src/app/payment/_components/payment-context.tsx` around lines 96 - 119, When
building the order for orderType === 'cart', guard against sending an empty
cartItemIds array: after mapping/filtering items by cartItemId (refer to items
and cartItemId) check the resulting array length and if it's zero, set an error
state or throw/return early (and reset setIsSubmitting(false)) instead of
calling createOrderFromCart; ensure you still pass usedPoints and shippingInfo
only when cartItemIds is non-empty and update any UI error messaging
accordingly.
| export function PaymentButton() { | ||
| const { finalPrice, isSubmitting, handlePayment } = usePayment(); | ||
| return ( | ||
| <div className="safe-area-bottom fixed right-0 bottom-0 left-0 z-50 bg-white p-4"> | ||
| <div className="mx-auto max-w-xl"> | ||
| <button | ||
| onClick={handlePayment} | ||
| disabled={isSubmitting} | ||
| className="bg-ongil-teal flex w-full justify-between rounded-xl px-6 py-4 text-3xl font-normal text-white" | ||
| > | ||
| <span>{finalPrice.toLocaleString()}원</span> | ||
| <span>{isSubmitting ? '처리중...' : '결제하기'}</span> | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
<button>에 type="button" 및 disabled 상태 스타일 누락
type="button"미지정 시 HTML 기본값이"submit"입니다.<form>내부에 배치되는 경우 의도치 않은 submit이 발생할 수 있습니다.disabled상태에서 시각적 피드백이 없어 사용자가 버튼 비활성 상태를 인식할 수 없습니다 (a11y).
♻️ 수정 제안
<button
+ type="button"
onClick={handlePayment}
disabled={isSubmitting}
- className="bg-ongil-teal flex w-full justify-between rounded-xl px-6 py-4 text-3xl font-normal text-white"
+ className="bg-ongil-teal flex w-full justify-between rounded-xl px-6 py-4 text-3xl font-normal text-white disabled:opacity-50"
>📝 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.
| export function PaymentButton() { | |
| const { finalPrice, isSubmitting, handlePayment } = usePayment(); | |
| return ( | |
| <div className="safe-area-bottom fixed right-0 bottom-0 left-0 z-50 bg-white p-4"> | |
| <div className="mx-auto max-w-xl"> | |
| <button | |
| onClick={handlePayment} | |
| disabled={isSubmitting} | |
| className="bg-ongil-teal flex w-full justify-between rounded-xl px-6 py-4 text-3xl font-normal text-white" | |
| > | |
| <span>{finalPrice.toLocaleString()}원</span> | |
| <span>{isSubmitting ? '처리중...' : '결제하기'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function PaymentButton() { | |
| const { finalPrice, isSubmitting, handlePayment } = usePayment(); | |
| return ( | |
| <div className="safe-area-bottom fixed right-0 bottom-0 left-0 z-50 bg-white p-4"> | |
| <div className="mx-auto max-w-xl"> | |
| <button | |
| type="button" | |
| onClick={handlePayment} | |
| disabled={isSubmitting} | |
| className="bg-ongil-teal flex w-full justify-between rounded-xl px-6 py-4 text-3xl font-normal text-white disabled:opacity-50" | |
| > | |
| <span>{finalPrice.toLocaleString()}원</span> | |
| <span>{isSubmitting ? '처리중...' : '결제하기'}</span> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/payment-context.tsx` around lines 190 - 206, The
PaymentButton component uses <button> without an explicit type and lacks visible
disabled styling; update the PaymentButton (and the JSX returned by the
component that uses usePayment, referencing finalPrice, isSubmitting,
handlePayment) to set type="button" to avoid implicit form submission and add a
visible/accessible disabled state by including the disabled attribute bound to
isSubmitting and applying conditional styling (e.g., lower opacity,
pointer-events-none or distinct background/text classes) when isSubmitting is
true; ensure any text indicating processing (isSubmitting ? '처리중...' : '결제하기')
remains and consider adding aria-disabled when appropriate for screen readers.
| <button | ||
| onClick={() => onPointsChange(Math.min(userPoints, totalPrice))} | ||
| className="h-[87px] rounded-lg border border-black text-left text-2xl leading-normal font-medium" | ||
| > | ||
| <div className="flex flex-col justify-center px-4 py-1"> | ||
| <span className="">모두</span> | ||
| <span className="">사용</span> | ||
| </div> | ||
| </button> |
There was a problem hiding this comment.
<button>에 type="button" 명시 필요
부모에 <form>이 존재할 경우, type 미지정 버튼은 기본적으로 type="submit"으로 동작하여 의도치 않은 폼 제출이 발생할 수 있습니다.
수정 제안
<button
+ type="button"
onClick={() => onPointsChange(Math.min(userPoints, totalPrice))}📝 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.
| <button | |
| onClick={() => onPointsChange(Math.min(userPoints, totalPrice))} | |
| className="h-[87px] rounded-lg border border-black text-left text-2xl leading-normal font-medium" | |
| > | |
| <div className="flex flex-col justify-center px-4 py-1"> | |
| <span className="">모두</span> | |
| <span className="">사용</span> | |
| </div> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => onPointsChange(Math.min(userPoints, totalPrice))} | |
| className="h-[87px] rounded-lg border border-black text-left text-2xl leading-normal font-medium" | |
| > | |
| <div className="flex flex-col justify-center px-4 py-1"> | |
| <span className="">모두</span> | |
| <span className="">사용</span> | |
| </div> | |
| </button> |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/payment-info.tsx` around lines 41 - 49, The
button in the payment-info component that calls
onPointsChange(Math.min(userPoints, totalPrice)) lacks an explicit type, so when
rendered inside a parent <form> it will act as type="submit" and may trigger
unintended form submission; update the JSX for that button element (the one
invoking onPointsChange with userPoints and totalPrice) to include type="button"
to prevent form submission.
| <Input | ||
| value={value.postalCode} | ||
| readOnly | ||
| placeholder="우편번호" | ||
| className="w-1/3 bg-gray-50" | ||
| /> |
There was a problem hiding this comment.
우편번호·주소 입력에 접근성 라벨 누락
postalCode와 deliveryAddress Input에 id/aria-label이 없어 스크린 리더에서 필드를 식별할 수 없습니다. Label과 연결하거나 aria-label을 추가하세요.
🤖 Prompt for AI Agents
In `@src/app/payment/_components/shipping-info.tsx` around lines 91 - 96, The
postalCode and deliveryAddress Input components lack accessible labels; update
the Input usages that render value={value.postalCode} and
value={value.deliveryAddress} (the Input elements in shipping-info.tsx) to
include either an explicit associated <label> (connect via matching id on Input)
or add aria-label attributes (e.g., aria-label="우편번호" and aria-label="배송지 주소");
ensure the id/aria-label strings are meaningful and unique so screen readers can
identify the fields and, if using label, set the Input id to match the label's
htmlFor.
| <span | ||
| className={cn( | ||
| 'text-center', | ||
| isActive | ||
| ? 'text-ongil-teal' | ||
| : 'text-gray-400 group-hover:text-gray-500', | ||
| )} | ||
| > | ||
| {step.label} | ||
| </span> |
There was a problem hiding this comment.
\n이 줄바꿈으로 렌더링되지 않습니다.
step.label에 \n이 포함되어 있지만, <span>에 whitespace-pre-line 클래스가 없어 브라우저가 공백으로 처리합니다. 두 줄 표시가 의도라면 CSS를 추가해 주세요.
수정 제안
<span
className={cn(
- 'text-center',
+ 'whitespace-pre-line text-center',
isActive
? 'text-ongil-teal'
: 'text-gray-400 group-hover:text-gray-500',
)}
>📝 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.
| <span | |
| className={cn( | |
| 'text-center', | |
| isActive | |
| ? 'text-ongil-teal' | |
| : 'text-gray-400 group-hover:text-gray-500', | |
| )} | |
| > | |
| {step.label} | |
| </span> | |
| <span | |
| className={cn( | |
| 'whitespace-pre-line text-center', | |
| isActive | |
| ? 'text-ongil-teal' | |
| : 'text-gray-400 group-hover:text-gray-500', | |
| )} | |
| > | |
| {step.label} | |
| </span> |
🤖 Prompt for AI Agents
In `@src/app/payment/_components/step-navigator.tsx` around lines 68 - 77, The
span rendering step.label doesn't preserve newline characters because it lacks
the whitespace-pre-line utility; update the className on the span in
step-navigator.tsx (the element that renders {step.label}) to include
"whitespace-pre-line" alongside the existing classes so any "\n" in step.label
is rendered as a line break (ensure it is applied only when multiline labels are
expected).
| import type { Metadata } from 'next'; | ||
| import { getOrderDetail } from '@/app/actions/order'; | ||
| import Image from 'next/image'; | ||
| import { auth } from '/auth'; |
There was a problem hiding this comment.
'/auth' import 경로 — '@/auth'로 수정 필요
page.tsx와 동일한 문제입니다. '/auth'는 파일 시스템 루트를 가리키므로 빌드 실패 가능성이 있습니다.
수정 제안
-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/payment/complete/page.tsx` at line 6, The import path for auth is
using a root-absolute string ("import { auth } from '/auth'") which can break
builds; change it to the project alias import ("@/auth") wherever this pattern
appears (replace the import in page.tsx that references auth and any similar
leading-slash imports), ensuring the symbol name auth remains unchanged and
tests/exports still resolve correctly.
| <span>주소</span> | ||
| <span className="text-right text-xl whitespace-pre-line"> | ||
| {order.deliveryAddress.replace('(', '\n(')} | ||
| </span> |
There was a problem hiding this comment.
order.deliveryAddress가 falsy일 때 .replace() 호출 시 런타임 에러
OrderDetail 타입에서 deliveryAddress는 string이지만, API 응답에서 빈 문자열이나 예상치 못한 값이 올 수 있습니다. optional chaining으로 방어하세요.
수정 제안
- {order.deliveryAddress.replace('(', '\n(')}
+ {order.deliveryAddress?.replace('(', '\n(') ?? ''}📝 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.
| <span>주소</span> | |
| <span className="text-right text-xl whitespace-pre-line"> | |
| {order.deliveryAddress.replace('(', '\n(')} | |
| </span> | |
| <span>주소</span> | |
| <span className="text-right text-xl whitespace-pre-line"> | |
| {order.deliveryAddress?.replace('(', '\n(') ?? ''} | |
| </span> |
🤖 Prompt for AI Agents
In `@src/app/payment/complete/page.tsx` around lines 113 - 116, The code calls
order.deliveryAddress.replace(...) which can throw if deliveryAddress is
null/undefined/other falsy; change the expression to guard and provide a safe
default before calling replace (e.g., use optional chaining or nullish
coalescing like (order.deliveryAddress ?? '').replace(...) or
order.deliveryAddress?.replace(...) ?? '' ) in the JSX where
order.deliveryAddress is used so replace is only invoked on a string and the UI
shows an empty/fallback value when missing.
| export default async function PaymentPage({ searchParams }: PageProps) { | ||
| const session = await auth(); | ||
| if (!session) redirect('/login'); | ||
|
|
||
| const params = await searchParams; | ||
| const isCartOrder = params.cart === 'true'; | ||
|
|
||
| const [user, items] = await Promise.all([ | ||
| getUserInfo(), | ||
| isCartOrder ? fetchCartOrderItems(params) : fetchDirectOrderItems(params), | ||
| ]); |
There was a problem hiding this comment.
loading.tsx 누락 — 서버 데이터 페칭 중 빈 화면 노출
PaymentPage는 서버 컴포넌트에서 auth(), getUserInfo(), fetchCartOrderItems()/fetchDirectOrderItems()를 await합니다. loading.tsx가 없으면 데이터 로딩 중 사용자에게 아무것도 표시되지 않습니다. src/app/payment/loading.tsx를 추가하세요.
Based on learnings: "Always provide a loading.tsx for route segments and granular Fallbacks for components."
🤖 Prompt for AI Agents
In `@src/app/payment/page.tsx` around lines 27 - 37, PaymentPage awaits server
data (auth(), getUserInfo(), fetchCartOrderItems(), fetchDirectOrderItems()) so
add a route-level fallback component by creating src/app/payment/loading.tsx
that renders a simple loading UI (spinner/placeholder) to display while the
PaymentPage data resolves; this ensures the route segment shows a fallback
instead of a blank screen during awaits and provides a granular loading state
for the PaymentPage route.
📝 개요
🚀 주요 변경 사항
📸 스크린샷 (선택)
✅ 체크리스트
이슈 해결 여부
PR 본문에 Closes #이슈번호 이라고 적으면, PR이 머지될 때 해당 이슈가 자동으로 닫힙니다