[FEAT]: 배송지 관리 플로우 고도화 및 마이페이지 내 정보 수정 기능 확장#62
Conversation
|
Important Review skippedThis PR was authored by the user configured for CodeRabbit reviews. By default, CodeRabbit skips reviewing PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback. To trigger a single review, invoke the You can disable this status message by setting the Use the checkbox below for a quick retry:
Walkthrough주소 CRUD 서버 액션 7개와 관련 타입을 추가하고, 주소 목록/등록/수정 페이지 및 클라이언트 컴포넌트(폼, 리스트, 아이템, 카드 등)를 도입했으며 주문 취소 플로우에 배송지 단계와 기본주소 연동을 통합함. (50단어 이내) Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Browser as Browser
participant AddressForm as AddressForm (Client)
participant ServerAction as Server Action<br/>src/app/actions/address.ts
participant API as Backend API
participant NextCache as Next.js Cache<br/>(revalidatePath)
User->>Browser: 주소 폼 제출
Browser->>AddressForm: submit 이벤트
AddressForm->>AddressForm: 입력 검증
AddressForm->>ServerAction: registerAddress / updateAddress
ServerAction->>API: POST/PUT /addresses
API-->>ServerAction: AddressResponseData
alt set as default 선택
ServerAction->>API: POST /addresses/{id}/default
API-->>ServerAction: ApiStatusResponse
end
ServerAction->>NextCache: revalidatePath('/address')
ServerAction->>NextCache: revalidatePath('/orders/checkout')
ServerAction-->>AddressForm: 응답 반환
AddressForm->>Browser: router.back() / router.refresh()
sequenceDiagram
participant User as User
participant Browser as Browser
participant Page as CancelReasonPage (Server)
participant CancelForm as CancelForm (Client)
participant AddrAction as getAddresses (Server Action)
participant API as Backend API
User->>Browser: 주문 취소 페이지 진입
Browser->>Page: 요청
Page->>AddrAction: getAddresses()
AddrAction->>API: GET /addresses
API-->>AddrAction: AddressItem[]
AddrAction-->>Page: addresses
Page->>Browser: 렌더 (CancelForm with defaultAddress)
User->>CancelForm: 사유 선택 (WRONG_ADDRESS)
CancelForm->>CancelForm: step -> 'address'
CancelForm->>AddrAction: (if mounted) getAddresses() 재요청
AddrAction->>API: GET /addresses
API-->>AddrAction: AddressItem[]
CancelForm-->>User: ShippingInfoCard 표시 / 다음(확인) 가능 여부 결정
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
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]/cancel/_components/cancel-form.tsx (1)
109-109:⚠️ Potential issue | 🟡 Minor
console.log잔존PR 체크리스트에 "불필요한 console.log 제거"로 표시되어 있으나 이 로그가 남아 있습니다.
🔧 제거
- console.log('환불 정보 응답:', data);
🤖 Fix all issues with AI agents
In `@src/app/actions/address.ts`:
- Around line 55-56: The code calls revalidatePath('/me/address') for address
mutations but the actual routes are /address, /address/[addressId],
/address/new; update each occurrence to revalidatePath('/address') so the
/address page cache is properly invalidated. Specifically, in the functions
registerAddress, updateAddress, deleteAddress, and setAsDefaultAddress replace
revalidatePath('/me/address') with revalidatePath('/address') and leave the
existing revalidatePath('/orders/checkout') call in registerAddress untouched.
In `@src/app/address/page.tsx`:
- Around line 36-43: The fixed bottom container isn't centered because mx-auto
alone doesn't center position:fixed elements; update the container div used
around the Link (the div with className starting "fixed bottom-0 ...") to
include horizontal inset (e.g., add left-0 right-0 or use inset-x-0) so the
max-w-2xl + mx-auto can center it, or alternatively use left-1/2 with transform
-translate-x-1/2 to center; modify the className on that div (the wrapper for
the "+ 새 배송지 추가" Link) accordingly.
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx:
- Around line 361-380: The address step currently returns only a "이전 단계" button
so after changing address via ShippingInfoCard there's no way to continue;
update the UI in the branch where step === 'address' (the component rendering
ShippingInfoCard and the footer buttons) to provide a forward action — either
add a "다음 단계"/"확인" button that calls setStep('reason' or nextStep) or, if
cancellation must restart reason selection, show a clear helper message and add
a button that resets step to the appropriate value (use the existing setStep
function and the 'address' step identifier and ShippingInfoCard component to
place the control). Ensure the new control is accessible in the bottom button
area next to the existing previous button.
In `@src/components/address/address-form.tsx`:
- Around line 107-108: The sequence router.back(); router.refresh(); is unsafe
because refresh may run against the current page rather than the navigated-to
page; update the flow by removing router.refresh() after router.back() and
instead call revalidatePath(...) from the server action that updates address
data (or invoke revalidatePath for the previous page path) to ensure the
previous page’s server components are invalidated; locate the usage in the
address form component (router.back, router.refresh) and the server action that
persists the address, then add revalidatePath with the correct route identifier
there.
In `@src/components/address/address-item.tsx`:
- Around line 18-29: The handleDelete function can leave isDeleting stuck true
because you only reset setIsDeleting(false) in the catch block; change it to
always reset in a finally block: wrap the await deleteAddress(item.addressId)
and router.refresh() in try and move setIsDeleting(false) into finally so
setIsDeleting is cleared regardless of success or error, and remove the
redundant setIsDeleting(false) from the catch; use the existing symbols
handleDelete, setIsDeleting, deleteAddress and router.refresh to locate and
update the code.
In `@src/components/address/shipping-info-card.tsx`:
- Around line 26-33: The container div in shipping-info-card.tsx applies both
gap-6 and space-y-3 which double-applies vertical spacing and also uses an
invalid Tailwind class wrap-break-words; update the className on the div (the
element rendering address.recipientName / baseAddress / recipientPhone /
detailAddress) to use only one spacing utility (remove space-y-3 or gap-6 — keep
gap-6 for consistent gap between flex children) and replace wrap-break-words
with the correct Tailwind v4 class wrap-break-word on the paragraph that renders
address.detailAddress.
🧹 Nitpick comments (9)
src/app/orders/[orderId]/cancel/page.tsx (1)
27-29:getOrderDetail과getAddresses를Promise.all로 병렬화두 API 호출이 독립적이므로 순차 실행할 이유가 없습니다. 병렬화하면 페이지 로딩 시간이 단축됩니다.
⚡ 병렬 fetch 제안
- const orderDetail = await getOrderDetail(numericId); - const addresses = await getAddresses(); - const defaultAddress = addresses.find((addr) => addr.isDefault) || null; + const [orderDetail, addresses] = await Promise.all([ + getOrderDetail(numericId), + getAddresses(), + ]); + const defaultAddress = addresses.find((addr) => addr.isDefault) || null;src/components/address/shipping-info-card.tsx (1)
1-1:'use client'제거 가능이 컴포넌트는 클라이언트 훅이나 이벤트 핸들러를 사용하지 않고
Link와 조건부 렌더링만 합니다. 서버 컴포넌트로 전환하면 클라이언트 번들 크기를 줄일 수 있습니다.src/app/address/[addressId]/page.tsx (2)
14-17: 전체 주소 목록을 가져와 단건 조회하는 패턴
getAddresses()로 전체 목록을 가져온 뒤find로 필터링하는 것은 불필요한 네트워크/DB 비용입니다. 단건 조회 API(GET /addresses/:id)를 서버 액션으로 추가하면 편집 페이지 로딩이 개선됩니다.#!/bin/bash # Description: 단건 주소 조회 API가 이미 존재하는지 확인 rg -n "getAddress\b" --type-add 'ts:*.tsx' --type-add 'ts:*.ts' --type=ts -C 3
14-14:isNaN→Number.isNaN일관성
cancel/page.tsx에서는Number.isNaN(numericId)를 사용하고 있습니다. 일관성을 위해 동일하게 맞춰주세요. (isNaN은 암묵적 타입 변환이 발생합니다.)🔧 수정 제안
- if (isNaN(id)) notFound(); + if (Number.isNaN(id)) notFound();src/components/address/address-form.tsx (2)
196-207:deliveryRequest필드 레이아웃 불일치 및 접근성다른 필드는 모두
grid-cols-[88px_1fr]레이아웃에<Label>을 사용하지만, 배송 요청사항 필드만 그리드 없이 렌더링되고<label>연결도 없습니다.<span aria-hidden="true" />는 의미 없는 빈 요소입니다. 접근성 측면에서 스크린 리더가 이 입력 필드의 용도를 파악할 수 없습니다.
139-145: 전화번호 입력에type="tel"및 기본 포맷 검증 부재
<Input>에type="tel"을 추가하면 모바일에서 숫자 키패드가 표시되어 UX가 개선됩니다. 최소한의 패턴 검증(예: 자릿수)도 고려해 주세요.src/components/address/address-item.tsx (1)
76-97: 접근성(a11y): 버튼에 구체적인aria-label누락"삭제", "기본 배송지로 설정" 버튼에 어떤 배송지에 대한 액션인지 스크린 리더가 구분할 수 없습니다. 목록에서 여러 아이템이 렌더링될 때 동일한 라벨의 버튼이 반복됩니다.
제안
<button onClick={handleSetDefault} disabled={isSettingDefault} + aria-label={`${item.recipientName} 배송지를 기본 배송지로 설정`} className="text-sm text-gray-500 underline decoration-gray-300 underline-offset-4 hover:text-gray-800" > <button onClick={handleDelete} disabled={isDeleting} + aria-label={`${item.recipientName} 배송지 삭제`} className="text-sm text-gray-500 underline decoration-gray-300 underline-offset-4 hover:text-red-500" >src/types/domain/address.ts (2)
1-10: 타입 간 필드 네이밍 불일치
AddressItem은recipientPhone,baseAddress+detailAddress를 사용하지만,AddressRequest는phone,baseAddress+detailAddress,AddressDetail은phone,address(단일 필드)를 사용합니다. 백엔드 API 스펙에 맞춘 것이라면 이해되지만, 프론트엔드 내부에서 매핑할 때 혼동의 소지가 있습니다.Also applies to: 12-20, 22-30
43-47:ApiStatusResponse는 도메인 공통 타입으로 분리 검토
ApiStatusResponse는 address 도메인에 한정되지 않는 범용 응답 타입입니다.src/types/common/등 공통 타입 위치로 이동하면 다른 도메인에서도 재사용할 수 있습니다.
There was a problem hiding this comment.
Actionable comments posted: 5
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]/cancel/_components/cancel-form.tsx (1)
109-109:⚠️ Potential issue | 🟡 Minor
console.log제거 필요PR 체크리스트에 "불필요한 console.log 제거"가 표시되어 있으나, 이 디버그 로그가 남아 있습니다.
🔧 수정 제안
- console.log('환불 정보 응답:', data);
🤖 Fix all issues with AI agents
In `@src/app/actions/address.ts`:
- Around line 67-86: updateAddress (and registerAddress) currently revalidates
only '/address' so the order cancel page (/orders/[orderId]/cancel) isn't
refreshed; after a successful API call in updateAddress and registerAddress,
also call revalidatePath('/orders', 'layout') (or
revalidatePath(`/orders/${orderId}/cancel`) when orderId is available) to
trigger layout-level revalidation for orders so the cancel page reflects the
updated address; locate the calls in updateAddress and registerAddress and add
the revalidatePath invocation immediately after the successful response
handling.
In `@src/app/address/page.tsx`:
- Around line 6-7: 현재 AddressListPage가 호출하는 getAddresses()에서 예외가 발생하면 처리할 에러
바운더리가 없어 사용자에게 흰 화면이 나올 수 있으니 src/app/address 경로에 error.tsx를 추가해 주세요; 파일은
Next.js App Router의 에러 경계 규약에 맞게 export default function Error({ error, reset
})를 구현하고 에러 메시지 표시와 함께 사용자에게 재시도 버튼을 제공하여 reset()을 호출하도록 하며(또는 간단한 친숙한 UI로 대체),
필요하면 getAddresses나 AddressListPage 관련 콘텍스트를 포함한 설명 문구를 포함해 에러 상태를 우아하게 처리하도록
만드세요.
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx:
- Around line 361-394: The page's defaultAddress prop isn't refreshed after
editing addresses because only revalidatePath('/address') is called; update the
server actions (updateAddress/registerAddress) to also call revalidatePath for
the cancel page (e.g. revalidatePath(`/orders/${orderId}/cancel`) or the
matching route) so the cancel page's defaultAddress is invalidated, or
alternatively add a client-side refresh when entering the address step (in the
component where step is set to 'address'—see setStep and canProceed logic around
defaultAddress and ShippingInfoCard) to refetch the current address before
rendering the buttons. Ensure you reference the existing functions
updateAddress/registerAddress and the cancel page route when implementing the
revalidation or client fetch.
In `@src/components/address/address-form.tsx`:
- Line 1: The file contains a UTF-8 BOM character (U+FEFF) immediately before
the module directive "'use client'"; remove the invisible BOM character at the
start of the file so the file begins exactly with 'use client' (edit the top of
the src/components/address/address-form.tsx file and re-save without BOM, or
re-encode the file to UTF-8 without BOM) to prevent linting/build issues.
- Around line 37-44: The form loses previously entered deliveryRequest because
AddressItem (used to populate initialData) lacks a deliveryRequest field; either
add deliveryRequest to the AddressItem type definition so
initialData?.deliveryRequest is available when initializing formState
(AddressRequest) or change the edit flow to fetch the full AddressDetail for the
selected address (e.g., call the individual address detail endpoint instead of
getAddresses()) and populate formState from that response; update references
around initialData, AddressItem, AddressDetail, getAddresses(), and the useState
initialization of formState to ensure deliveryRequest is restored in edit mode.
🧹 Nitpick comments (4)
src/app/actions/address.ts (1)
14-28: 읽기 전용 함수의'use server'사용
getAddresses와getMyAddress는 데이터 조회만 수행하는데 서버 액션으로 노출되어 있습니다. 기능상 문제는 없지만, Next.js에서 서버 액션은 주로 mutation용이고, 서버 컴포넌트에서의 데이터 페칭은 일반 async 함수나fetch직접 호출이 관용적입니다. 현재 구조상 서버 컴포넌트와 클라이언트 컴포넌트 양쪽에서 호출해야 하므로 합리적인 선택이긴 합니다.Also applies to: 30-44
src/components/address/address-form.tsx (2)
74-86: 전화번호 형식 검증 부재필수 필드 존재 여부만 확인하고 있습니다.
phone필드에 대한 형식 검증(예: 숫자만, 올바른 길이)이 없어 잘못된 값이 서버로 전달될 수 있습니다. 서버 측 검증이 있더라도 클라이언트에서 조기 피드백을 주는 것이 UX상 좋습니다.
197-208:aria-hidden빈<span>용도 불명확Lines 198, 211의
<span aria-hidden="true" />는 다른 필드 섹션의grid-cols-[88px_1fr]레이아웃에 맞추기 위한 의도로 보이지만, 해당 컨테이너(div)에는grid클래스가 없어 실제로 아무 역할을 하지 않습니다. grid 정렬이 필요하다면 동일한 grid 패턴을 적용하고, 아니라면 제거해 주세요.🔧 grid 정렬 적용 또는 span 제거
grid 정렬이 불필요한 경우:
- <div className="items-center gap-4"> - <span aria-hidden="true" /> - <div className="relative"> + <div className="relative"><div className="flex items-center justify-start gap-4 pt-2"> - <span aria-hidden="true" /> <labelAlso applies to: 210-211
src/components/address/shipping-info-card.tsx (1)
26-33: address-item.tsx와 Tailwind 유틸리티 클래스 일관성 맞추기동일한
detailAddress필드를 처리하는 address 도메인 컴포넌트들이 다른 유틸리티를 사용 중입니다. shipping-info-card.tsx는wrap-break-word(line 30), address-item.tsx는break-words(line 71)를 사용합니다. Tailwind v4에서 두 클래스 모두 유효하며 기능이 동일하지만, 도메인 내 컴포넌트 간 일관성을 위해 하나로 통일하는 것을 권장합니다.
| export default async function AddressListPage() { | ||
| const addresses = await getAddresses(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for error.tsx files in the address route and its parents
echo "=== Checking error.tsx in address route ==="
find src/app/address -name "error.tsx" 2>/dev/null
echo -e "\n=== Checking error.tsx in parent routes ==="
find src/app -name "error.tsx" 2>/dev/null
echo -e "\n=== Directory structure of address route ==="
ls -la src/app/address/ 2>/dev/null || echo "Directory not found"Repository: IT-Cotato/12th-OnGil-FE
Length of output: 489
🏁 Script executed:
#!/bin/bash
# Check for root/global error.tsx
echo "=== Looking for error.tsx at app root ==="
ls -la src/app/error.tsx 2>/dev/null || echo "No root error.tsx found"
# Find getAddresses implementation
echo -e "\n=== Finding getAddresses function ==="
rg "getAddresses" src/ -A 5 --type tsx --type ts -B 2
# Check app structure for error boundaries
echo -e "\n=== App directory structure ==="
find src/app -maxdepth 1 -type f -name "*.tsx" | sortRepository: IT-Cotato/12th-OnGil-FE
Length of output: 285
🏁 Script executed:
#!/bin/bash
# Search for getAddresses without file type restrictions
echo "=== Finding getAddresses function ==="
rg "getAddresses" src/ -B 3 -A 5
# Check imports in the address page
echo -e "\n=== Address page imports ==="
head -20 src/app/address/page.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 6196
🏁 Script executed:
#!/bin/bash
# Check for rethrowNextError to understand error handling
echo "=== rethrowNextError implementation ==="
rg "rethrowNextError" src/ -B 2 -A 8
# Check if there's a global error handler at root
echo -e "\n=== Files in src/app root ==="
ls -la src/app/*.tsx
# Check global.error.tsx or other error handlers
echo -e "\n=== All error.tsx and error.* files in project ==="
find src -name "*error*" 2>/dev/nullRepository: IT-Cotato/12th-OnGil-FE
Length of output: 23438
src/app/address 라우트에 error.tsx 바운더리 추가 필요
현재 getAddresses()가 throw할 경우 처리할 error.tsx가 존재하지 않습니다. 프로덕션 환경에서 사용자에게 흰 화면이 표시될 수 있으므로 src/app/address/error.tsx를 추가하여 에러 상태를 처리하세요.
🤖 Prompt for AI Agents
In `@src/app/address/page.tsx` around lines 6 - 7, 현재 AddressListPage가 호출하는
getAddresses()에서 예외가 발생하면 처리할 에러 바운더리가 없어 사용자에게 흰 화면이 나올 수 있으니 src/app/address
경로에 error.tsx를 추가해 주세요; 파일은 Next.js App Router의 에러 경계 규약에 맞게 export default
function Error({ error, reset })를 구현하고 에러 메시지 표시와 함께 사용자에게 재시도 버튼을 제공하여 reset()을
호출하도록 하며(또는 간단한 친숙한 UI로 대체), 필요하면 getAddresses나 AddressListPage 관련 콘텍스트를 포함한 설명
문구를 포함해 에러 상태를 우아하게 처리하도록 만드세요.
There was a problem hiding this comment.
Actionable comments posted: 4
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]/cancel/_components/cancel-form.tsx (1)
113-113:⚠️ Potential issue | 🟡 Minor
console.log잔존 — PR 체크리스트와 불일치.PR 설명에 "불필요한 console.log 제거 완료"로 표시되어 있으나, 이 로그가 남아 있습니다. 프로덕션 빌드에서 환불 정보가 콘솔에 노출됩니다.
🧹 제거 제안
- console.log('환불 정보 응답:', data);
🤖 Fix all issues with AI agents
In `@src/app/orders/`[orderId]/cancel/_components/cancel-form.tsx:
- Around line 130-155: The address fetch useEffect currently swallows
getAddresses() errors and only logs to console; update the catch handler in the
useEffect that watches step to set an explicit error state or call
setAlertMessage (same pattern as getRefundInfo's handler) so the UI shows a
network/fetch error and offers retry; ensure you still respect the isMounted
guard and that setAddressLoading(false) runs in finally so loading state is
cleared, and update any canProceed logic to consider the new error state when
disabling the next step (use the useEffect/getAddresses block and functions
setAddressLoading, setCurrentDefaultAddress, setAlertMessage to implement this).
In `@src/components/address/address-form.tsx`:
- Around line 33-35: The checkbox state initialization (isDefaultAddress via
useState(initialData?.isDefault ?? false)) and the update flow only call
setAsDefaultAddress, so when editing an address that is already the default you
cannot unset it; either call the API to unset the default or disable unchecking
in the UI. Update the address form to detect initialData?.isDefault and, if the
backend has no "unset" endpoint, render the checkbox disabled (and keep
isDefaultAddress true) or, if an unset API exists, add an API call in the change
handler that calls the unset endpoint (and update setIsDefaultAddress
accordingly) instead of only calling setAsDefaultAddress. Ensure you modify the
checkbox change handler and any submit logic that references isDefaultAddress,
setIsDefaultAddress, and setAsDefaultAddress so the UI and server remain
consistent.
- Around line 205-219: deliveryRequest input lacks an accessible label; add a
<Label htmlFor="deliveryRequest"> (matching the other fields and the grid
layout) to associate with the Input having id="deliveryRequest" so screen
readers can identify it, or alternatively add a descriptive aria-label on the
Input; update the same JSX block that contains Input (refer to
id="deliveryRequest", setIsDeliveryRequestTouched, and handleChange) to include
the Label element with the correct htmlFor or add aria-label text.
- Around line 104-110: The current falsy check skips setting default when
targetAddressId === 0; update the conditional to explicitly test for
null/undefined instead of a truthy check: after calling
registerAddress(formState) and assigning targetAddressId =
response.shippingDetail.addressId, ensure the subsequent if uses
isDefaultAddress && targetAddressId != null (or !== null && !== undefined)
before calling setAsDefaultAddress(targetAddressId); keep usage of
registerAddress, response.shippingDetail.addressId, isDefaultAddress and
setAsDefaultAddress as the referenced symbols to locate the change.
🧹 Nitpick comments (5)
src/app/orders/[orderId]/cancel/_components/cancel-form.tsx (2)
329-335:WRONG_ADDRESS플로우에서 "이전 단계" 클릭 시address단계를 건너뜀.
reason → address → confirm순서로 진입했더라도, confirm 단계의 "이전 단계" 버튼은 항상'reason'으로 돌아갑니다.WRONG_ADDRESS경로에서는'address'로 돌아가는 것이 자연스러운 UX입니다.♻️ 조건부 back 네비게이션 제안
<button className="rounded-xl bg-[`#D9D9D9`] py-4 text-2xl" - onClick={() => setStep('reason')} + onClick={() => setStep(selectedReason === 'WRONG_ADDRESS' ? 'address' : 'reason')} > 이전 단계 </button>
392-427: 배송지 단계 구현 — 이전 리뷰 피드백 반영 확인.앞으로 진행 버튼과 진입 시 최신 주소 fetch가 모두 반영되어 있습니다.
접근성 관련 한 가지: line 399-405의 상태 메시지가 동적으로 변경되지만
aria-live속성이 없어 스크린 리더 사용자에게 변경 사항이 전달되지 않습니다.♿ aria-live 추가 제안
- <p className="mt-4 px-1 text-center text-sm text-gray-600"> + <p className="mt-4 px-1 text-center text-sm text-gray-600" aria-live="polite">src/components/address/address-form.tsx (3)
80-89: 전화번호 형식 검증 누락
phone필드가 비어있는지만 확인하고, 실제 전화번호 형식(예: 숫자만, 010-XXXX-XXXX 패턴)은 검증하지 않습니다. 잘못된 형식이 서버로 전송될 수 있습니다.제안
+ const phoneRegex = /^01[016789]-?\d{3,4}-?\d{4}$/; + if ( !formState.recipientName || !formState.phone || !formState.postalCode || !formState.baseAddress || !formState.detailAddress ) { alert('필수 정보를 모두 입력해주세요.'); return; } + + if (!phoneRegex.test(formState.phone)) { + alert('올바른 휴대폰 번호를 입력해주세요.'); + return; + }
221-236: 기본 배송지 체크박스 영역의 불필요한<span aria-hidden="true" />Line 222의
<span aria-hidden="true" />는 DOM에 빈 요소를 추가할 뿐 레이아웃이나 접근성에 기여하지 않습니다. 제거해도 동작에 영향이 없습니다.
148-154: 전화번호 입력에type="tel"누락모바일 기기에서 숫자 키패드를 표시하고 접근성을 높이려면
type="tel"을 지정하는 것이 좋습니다.<Input id="phone" + type="tel" value={formState.phone}
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/orders/`[orderId]/cancel/page.tsx:
- Around line 29-33: Promise.all currently causes the whole page to fail if
getAddresses rejects; change the fetch so getOrderDetail(numericId) is awaited
normally and getAddresses() is wrapped to gracefully handle errors (e.g.,
try/catch or Promise.allSettled) and fall back to an empty array on failure so
defaultAddress becomes null; keep references to getOrderDetail, getAddresses,
numericId, defaultAddress and leave CancelForm's client-side address re-fetch
intact so the cancel flow still renders even when addresses API is down.
🧹 Nitpick comments (5)
src/app/orders/[orderId]/cancel/_components/cancel-form.tsx (1)
404-456: ShippingInfoCard 링크를 통한 전체 페이지 네비게이션 시 클라이언트 상태 소실
ShippingInfoCard내부의Link가/address/...로 이동하면 현재 cancel 페이지의 클라이언트 상태(step,selectedReason,detail등)가 모두 초기화됩니다. 사용자가 배송지를 수정 후 브라우저 뒤로가기로 돌아오면 처음부터 다시 시작해야 합니다.당장 수정이 어려울 수 있지만, 향후 개선 방향으로 다음을 고려해보세요:
ShippingInfoCard의 링크에searchParams로returnTo를 전달하고, 배송지 수정 완료 후 자동 리다이렉트- 또는 배송지 수정을 모달/시트로 처리하여 현재 페이지 상태를 유지
src/components/address/address-form.tsx (3)
98-129:setAsDefaultAddress실패 시 부분 성공 상태가 사용자에게 전달되지 않음.
registerAddress/updateAddress성공 후setAsDefaultAddress가 실패하면, 주소는 이미 저장된 상태이지만 catch 블록의 에러 메시지만 표시됩니다. 사용자는 주소 저장이 실패한 것으로 오해할 수 있습니다.기본 배송지 설정 실패를 별도로 처리하면 UX가 개선됩니다:
♻️ 수정 제안
if (isDefaultAddress && targetAddressId != null) { - await setAsDefaultAddress(targetAddressId); + try { + await setAsDefaultAddress(targetAddressId); + } catch { + alert('배송지는 저장되었으나, 기본 배송지 설정에 실패했습니다.'); + } }
92-96: 전화번호 정규식이 비일관적 하이픈 패턴을 허용합니다.
/^01[0-9]-?\d{3,4}-?\d{4}$/는010-12345678(하이픈 하나만)이나01012345678(하이픈 없음) 모두 통과합니다. 의도된 동작이라면 무방하나, 일관된 포맷을 강제하려면submit시 하이픈 제거 후 검증하는 방식이 더 견고합니다.
19-25: 라이브러리에서Address타입을 직접 import하세요.
react-daum-postcode가 export하는Address타입을 사용하면 라이브러리 업데이트 시 타입 동기화 문제를 피할 수 있습니다.♻️ 수정 제안
-interface PostcodeData { - address: string; - addressType: string; - bname: string; - buildingName: string; - zonecode: string; -} +import type { Address as PostcodeData } from 'react-daum-postcode';src/app/address/[addressId]/page.tsx (1)
14-19: 전체 배송지 목록 조회 후 필터링 — 개별 조회 API 도입 검토.
getAddresses()로 전체 목록을 가져온 뒤find()로 단일 주소를 찾고 있습니다. 배송지 수가 적다면 문제없으나, 개별 주소 조회 엔드포인트(GET /addresses/:id)가 있다면 네트워크 비용을 줄일 수 있습니다.
| const [orderDetail, addresses] = await Promise.all([ | ||
| getOrderDetail(numericId), | ||
| getAddresses(), | ||
| ]); | ||
| const defaultAddress = addresses.find((addr) => addr.isDefault) || null; |
There was a problem hiding this comment.
getAddresses() 실패 시 취소 페이지 전체가 렌더링 불가
Promise.all은 하나라도 reject되면 전체가 실패합니다. 배송지 API 장애 시 주문 취소 페이지 자체에 접근할 수 없게 됩니다. 배송지 정보는 WRONG_ADDRESS 사유에서만 필요하므로, 주문 취소의 핵심 기능을 보호해야 합니다.
🛡️ getAddresses 실패를 graceful하게 처리
- const [orderDetail, addresses] = await Promise.all([
- getOrderDetail(numericId),
- getAddresses(),
- ]);
- const defaultAddress = addresses.find((addr) => addr.isDefault) || null;
+ const [orderDetail, addresses] = await Promise.all([
+ getOrderDetail(numericId),
+ getAddresses().catch(() => [] as Awaited<ReturnType<typeof getAddresses>>),
+ ]);
+ const defaultAddress = addresses.find((addr) => addr.isDefault) || null;이렇게 하면 배송지 fetch 실패 시 빈 배열로 fallback되어 defaultAddress가 null이 되고, CancelForm 내부의 address step에서 클라이언트 측 re-fetch가 동작합니다.
🤖 Prompt for AI Agents
In `@src/app/orders/`[orderId]/cancel/page.tsx around lines 29 - 33, Promise.all
currently causes the whole page to fail if getAddresses rejects; change the
fetch so getOrderDetail(numericId) is awaited normally and getAddresses() is
wrapped to gracefully handle errors (e.g., try/catch or Promise.allSettled) and
fall back to an empty array on failure so defaultAddress becomes null; keep
references to getOrderDetail, getAddresses, numericId, defaultAddress and leave
CancelForm's client-side address re-fetch intact so the cancel flow still
renders even when addresses API is down.
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/components/product/size/body-info-form.tsx (1)
182-182: 🛠️ Refactor suggestion | 🟠 Major
export default function패턴 위반코딩 가이드라인에 따르면
src/components/**/*.tsx파일은export default function패턴을 사용해야 합니다. 현재 named export로 선언되어 있습니다.-export function BodyInfoForm({ initialData, onSuccess }: BodyInfoFormProps) { +export default function BodyInfoForm({ initialData, onSuccess }: BodyInfoFormProps) {
wrapper.tsx의 import도 함께 수정 필요:-import { BodyInfoForm } from '@/components/product/size/body-info-form'; +import BodyInfoForm from '@/components/product/size/body-info-form';As per coding guidelines: "export default function 패턴 (화살표 함수 금지)"
🤖 Fix all issues with AI agents
In `@src/app/actions/user.ts`:
- Around line 50-51: The code reads session = await auth() and then uses
session?.accessToken, which allows requests to proceed without a session; add an
early validation right after auth() (using the session and accessToken symbols)
to fail fast if session is null/undefined — e.g., throw or return a clear
unauthorized error (or redirect) before any network request so you never send a
request without an Authorization header.
In `@src/app/address/page.tsx`:
- Around line 21-34: The resolvedAddresses mapping currently sets
deliveryRequest to myAddress.shippingDetail.deliveryRequest ||
address.deliveryRequest but lacks the final || '' fallback used in
[addressId]/page.tsx; update the deliveryRequest assignment inside the
addresses.map in resolvedAddresses (and/or extract a shared util) so it matches
the other file by ensuring deliveryRequest falls back to
myAddress.shippingDetail.deliveryRequest || address.deliveryRequest || '' (or
use a shared function that returns the same normalized value) and reference the
resolvedAddresses, addresses.map, deliveryRequest, myAddress, and
shippingDetail.addressId symbols when making the change.
In `@src/app/me/edit/page.tsx`:
- Around line 49-53: The body info display is missing units for height and
weight; update the JSX in src/app/me/edit/page.tsx where bodyInfo.height and
bodyInfo.weight are rendered (the <p> lines using bodyInfo.height,
bodyInfo.weight, usualTopSize, usualBottomSize, usualShoeSize) to append the
appropriate units ("cm" for height and "kg" for weight), and ensure you handle
null/undefined values gracefully (e.g., fall back to a placeholder like "-" or
"N/A" when bodyInfo.height/bodyInfo.weight is not present) so the UI always
shows a clear value with units.
In `@src/components/address/address-form.tsx`:
- Around line 98-128: The save flow is non-atomic: if updateAddress or
registerAddress succeeds but setAsDefaultAddress fails, the current code treats
it as a full failure; update the try/catch to detect and handle partial success
by capturing the result of the address save (use targetAddressId from
registerAddress/updateAddress) and then wrapping setAsDefaultAddress in its own
try/catch so a failure there does not revert or trigger a full error path—on
setAsDefaultAddress failure show a distinct alert like "주소는 저장되었으나 기본 배송지 설정에
실패했습니다." (while still calling router.back() and setIsSubmitting(false)), and
ensure the original catch only handles failures of the primary save
(registerAddress/updateAddress) so users won’t accidentally re-create the same
address on retry; reference functions/vars: registerAddress, updateAddress,
setAsDefaultAddress, targetAddressId, isDefaultAddress, isEditMode, alert,
router.back, setIsSubmitting.
In `@src/components/mypage/edit-profile-photo-sheet.tsx`:
- Around line 73-79: remotePatterns in next.config is using a placeholder
(example.com) and may not include the actual host used by profile images;
replace the TODO placeholder with the real image host, and ensure the host
returned by updateProfileImageAction (the profileUrl value) is covered by
remotePatterns so the Next/Image component rendering currentImageUrl will load
correctly; verify the exact hostname(s) (and port/subdomain if any) from
updateProfileImageAction and add matching entries to remotePatterns.
In `@src/components/product/size/body-info-form.tsx`:
- Around line 99-105: The component in this file is currently a named export;
change it to a default export by converting the exported declaration that
renders the input (the function/const that contains the JSX with
value={field.value} and onChange={event => field.onChange(event.target.value)})
into an "export default function ComponentName(props) { ... }" form, keeping the
implementation (including field.onChange usage and props) unchanged, and update
any imports elsewhere to use the default import.
🧹 Nitpick comments (11)
src/components/address/address-list.tsx (1)
16-22:addressesprop 변경 시selectedAddressId가 동기화되지 않음
useMemo는addresses가 바뀔 때 새 값을 계산하지만,useState의 초기값은 최초 마운트 시에만 사용됩니다.router.refresh()등으로 부모 서버 컴포넌트가 재렌더링되어 새addresses가 내려와도 선택 상태는 이전 값에 머무릅니다.
useEffect로 동기화하거나, 부모에서key={...}를 통해 리마운트를 유도하는 방안을 고려해 주세요.♻️ useEffect 동기화 예시
+ import { useEffect, useMemo, useState } from 'react'; const initialSelectedAddressId = useMemo( () => addresses.find((addr) => addr.isDefault)?.addressId ?? null, [addresses], ); const [selectedAddressId, setSelectedAddressId] = useState<number | null>( initialSelectedAddressId, ); + useEffect(() => { + setSelectedAddressId(initialSelectedAddressId); + }, [initialSelectedAddressId]);src/components/ui/close-button.tsx (1)
29-40: Named export 사용 — 코딩 가이드라인 확인 필요
src/components/**/*.tsx에 대해export default function패턴이 요구되지만, 이 파일은CloseButton과CloseXButton두 컴포넌트를 named export하고 있습니다. 하나의 파일에 두 컴포넌트가 공존하므로 default export가 불가능한 상황이긴 하나, 가이드라인 준수를 위해 파일 분리를 고려해 주세요.As per coding guidelines,
src/components/**/*.tsx:export default function패턴 (화살표 함수 금지).Also applies to: 42-53
src/components/address/shipping-info-card-fetcher.tsx (1)
17-20:getAddresses()실패 시 에러 바운더리로 전파됨
getMyAddress()는.catch(() => null)로 보호되어 있지만,getAddresses()는 예외가 그대로 throw됩니다. 배송지 목록 조회 실패 시 전체 페이지가 에러 상태로 전환될 수 있습니다.이 컴포넌트를 사용하는 곳에서
<Suspense>+ error boundary로 감싸거나,getAddresses()에도 fallback 처리를 추가하는 것을 권장합니다.Based on learnings: "Use Server Components for data fetching whenever possible. Prefetch critical data but wrap secondary/dynamic data in
Suspenseto avoid blocking the initial shell."src/components/mypage/edit-profile-photo-sheet.tsx (1)
37-55: 업로드 전 파일 크기/타입 검증 누락
handleUpload에서 서버로 전송하기 전에 파일 크기나 타입에 대한 클라이언트 사이드 검증이 없습니다. 대용량 파일 업로드 시 불필요한 네트워크 비용이 발생하고 서버 에러로 이어질 수 있습니다.♻️ 클라이언트 사이드 검증 추가 예시
+ const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp']; + const handleUpload = async (file: File | null) => { if (!file) return; + + if (!ALLOWED_TYPES.includes(file.type)) { + alert('JPG, PNG, WebP 형식의 이미지만 업로드 가능합니다.'); + return; + } + if (file.size > MAX_FILE_SIZE) { + alert('5MB 이하의 이미지만 업로드 가능합니다.'); + return; + } setIsUploading(true);src/app/me/edit/page.tsx (1)
39-42:ShippingInfoCardFetcher를<Suspense>로 감싸는 것을 권장
ShippingInfoCardFetcher는 내부에서 두 개의 API를 호출하는 async 서버 컴포넌트입니다. Suspense 없이 사용하면 배송지 데이터 로딩이 전체 페이지 렌더링을 블로킹합니다.♻️ Suspense 래핑 예시
+ import { Suspense } from 'react'; - <ShippingInfoCardFetcher - actionHref="/address?mode=manage" - actionLabel="배송지 수정하기" - /> + <Suspense fallback={<div className="h-48 animate-pulse rounded-xl bg-gray-100" />}> + <ShippingInfoCardFetcher + actionHref="/address?mode=manage" + actionLabel="배송지 수정하기" + /> + </Suspense>Based on learnings: "Prefetch critical data but wrap secondary/dynamic data in
Suspenseto avoid blocking the initial shell."src/app/actions/user.ts (1)
16-22:normalizeUserPoints가 입력 객체를 직접 변이(mutate)인자로 받은
user객체의points를 직접 수정하고 있습니다. 서버 액션 내부에서만 사용되므로 현재 문제는 없지만, 순수 함수로 작성하면 의도가 더 명확해집니다.♻️ 불변 방식
function normalizeUserPoints(user: UserInfoResDto): UserInfoResDto { const rawPoints: unknown = user.points; if (typeof rawPoints === 'string') { - user.points = Number(rawPoints.replace(/,/g, '')) || 0; + return { ...user, points: Number(rawPoints.replace(/,/g, '')) || 0 }; } return user; }src/components/address/address-form.tsx (1)
91-95: 전화번호 정규식이 하이픈 포함 상태를 허용하지만 정규화 없이 전송
/^01[0-9]-?\d{3,4}-?\d{4}$/는010-1234-5678과01012345678모두 통과시키지만, 서버에 전송할 때 하이픈 제거 등의 정규화를 하지 않습니다. 서버 API가 특정 포맷을 기대한다면 불일치가 발생할 수 있습니다.src/app/address/[addressId]/page.tsx (2)
25-36:resolvedAddress병합 로직이address/page.tsx와 중복됨
src/app/address/page.tsx(lines 21-34)에도 동일한deliveryRequest병합 로직이 존재합니다. 공통 유틸 함수로 추출하면 유지보수가 편해집니다.♻️ 유틸 추출 제안
예:
src/utils/address.tsexport function resolveDeliveryRequest( address: AddressItem, myAddress: AddressResponseData | null, ): AddressItem { if ( address.isDefault && myAddress?.hasShippingInfo && myAddress.shippingDetail.addressId === address.addressId ) { return { ...address, deliveryRequest: myAddress.shippingDetail.deliveryRequest || address.deliveryRequest || '', }; } return address; }
1-49: 이 라우트 세그먼트에loading.tsx누락서버 컴포넌트에서
getAddresses()+getMyAddress()를 await하므로 네트워크 지연 시 사용자에게 빈 화면이 노출됩니다.src/app/address/[addressId]/loading.tsx를 추가하세요. Based on learnings: "Always provide aloading.tsxfor route segments and granular Fallbacks for components."src/app/me/edit/body-info/_components/wrapper.tsx (1)
1-22:/src/app내 컴포넌트 파일 배치프로젝트 규칙에 따르면
/src/app디렉토리에는page.tsx,layout.tsx,loading.tsx,error.tsx만 허용됩니다._components접두사는 Next.js 라우팅에서 제외되지만, 규칙 준수를 위해src/components/me/등으로 이동을 검토해 주세요. Based on learnings: "Do not make component files inside /src/app directory. Only allow page.tsx, layout.tsx, loading.tsx, and error.tsx in /src/app."src/app/me/edit/body-info/page.tsx (1)
5-24: LGTM — 구조 깔끔서버 컴포넌트에서 데이터 fetch 후 클라이언트 wrapper에 전달하는 패턴이 적절합니다. 다만 이 라우트 세그먼트에도
loading.tsx추가를 권장합니다. Based on learnings: "Always provide aloading.tsxfor route segments."
| const session = await auth(); | ||
| const accessToken = session?.accessToken as string | undefined; |
There was a problem hiding this comment.
인증 세션 없이 요청 진행 가능
session이 null이면 accessToken도 undefined가 되어 Authorization 헤더 없이 요청이 전송됩니다. 서버에서 401로 거부하겠지만, 불필요한 네트워크 요청 전에 조기 실패하는 것이 더 명확합니다.
♻️ 조기 검증 추가
const session = await auth();
- const accessToken = session?.accessToken as string | undefined;
+ if (!session?.accessToken) {
+ throw new Error('로그인이 필요합니다.');
+ }
+ const accessToken = session.accessToken;📝 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 session = await auth(); | |
| const accessToken = session?.accessToken as string | undefined; | |
| const session = await auth(); | |
| if (!session?.accessToken) { | |
| throw new Error('로그인이 필요합니다.'); | |
| } | |
| const accessToken = session.accessToken; |
🤖 Prompt for AI Agents
In `@src/app/actions/user.ts` around lines 50 - 51, The code reads session = await
auth() and then uses session?.accessToken, which allows requests to proceed
without a session; add an early validation right after auth() (using the session
and accessToken symbols) to fail fast if session is null/undefined — e.g., throw
or return a clear unauthorized error (or redirect) before any network request so
you never send a request without an Authorization header.
| const resolvedAddresses = addresses.map((address) => { | ||
| if ( | ||
| address.isDefault && | ||
| myAddress?.hasShippingInfo && | ||
| myAddress.shippingDetail.addressId === address.addressId | ||
| ) { | ||
| return { | ||
| ...address, | ||
| deliveryRequest: | ||
| myAddress.shippingDetail.deliveryRequest || address.deliveryRequest, | ||
| }; | ||
| } | ||
| return address; | ||
| }); |
There was a problem hiding this comment.
deliveryRequest 폴백 값이 [addressId]/page.tsx와 불일치
[addressId]/page.tsx에서는 || '' 폴백이 추가되어 있지만 이 파일에서는 누락되어 있습니다. 두 곳의 동작을 통일하세요 (앞서 제안한 공통 유틸 추출로 해결 가능).
deliveryRequest:
- myAddress.shippingDetail.deliveryRequest || address.deliveryRequest,
+ myAddress.shippingDetail.deliveryRequest || address.deliveryRequest || '',📝 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 resolvedAddresses = addresses.map((address) => { | |
| if ( | |
| address.isDefault && | |
| myAddress?.hasShippingInfo && | |
| myAddress.shippingDetail.addressId === address.addressId | |
| ) { | |
| return { | |
| ...address, | |
| deliveryRequest: | |
| myAddress.shippingDetail.deliveryRequest || address.deliveryRequest, | |
| }; | |
| } | |
| return address; | |
| }); | |
| const resolvedAddresses = addresses.map((address) => { | |
| if ( | |
| address.isDefault && | |
| myAddress?.hasShippingInfo && | |
| myAddress.shippingDetail.addressId === address.addressId | |
| ) { | |
| return { | |
| ...address, | |
| deliveryRequest: | |
| myAddress.shippingDetail.deliveryRequest || address.deliveryRequest || '', | |
| }; | |
| } | |
| return address; | |
| }); |
🤖 Prompt for AI Agents
In `@src/app/address/page.tsx` around lines 21 - 34, The resolvedAddresses mapping
currently sets deliveryRequest to myAddress.shippingDetail.deliveryRequest ||
address.deliveryRequest but lacks the final || '' fallback used in
[addressId]/page.tsx; update the deliveryRequest assignment inside the
addresses.map in resolvedAddresses (and/or extract a shared util) so it matches
the other file by ensuring deliveryRequest falls back to
myAddress.shippingDetail.deliveryRequest || address.deliveryRequest || '' (or
use a shared function that returns the same normalized value) and reference the
resolvedAddresses, addresses.map, deliveryRequest, myAddress, and
shippingDetail.addressId symbols when making the change.
| <p>키: {bodyInfo.height}</p> | ||
| <p>몸무게: {bodyInfo.weight}</p> | ||
| <p>상의: {bodyInfo.usualTopSize}</p> | ||
| <p>하의: {bodyInfo.usualBottomSize}</p> | ||
| <p>신발 사이즈: {bodyInfo.usualShoeSize}</p> |
There was a problem hiding this comment.
체형 정보 표시 시 단위 누락
키와 몸무게 값에 단위(cm, kg)가 표시되지 않아 사용자가 값의 의미를 파악하기 어렵습니다.
♻️ 단위 추가
- <p>키: {bodyInfo.height}</p>
- <p>몸무게: {bodyInfo.weight}</p>
+ <p>키: {bodyInfo.height}cm</p>
+ <p>몸무게: {bodyInfo.weight}kg</p>📝 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>키: {bodyInfo.height}</p> | |
| <p>몸무게: {bodyInfo.weight}</p> | |
| <p>상의: {bodyInfo.usualTopSize}</p> | |
| <p>하의: {bodyInfo.usualBottomSize}</p> | |
| <p>신발 사이즈: {bodyInfo.usualShoeSize}</p> | |
| <p>키: {bodyInfo.height}cm</p> | |
| <p>몸무게: {bodyInfo.weight}kg</p> | |
| <p>상의: {bodyInfo.usualTopSize}</p> | |
| <p>하의: {bodyInfo.usualBottomSize}</p> | |
| <p>신발 사이즈: {bodyInfo.usualShoeSize}</p> |
🤖 Prompt for AI Agents
In `@src/app/me/edit/page.tsx` around lines 49 - 53, The body info display is
missing units for height and weight; update the JSX in src/app/me/edit/page.tsx
where bodyInfo.height and bodyInfo.weight are rendered (the <p> lines using
bodyInfo.height, bodyInfo.weight, usualTopSize, usualBottomSize, usualShoeSize)
to append the appropriate units ("cm" for height and "kg" for weight), and
ensure you handle null/undefined values gracefully (e.g., fall back to a
placeholder like "-" or "N/A" when bodyInfo.height/bodyInfo.weight is not
present) so the UI always shows a clear value with units.
| try { | ||
| let targetAddressId: number | null = null; | ||
|
|
||
| if (isEditMode && initialData) { | ||
| const updatePayload: AddressRequest = { ...formState }; | ||
| if (!isDeliveryRequestTouched) { | ||
| delete updatePayload.deliveryRequest; | ||
| } | ||
|
|
||
| await updateAddress(initialData.addressId, updatePayload); | ||
| targetAddressId = initialData.addressId; | ||
| } else { | ||
| const response = await registerAddress(formState); | ||
| targetAddressId = response.shippingDetail.addressId; | ||
| } | ||
|
|
||
| if (isDefaultAddress && targetAddressId != null) { | ||
| await setAsDefaultAddress(targetAddressId); | ||
| } | ||
|
|
||
| alert( | ||
| isEditMode | ||
| ? '배송지 정보가 수정되었습니다.' | ||
| : '배송지 정보가 등록되었습니다.', | ||
| ); | ||
| router.back(); | ||
| } catch (error) { | ||
| alert(error instanceof Error ? error.message : '오류가 발생했습니다.'); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } |
There was a problem hiding this comment.
주소 저장과 기본 배송지 설정이 비원자적(non-atomic)
updateAddress/registerAddress 성공 후 setAsDefaultAddress가 실패하면, 주소는 저장되었지만 에러 alert가 표시됩니다. 사용자는 저장 실패로 인식하고 재시도할 수 있으며, 이 경우 중복 등록(create 모드)이 발생할 수 있습니다.
최소한 에러 메시지에서 "주소는 저장되었으나 기본 배송지 설정에 실패했습니다"와 같이 구분하거나, catch 블록에서 부분 성공을 처리하는 것을 고려해 주세요.
🤖 Prompt for AI Agents
In `@src/components/address/address-form.tsx` around lines 98 - 128, The save flow
is non-atomic: if updateAddress or registerAddress succeeds but
setAsDefaultAddress fails, the current code treats it as a full failure; update
the try/catch to detect and handle partial success by capturing the result of
the address save (use targetAddressId from registerAddress/updateAddress) and
then wrapping setAsDefaultAddress in its own try/catch so a failure there does
not revert or trigger a full error path—on setAsDefaultAddress failure show a
distinct alert like "주소는 저장되었으나 기본 배송지 설정에 실패했습니다." (while still calling
router.back() and setIsSubmitting(false)), and ensure the original catch only
handles failures of the primary save (registerAddress/updateAddress) so users
won’t accidentally re-create the same address on retry; reference
functions/vars: registerAddress, updateAddress, setAsDefaultAddress,
targetAddressId, isDefaultAddress, isEditMode, alert, router.back,
setIsSubmitting.
| <Image | ||
| src={currentImageUrl} | ||
| alt="프로필 이미지" | ||
| width={96} | ||
| height={96} | ||
| className="h-full w-full object-cover" | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find next.config file
fd "next.config" --type fRepository: IT-Cotato/12th-OnGil-FE
Length of output: 81
🏁 Script executed:
# Read the component file to understand the context
cat -n src/components/mypage/edit-profile-photo-sheet.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 5489
🏁 Script executed:
# Check for any images configuration in tsconfig or other config files
fd "next\.config|tsconfig|package\.json" --type f -e js -e ts -e json | head -20Repository: IT-Cotato/12th-OnGil-FE
Length of output: 108
🏁 Script executed:
# Read next.config.ts to check images configuration
cat -n next.config.tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 2521
🏁 Script executed:
# Also check if there are any environment files that might define the image domain
fd "\.env" --type fRepository: IT-Cotato/12th-OnGil-FE
Length of output: 49
next.config의 remotePatterns 설정 불완전 — 실제 API 도메인 확인 필요
remotePatterns가 구성되어 있으나, line 17에 example.com에 대한 TODO 주석이 있습니다. 프로필 이미지 호스팅 서비스의 실제 API 도메인으로 교체하고, updateProfileImageAction이 반환하는 profileUrl의 호스트가 remotePatterns에 정확히 등록되어 있는지 확인하세요.
🤖 Prompt for AI Agents
In `@src/components/mypage/edit-profile-photo-sheet.tsx` around lines 73 - 79,
remotePatterns in next.config is using a placeholder (example.com) and may not
include the actual host used by profile images; replace the TODO placeholder
with the real image host, and ensure the host returned by
updateProfileImageAction (the profileUrl value) is covered by remotePatterns so
the Next/Image component rendering currentImageUrl will load correctly; verify
the exact hostname(s) (and port/subdomain if any) from updateProfileImageAction
and add matching entries to remotePatterns.
| <input | ||
| type="number" | ||
| inputMode="numeric" | ||
| step="1" | ||
| placeholder={placeholder} | ||
| value={field.value ?? ''} | ||
| onChange={(event) => field.onChange(event.target.value)} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# body-info 관련 파일 찾기
fd "body-info" --extension ts --extension tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 253
🏁 Script executed:
#!/bin/bash
# height/weight 필드 및 z.number/z.coerce 확인
rg "height|weight" --type ts --type tsx -B 2 -A 2 | head -50Repository: IT-Cotato/12th-OnGil-FE
Length of output: 94
🏁 Script executed:
#!/bin/bash
# 컴포넌트 파일의 export 패턴 및 use client 확인
head -20 src/components/product/size/body-info-form.tsx
tail -20 src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 1072
🏁 Script executed:
#!/bin/bash
# body-info 스키마 파일 읽기
cat -n src/schemas/body-info.tsRepository: IT-Cotato/12th-OnGil-FE
Length of output: 789
🏁 Script executed:
#!/bin/bash
# body-info-form.tsx 파일 크기 확인
wc -l src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 117
🏁 Script executed:
#!/bin/bash
# body-info-form.tsx 98-110 라인 확인 (lines 99-105)
sed -n '95,110p' src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 652
🏁 Script executed:
#!/bin/bash
# body-info-form.tsx export 패턴 확인
grep -n "^export\|^const\|^function" src/components/product/size/body-info-form.tsx | tail -5Repository: IT-Cotato/12th-OnGil-FE
Length of output: 292
🏁 Script executed:
#!/bin/bash
# export 패턴 확인
grep -n "export.*BodyInfoForm\|export default" src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 148
🏁 Script executed:
#!/bin/bash
# 'any' 타입 검색
grep -n "any" src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 49
🏁 Script executed:
#!/bin/bash
# BodyInfoForm 함수 선언 전후 라인 확인 (178-185)
sed -n '178,185p' src/components/product/size/body-info-form.tsxRepository: IT-Cotato/12th-OnGil-FE
Length of output: 333
export default function 패턴으로 변경
현재 named export를 사용 중인데, 코딩 가이드라인에 따라 export default function으로 변경해야 합니다.
참고: 숫자 필드의 string 값 전달은 문제없습니다. 스키마에서 z.coerce.number()를 사용하고 있어 Zod이 자동으로 문자열을 숫자로 변환하므로, 현재 field.onChange(event.target.value) 구현은 올바릅니다.
🤖 Prompt for AI Agents
In `@src/components/product/size/body-info-form.tsx` around lines 99 - 105, The
component in this file is currently a named export; change it to a default
export by converting the exported declaration that renders the input (the
function/const that contains the JSX with value={field.value} and
onChange={event => field.onChange(event.target.value)}) into an "export default
function ComponentName(props) { ... }" form, keeping the implementation
(including field.onChange usage and props) unchanged, and update any imports
elsewhere to use the default import.
📝 개요
이번 PR은 마이페이지 내 정보 수정 플로우와 배송지 관리 플로우를 중심으로, 실제 사용 중 발생하던 데이터 불일치/초기화 문제를 해결하고 화면 동작을 역할별로 분리한 작업입니다.
핵심 목표:
/me/edit,/me/edit/body-info)관련 이슈 번호: #이슈번호
🚀 주요 변경 사항
1) 마이페이지 내 정보 수정 플로우 분리
/me/edit페이지 신설/me/edit/body-info페이지 신설/me/edit로 복귀하도록 wrapper 분리CloseButton)을 경로 기반 이동 지원으로 보강해서 히스토리 꼬임 완화2) 프로필 이미지 수정 UI/서버 액션 추가
updateProfileImageAction)revalidatePath) 처리3) 체형 정보 입력 안정화
BodyInfoForm에서 키/몸무게 입력 반영 누락되던 이슈 수정body-info/page,body-info-modal, 각 wrapper)4) 배송지 수정 데이터 유실 이슈 수정
deliveryRequest(요청사항) untouched 상태에서 잘못 덮어쓰던 문제 수정deliveryRequest보정 로직 추가5) 주소 목록 화면 모드 분리
/address를 모드 기반으로 분리mode=manage: 내정보 수정 진입용(선택 버튼 숨김)address-list/address-page-footer컴포넌트 추가6) 주문 취소 주소 단계 안정화
getAddresses()실패 시 빈 배열 fallback 처리🧪 테스트/검증 포인트
/me/edit진입 가능/address?mode=manage이동/me/edit복귀/address기본 모드: 선택 버튼 노출/address?mode=manage: 선택 버튼 숨김, 수정 버튼만 노출/orders/[orderId]/cancel화면 렌더링 유지되는지 확인next.config.tsremotePatterns) 관련 정리는 후속 작업으로 분리 예정✅ 체크리스트
이슈 해결 여부
Closes #이슈번호