Skip to content

Refactor/cart#59

Merged
Seoje1405 merged 14 commits intodevelopfrom
refactor/cart
Feb 12, 2026
Merged

Refactor/cart#59
Seoje1405 merged 14 commits intodevelopfrom
refactor/cart

Conversation

@Seoje1405
Copy link
Copy Markdown
Contributor

📝 개요

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

🚀 주요 변경 사항

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

📸 스크린샷 (선택)

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

✅ 체크리스트

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

이슈 해결 여부

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 12, 2026

Walkthrough

서버 액션으로 데이터 페칭과 에러 재던지기를 중앙화(rethrowNextError)하고, 카트 UI를 컴포넌트 단위로 분리·옵션 편집(바텀시트)·디바운스 수량 입력을 추가했으며, 상품 상세는 관련 데이터(Promise.all) 병렬 조회와 도메인 타입으로 마이그레이션했습니다. (50자 이내)

Changes

Cohort / File(s) Summary
에러 처리 중앙화
src/lib/server-action-utils.ts, src/app/actions/...
src/app/actions/body-info.ts, src/app/actions/cart.ts, src/app/actions/order.ts, src/app/actions/product.ts, src/app/actions/user.ts, src/app/actions/wishlist.ts, src/components/cart/cart-data-fetcher.tsx, src/app/cart/page.tsx
catch 블록의 개별 digest 검사 로직을 rethrowNextError(error)로 통일. Next.js 내부 에러 재던지기 유틸 및 ActionResult 타입 추가.
카트 UI 분해·신규 컴포넌트
src/components/cart/cart-views.tsx (삭제), src/components/cart/cart-header-control.tsx, src/components/cart/cart-item-list.tsx, src/components/cart/cart-summary-footer.tsx, src/components/cart/cart-item.tsx, src/components/cart/cart-option-change-sheet.tsx
카트 뷰를 헤더/아이템리스트/푸터로 분리. CartItem에 디바운스 입력 도입, 옵션 변경 훅업 및 옵션 변경용 바텀시트 추가. 기존 통합 모듈 제거.
카트 서비스 로직 변경
src/components/cart/use-cart-service.ts
옵션 업데이트 액션(UPDATE_OPTION) 추가, handleOptionChange 공개, 낙관적 업데이트·실패시 새로고침 패턴 통일, 체크아웃을 클라이언트 네비게이션으로 변경(결제 경로, 쿼리 직렬화).
서버 액션 사용으로 마이그레이션된 바디 인포
src/app/@modal/(.)body-info/page.tsx, src/app/body-info/page.tsx, src/app/@modal/(.)body-info/_components/wrapper.tsx
모의 fetch 제거, getMyBodyInfoAction() 사용으로 action 결과(result.success / data.hasBodyInfo)를 기반해 userInfo 유도. 타입 임포트 경로 조정.
상품 상세·옵션/병렬 데이터 조회
src/app/actions/product.ts, src/app/product/[id]/layout.tsx, src/app/product/[id]/page.tsx, src/components/product/*, src/components/product/descriptions/*
getProductDetail, getSimilarProducts, getProductOptions 추가. 상세 페이지에서 bodyInfo, wishlist, similarProducts 등을 Promise.all로 병렬 호출. 관련 컴포넌트 prop 타입·export 변경, 이미지 렌더링에 src 존재 가드 추가.
도메인 타입 추가 및 마이그레이션
src/types/domain/size.ts, src/components/product/size/*, src/app/@modal/(.)body-info/_components/wrapper.tsx
UserBodyInfo, SizeAnalysisResult 타입 추가 및 기존 mock 타입 사용처를 도메인 타입으로 전환(임포트 경로 변경).
유틸·목데이터 변경
src/lib/format.ts, src/mocks/product-data.ts, src/mocks/order-data.ts
한국 로케일 가격 포맷 formatPrice 추가. generateDummyProducts에 deterministic startId 도입. placeholderOrderData 삭제.
리팩터: export 형태 변경
src/components/product/index.tsx, src/components/product/descriptions/*, src/components/cart/cart-data-fetcher.tsx, src/components/cart/cart-item.tsx, src/app/layout.tsx
일부 컴포넌트의 named ↔ default export 변환(재내보내기 조정). CartDataFetcher와 CartItem의 export 형태 변경으로 호출부 점검 필요.
제품 옵션 관련 훅 개선
src/components/product-option-sheet/use-product-option.ts
addItemToSelection을 useCallback으로 메모이제이션, 더미 옵션 제거, SOLD_OUT 가드 로직 보강.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as 사용자(브라우저)
participant Sheet as CartOptionChangeSheet
participant Hook as useCartService
participant Action as updateBodyCartOptionAction
participant Router as NextRouter
User->>Sheet: 옵션 선택 (색상/사이즈) → Change
Sheet->>Hook: onConfirm(cartId, color, size)
Hook->>Hook: 낙관적 UI 업데이트 (UPDATE_OPTION)
Hook->>Action: 서버 액션 호출 (update option)
Action-->>Hook: 성공 / 실패 응답
alt 성공
Hook-->>Sheet: 변경 성공, 닫기
else 실패
Hook->>Router: 새로고침 또는 경로 재요청
Hook-->>Sheet: 실패 메시지 전달
end

(컴포넌트: CartOptionChangeSheet → 훅(useCartService) → 서버 액션(update) → 클라이언트 라우터 처리)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Refactor/product detail api #50: body-info 액션 및 도메인 타입 추가/변경과 직접 중복 — getMyBodyInfoAction, size 타입 관련 수정이 겹칩니다.
  • Feature/cartt #51: 카트 서브시스템 리팩터(컴포넌트 분할·use-cart-service 변경)와 코드 레벨 중복 가능성이 높습니다.
  • Feat/heart-pay api #53: 서버 액션들의 에러 처리(특히 cart/wishlist/order actions) 관련 변경과 연관성이 큽니다.
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명은 템플릿만 포함하고 실제 변경 사항, 관련 이슈 번호, 구체적인 개선 내용이 명시되어 있지 않습니다. 개요, 주요 변경 사항 목록, 관련 이슈 번호를 구체적으로 작성하고 체크리스트를 완료해주세요. 예: 'Closes #이슈번호' 형식으로 이슈를 연결하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 65.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 'Refactor/cart'는 변경 사항의 핵심 내용인 장바구니 컴포넌트 분리 및 에러 처리 리팩토링을 명확히 반영하고 있습니다.

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

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

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (3)
src/components/product/product-image-slider.tsx (1)

18-18: 🛠️ Refactor suggestion | 🟠 Major

코딩 가이드라인 위반: export default function 패턴 미사용

🔧 수정 제안
-export function ProductImageSlider({ imageUrl }: ProductImageSliderProps) {
+export default function ProductImageSlider({ imageUrl }: ProductImageSliderProps) {

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

src/app/actions/order.ts (1)

63-93: ⚠️ Potential issue | 🟠 Major

fetchCartOrderItems / fetchDirectOrderItems 비동기 에러 미처리

fetchCartOrderItemsgetCartItems()를 (라인 75), fetchDirectOrderItemsgetProductDetail()을 (라인 114) 호출할 때 try/catch가 없습니다. 이들 함수가 throw하면 에러가 그대로 전파되어 서버 액션을 거쳐 클라이언트에 내부 에러 메시지가 노출됩니다.

같은 파일의 다른 함수들은 rethrowNextError() 패턴으로 에러를 처리하므로 (라인 53-54), 이 두 함수도 동일한 패턴을 적용하거나 notFound()/redirect()로 적절히 처리해야 합니다. fetchDirectOrderItems는 이미 JSON 파싱 에러 처리가 있으나 API 호출 에러는 미처리된 상태입니다.

src/app/actions/cart.ts (1)

96-108: ⚠️ Potential issue | 🟡 Minor

getCartCount의 제네릭 타입과 런타임 체크가 불일치.

api.get<{ count: number }> 로 선언했지만 typeof data === 'number' 분기가 있어, 실제 응답 형태가 불확실함을 암시합니다. 제네릭 타입이 { count: number } 라면 number 분기는 도달 불가(dead code)이고, bare number가 올 수 있다면 제네릭을 number | { count: number }로 수정해야 합니다.

제안: 타입을 확정하고 불필요한 분기 제거

API가 { count: number } 만 반환하는 경우:

 export async function getCartCount(): Promise<number> {
   try {
     const data = await api.get<{ count: number }>('/carts/count');
-
-    if (typeof data === 'number') {
-      return data;
-    }
     return data.count;
   } catch (error) {
🤖 Fix all issues with AI agents
In `@src/app/cart/page.tsx`:
- Around line 6-8: 세 컴포넌트 CartHeaderControl, CartItemList, CartSummaryFooter가 현재
named export로 작성되어 있으니 각 컴포넌트 파일에서 `export function <Name>()` 패턴을 `export
default function <Name>()`로 변경하고 해당 파일의 any other named exports가 있다면 이름을 유지하되
default로 내보내는지 확인한 뒤, 페이지 파일의 import 문을 named import에서 기본(default) import로 바꿔
`import CartHeaderControl from '@/components/cart/cart-header-control'`, `import
CartItemList from '@/components/cart/cart-item-list'`, `import CartSummaryFooter
from '@/components/cart/cart-summary-footer'` 형태로 업데이트하세요; 컴포넌트 내부/외부 참조가 있다면
호출부(예: JSX 사용처)에서 변경된 export 방식이 정상 동작하는지 확인하세요.

In `@src/app/product/`[id]/page.tsx:
- Line 4: The import of the mock fetchSizeAnalysis from '@/mocks/size' remains
and is used in this file (fetchSizeAnalysis), but other data-fetching functions
(getProductDetail, getSimilarProducts, getMyBodyInfoAction, getMyWishlist) were
migrated to server actions; either migrate the size analysis logic into a server
action and replace fetchSizeAnalysis usage with that new action (e.g., create
getSizeAnalysisAction() and call it from this page), or if you intentionally
want to keep the mock, remove the import of fetchSizeAnalysis and add an
explicit comment near getSizeOptionsAction/usage explaining why the mock is
retained (and ensure test-only guards or feature flags prevent it from
shipping); update references accordingly to use the new server action name or
the documented mock retention rationale.

In `@src/components/cart/cart-data-fetcher.tsx`:
- Line 6: The component is using a named export; change it to the required
default function pattern by converting the declaration to a default exported
function named CartDataFetcher (i.e., use "export default function
CartDataFetcher(...)") so it follows the src/components/**/*.tsx guideline (do
not use an arrow function), and ensure any imports or usages referencing
CartDataFetcher are updated if they relied on a named import.

In `@src/components/cart/cart-header-control.tsx`:
- Line 7: The component CartHeaderControl is using a named export but must
follow the project pattern of a default-exported function; change its
declaration from "export function CartHeaderControl()" to "export default
function CartHeaderControl()" (do not convert to an arrow function) and update
any imports elsewhere to use the default import form (e.g., import
CartHeaderControl from '...') so references remain correct.

In `@src/components/cart/cart-item-list.tsx`:
- Line 27: The component currently uses a named export "CartItemList"; change
its declaration to the required default-export function pattern by making it an
export default function named CartItemList (not an arrow function) so it matches
the src/components/**/*.tsx guideline; update any imports that rely on the named
export to use the default import form if needed.

In `@src/components/cart/cart-item.tsx`:
- Line 22: 현재 컴포넌트는 named export인 `export function CartItem`로 선언되어 있어
가이드라인(`src/components/**/*.tsx`: export default function 패턴, 화살표 함수 금지)을 위반합니다;
`CartItem` 컴포넌트를 화살표 함수가 아닌 `export default function CartItem(...) { ... }` 형태로
변경하고 파일 최종부에 default export가 아닌 선언 자체가 기본 export가 되도록 수정해 주세요(즉 `export function
CartItem` → `export default function CartItem` 및 함수 선언식으로 변환).

In `@src/components/cart/cart-option-change-sheet.tsx`:
- Line 26: The component is exported using a named export "export function
CartOptionChangeSheet" but the project guideline requires the "export default
function" pattern for files under src/components/**/*.tsx and forbids arrow
functions; change the declaration to a default export function (e.g., "export
default function CartOptionChangeSheet(...)") instead of a named export so the
file follows the required export style and retains the same identifier for
internal use.
- Around line 101-106: The onClick handler currently calls onConfirm(...) and
then onClose() without awaiting, causing a race when onConfirm returns a
Promise; change the onClick handler to an async function that awaits
onConfirm(item.cartId, currentColor, currentSize) before calling onClose(), and
wrap the await in a try/catch to handle and log/rethrow errors (or surface them
via an error callback) so unhandled rejections are avoided; update the handler
referencing onClick, onConfirm, onClose, item, currentColor and currentSize
accordingly.

In `@src/components/cart/cart-summary-footer.tsx`:
- Line 8: Change the named export function CartSummaryFooter to the required
default-export pattern used across components: replace "export function
CartSummaryFooter" with a default export function declaration (export default
function CartSummaryFooter(...)) so it follows the "export default function"
guideline (matching the pattern used in cart-header-control.tsx); ensure the
function name remains CartSummaryFooter and update any imports that referenced
the named export to use the default import if needed.

In `@src/components/product-option-sheet/use-product-option.ts`:
- Around line 68-93: The addItemToSelection callback mutates an item object when
incrementing quantity (newItems[existingIdx].quantity += 1), violating
immutability; instead, create a new item object when updating: copy the prev
array, replace the item at existingIdx with a shallow-cloned object that has
quantity: old.quantity + 1, then return the new array; ensure this change is
made inside addItemToSelection (and references like setSelectedItems,
existingIdx, newItems, and option) so no original item object is mutated.

In `@src/components/product/descriptions/product-description.tsx`:
- Line 13: The component currently uses a named export ("export function
ProductDescription") which violates the guideline requiring the "export default
function" pattern for files under src/components; change the declaration to a
default function export (e.g., export default function ProductDescription(...) {
... }) instead of a named or arrow function, keep the same function name and
props signature, and update any internal references/imports if needed to use the
default export.

In `@src/components/product/descriptions/recommended-products-carousel.tsx`:
- Line 54: The key fallback uses the falsy-coalescing operator
(similarProduct.id || i) which treats id === 0 as falsy and falls back to index;
in the JSX for the RecommendedProductsCarousel component replace the logical OR
with the nullish coalescing operator so the key becomes based on
similarProduct.id when it is 0 (use similarProduct.id ?? i), locating the prop
where key={similarProduct.id || i} and updating it to use ?? to preserve 0 as a
valid id.
- Around line 23-25: Change the named export to a default export for the
component by updating the declaration to "export default function
RecommendedProductsCarousel(...)" in RecommendedProductsCarousel, then update
any imports: in product-description.tsx replace the named import (e.g. import {
RecommendedProductsCarousel } ...) with a default import (import
RecommendedProductsCarousel ...), and update the re-export in product/index.tsx
to export the default (e.g. export { default as RecommendedProductsCarousel }
from '...') so all references use the default export.

In `@src/components/product/product-image-slider.tsx`:
- Around line 55-67: The component currently renders empty CarouselItem slides
when image URLs are falsy; update product-image-slider to filter out falsy
entries before mapping (use images.filter(Boolean) or similar) and map over that
filtered array in the images.map block so only valid Image components render; if
the filtered array is empty, render a fallback UI (single placeholder
CarouselItem/Image or a “no image” placeholder) and ensure any counter or
pagination logic uses the filtered array length instead of the original
Array(5).fill(...) source so the slide count and visuals stay consistent.

In `@src/mocks/product-data.ts`:
- Around line 87-90: The code can produce reviewRating = 0.0 even when
reviewCount > 0 and never reaches 5.0; update the reviewRating computation (the
symbol reviewRating that uses startId and i and the derived raw value) so that
when reviewCount > 0 you clamp the computed rating to a minimum of 1.0 (e.g.,
compute the raw rating from (((startId + i) * 53 + 7) % 50) / 10 and then set
reviewRating = Math.max(1.0, parsedRaw) after rounding to one decimal), ensuring
no positive-review product gets 0.0 while preserving the existing distribution.
🧹 Nitpick comments (16)
src/components/product/product-info.tsx (1)

9-10: 중복되는 복합 타입 — 공통 유틸리티 타입으로 추출 권장

AI summary에 따르면 CompactProductHeader, ProductDescription, ProductDetailView 등 여러 컴포넌트에서 동일한 Omit + Partial<Pick> 패턴을 반복 사용하고 있습니다. product.ts에 공통 타입을 한 번만 선언하면 변경 시 한 곳만 수정하면 됩니다.

♻️ 제안: product.ts에 유틸리티 타입 추가

src/types/domain/product.ts에 추가:

/** 조회수/구매수/리뷰수가 선택적인 Product 타입 */
export type ProductSummary = Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> &
  Partial<Pick<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'>>;

이후 각 컴포넌트에서:

-  product: Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> &
-    Partial<Pick<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'>>;
+  product: ProductSummary;
#!/bin/bash
# 동일한 Omit+Partial+Pick 패턴이 다른 컴포넌트에서도 반복되는지 확인
rg -n "Omit<Product" --type=ts --type=tsx -g '*.tsx' -g '*.ts'
src/components/product/size/size-guide-section.tsx (1)

21-21: 코딩 가이드라인 위반: export default function 패턴 미사용

기존 코드이지만, 이 PR에서 수정하는 김에 export default function으로 전환하는 것을 권장합니다.

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

src/components/product/compact-product-header.tsx (1)

44-46: formatPrice 유틸 사용 권장

src/lib/format.tsformatPrice가 도입되었으므로, toLocaleString() 대신 통일된 포맷 유틸을 사용하면 일관성이 높아집니다.

♻️ 제안
+import { formatPrice } from '@/lib/format';
...
-            {product.finalPrice.toLocaleString()}원
+            {formatPrice(product.finalPrice)}원
src/app/cart/page.tsx (1)

16-24: 서버 컴포넌트 에러 처리 패턴 확인

rethrowNextError가 digest 에러를 재throw한 후, 나머지 에러는 console.error로 로깅하고 빈 배열로 폴백합니다. 사용자에게 장바구니가 비어있는 것처럼 보일 수 있으므로, 에러 상태를 UI에 전달하는 것도 고려해보세요.

src/app/actions/user.ts (1)

11-13: API 응답 타입 불일치 방어 코드

points가 string으로 올 수 있다는 건 백엔드 API 계약이 불안정하다는 신호입니다. 장기적으로는 API 응답 스키마를 zod 등으로 검증하는 것이 더 안전합니다. 현재 방어 로직 자체는 동작에 문제없습니다.

src/app/body-info/page.tsx (1)

3-3: 상대 경로 대신 @/ alias 사용 권장

다른 import들은 @/ alias를 사용하는데 이 줄만 상대 경로입니다. 일관성을 위해 통일하세요.

♻️ 제안
-import { CloseButton } from '../../components/ui/close-button';
+import { CloseButton } from '@/components/ui/close-button';
src/app/product/[id]/layout.tsx (1)

20-23: thumbnailImageUrl이 빈 문자열일 경우 OG 이미지 처리

product.thumbnailImageUrl이 빈 문자열이면 openGraph.images에 빈 값이 들어가 잘못된 OG 태그가 생성될 수 있습니다. compact-product-header.tsx에서처럼 falsy 체크를 추가하는 것을 권장합니다.

♻️ 제안
       openGraph: {
         title: product.name,
-        images: [product.thumbnailImageUrl],
+        ...(product.thumbnailImageUrl && {
+          images: [product.thumbnailImageUrl],
+        }),
       },
src/app/actions/body-info.ts (1)

24-29: ActionResponsedata?: unknown 타입 강화 고려

data?: unknown은 범용적이지만, 소비하는 쪽에서 매번 타입 단언이 필요합니다. 제네릭으로 만들면 타입 안전성이 향상됩니다.

♻️ 제안
-export type ActionResponse = {
+export type ActionResponse<T = unknown> = {
   success: boolean;
   message: string;
   errors?: Record<string, string[]>;
-  data?: unknown;
+  data?: T;
 };
src/components/product-option-sheet/use-product-option.ts (1)

56-56: 불필요한 alias: const options = initialOptions.

이전에는 mock 데이터 fallback 로직이 있었으나 제거되면서 단순 alias만 남았습니다. initialOptions를 직접 사용하거나, 파라미터명을 options로 변경하는 것이 깔끔합니다.

수정 제안 (파라미터명 변경)
 export default function useProductOption({
   productId,
   price,
-  options: initialOptions = [],
+  options = [],
   open: externalOpen,
   onOpenChange: externalOnOpenChange,
 }: UseProductOptionProps) {
   // ...
-  const options = initialOptions;
src/components/product/descriptions/recommended-products-carousel.tsx (1)

29-30: 주석 처리된 dead code 제거 필요

이전 products 변수명을 참조하는 코드가 남아 있습니다. 빈 배열 가드가 필요하면 similarProducts로 복원하고, 아니면 삭제하세요.

♻️ 수정 제안
   // 데이터가 빈 배열일 경우 렌더링 안 함
-  // if (products.length === 0) return null;
+  if (similarProducts.length === 0) return null;
src/app/product/[id]/page.tsx (1)

14-14: Number(id)NaN일 경우 방어 처리 부재

id가 비숫자 문자열이면 productIdNaN이 됩니다. getProductDetail에서 실패하여 notFound()로 빠지긴 하지만, 명시적 검증이 더 안전합니다.

🛡️ 수정 제안
  const productId = Number(id);
+ if (Number.isNaN(productId)) notFound();
src/components/cart/cart-item.tsx (1)

40-50: debounce 타이머 unmount 시 정리 누락

컴포넌트가 언마운트될 때 debounceRef.current에 남아있는 타이머가 정리되지 않아, 언마운트 후 onQuantityChange가 호출될 수 있습니다.

🛡️ cleanup effect 추가 제안
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    setInputValue(String(quantity));
  }, [quantity]);

+ useEffect(() => {
+   return () => {
+     if (debounceRef.current) clearTimeout(debounceRef.current);
+   };
+ }, []);
+
  const debouncedChange = (val: number) => {
src/components/product/descriptions/product-description.tsx (1)

16-19: 인라인 prop 타입이 복잡 — 별도 interface 추출 권장

Omit<...> & Partial<Pick<...>> 조합이 인라인으로 선언되어 가독성이 떨어집니다. ProductDescriptionProps 같은 별도 interface로 분리하면 재사용성과 가독성이 모두 향상됩니다.

src/components/product/product-detail-view.tsx (1)

74-77: 하드코딩된 availableOptionsproduct.options 사용 가능

ProductDetailPropsoptions?: ProductOption[]가 선언되어 있지만, reviewProductInfo.availableOptions는 하드코딩된 sizes/colors를 사용합니다. product.options에서 동적으로 추출해야 실제 데이터와 일치합니다.

♻️ 수정 제안
      availableOptions: {
-      sizes: ['XS', 'S', 'M', 'L', 'XL'],
-      colors: ['Black', 'White', 'Navy', 'Gray', 'Beige'],
+      sizes: [...new Set(product.options?.map((o) => o.size) ?? [])],
+      colors: [...new Set(product.options?.map((o) => o.color) ?? [])],
      },
src/components/cart/use-cart-service.ts (2)

37-46: UPDATE_OPTION에서 가격 재계산 미반영 확인

옵션 변경 시 selectedColor/selectedSize만 업데이트하고 totalPrice는 갱신하지 않습니다. 색상·사이즈별 가격이 동일하면 문제없지만, 다를 경우 서버 응답(revalidatePath) 전까지 UI에 이전 가격이 표시됩니다.

현재 서버 실패 시 router.refresh()로 복구하므로 즉각적 문제는 아니지만, 옵션별 가격 차이가 있는 도메인이라면 고려 필요합니다.


187-194: checkout 시 URL 인코딩 검증 필요

items 파라미터에 쉼표로 구분된 ID를 넣고 있는데, 대량의 장바구니 아이템이 있을 경우 URL 길이 제한에 근접할 수 있습니다. 현실적으로는 문제 없겠지만, 향후 확장 시 POST 기반 checkout이나 세션 저장 방식도 고려할 수 있습니다.

Comment thread src/app/cart/page.tsx Outdated
Comment on lines +6 to +8
import { CartHeaderControl } from '@/components/cart/cart-header-control';
import { CartItemList } from '@/components/cart/cart-item-list';
import { CartSummaryFooter } from '@/components/cart/cart-summary-footer';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the export patterns of cart components
echo "=== Locating cart components ==="
fd "cart-header-control|cart-item-list|cart-summary-footer" src/components --type f

echo -e "\n=== Examining export patterns ==="
fd "cart-header-control|cart-item-list|cart-summary-footer" src/components --type f --exec head -20 {}

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1975


🏁 Script executed:

#!/bin/bash
# Check the export statement in CartItemList
echo "=== CartItemList export pattern ==="
rg "export" src/components/cart/cart-item-list.tsx -A 2 -B 1

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 166


CartHeaderControl, CartItemList, CartSummaryFooter를 default export로 변경

세 컴포넌트 모두 named export(export function ComponentName())를 사용 중입니다. 코딩 가이드라인에 따르면 src/components/**/*.tsx는 반드시 export default function 패턴을 사용해야 합니다. 각 컴포넌트 파일을 수정하고, 페이지의 import 문도 기본값 import로 업데이트해야 합니다.

🤖 Prompt for AI Agents
In `@src/app/cart/page.tsx` around lines 6 - 8, 세 컴포넌트 CartHeaderControl,
CartItemList, CartSummaryFooter가 현재 named export로 작성되어 있으니 각 컴포넌트 파일에서 `export
function <Name>()` 패턴을 `export default function <Name>()`로 변경하고 해당 파일의 any other
named exports가 있다면 이름을 유지하되 default로 내보내는지 확인한 뒤, 페이지 파일의 import 문을 named
import에서 기본(default) import로 바꿔 `import CartHeaderControl from
'@/components/cart/cart-header-control'`, `import CartItemList from
'@/components/cart/cart-item-list'`, `import CartSummaryFooter from
'@/components/cart/cart-summary-footer'` 형태로 업데이트하세요; 컴포넌트 내부/외부 참조가 있다면 호출부(예:
JSX 사용처)에서 변경된 export 방식이 정상 동작하는지 확인하세요.

import { fetchUserBodyInfo, fetchSizeAnalysis } from '@/mocks/size';
import { getProductDetail, getSimilarProducts } from '@/app/actions/product';
import { getMyBodyInfoAction } from '@/app/actions/body-info';
import { fetchSizeAnalysis } from '@/mocks/size';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# fetchSizeAnalysis 사용처 확인
echo "=== fetchSizeAnalysis 사용처 ==="
rg -n "fetchSizeAnalysis" --type ts --type tsx

echo -e "\n=== SizeAnalysis 관련 server action 확인 ==="
rg -n "SizeAnalysis" --type ts --type tsx src/app/actions/

echo -e "\n=== mocks/size 파일 내용 확인 ==="
find . -path "./src/mocks/size*" -type f | head -5

echo -e "\n=== src/app/product/[id]/page.tsx 라인 4 주변 확인 ==="
head -20 src/app/product/[id]/page.tsx

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 980


🏁 Script executed:

#!/bin/bash
# fetchSizeAnalysis 전체 검색
echo "=== fetchSizeAnalysis 모든 파일에서 검색 ==="
rg "fetchSizeAnalysis" -n 2>/dev/null || echo "No matches found"

echo -e "\n=== mocks/size.ts 내용 ==="
cat -n src/mocks/size.ts

echo -e "\n=== ProductDetailView 컴포넌트 확인 ==="
cat -n src/components/product/product-detail-view.tsx | head -50

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 3981


🏁 Script executed:

#!/bin/bash
# Server action 내 사이즈 관련 함수 검색
echo "=== app/actions 디렉토리 구조 ==="
find src/app/actions -type f -name "*.ts" | head -20

echo -e "\n=== 'size' 관련 server action 검색 ==="
rg "size|Size" src/app/actions/ -l

echo -e "\n=== 모든 app/actions 파일 목록 ==="
ls -la src/app/actions/

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 928


🏁 Script executed:

#!/bin/bash
# body-info.ts와 order.ts에서 사이즈 분석 관련 함수 확인
echo "=== body-info.ts 전체 내용 ==="
cat -n src/app/actions/body-info.ts

echo -e "\n=== order.ts에서 'size' 관련 부분 ==="
rg -n "size|Size" src/app/actions/order.ts -A 3

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 6091


fetchSizeAnalysis mock 의존성 아직 잔존

PR 목표인 mock 데이터 제거가 완료되지 않았습니다. fetchSizeAnalysis가 여전히 @/mocks/size에서 import되어 라인 36에서 활용 중인데, 다른 함수들(getProductDetail, getSimilarProducts, getMyBodyInfoAction, getMyWishlist)은 모두 server action으로 이미 마이그레이션되었습니다. getSizeOptionsAction()은 사이즈 옵션 조회만 처리하므로 fetchSizeAnalysis를 대체할 수 없습니다. 사이즈 분석 기능도 server action으로 마이그레이션하거나, 의도적으로 mock 유지 시 그 이유를 명시해야 합니다.

🤖 Prompt for AI Agents
In `@src/app/product/`[id]/page.tsx at line 4, The import of the mock
fetchSizeAnalysis from '@/mocks/size' remains and is used in this file
(fetchSizeAnalysis), but other data-fetching functions (getProductDetail,
getSimilarProducts, getMyBodyInfoAction, getMyWishlist) were migrated to server
actions; either migrate the size analysis logic into a server action and replace
fetchSizeAnalysis usage with that new action (e.g., create
getSizeAnalysisAction() and call it from this page), or if you intentionally
want to keep the mock, remove the import of fetchSizeAnalysis and add an
explicit comment near getSizeOptionsAction/usage explaining why the mock is
retained (and ensure test-only guards or feature flags prevent it from
shipping); update references accordingly to use the new server action name or
the documented mock retention rationale.

Comment thread src/components/cart/cart-data-fetcher.tsx Outdated
Comment thread src/components/cart/cart-header-control.tsx Outdated
Comment thread src/components/cart/cart-item-list.tsx Outdated
Comment thread src/components/product/descriptions/product-description.tsx Outdated
Comment on lines 23 to 25
export function RecommendedProductsCarousel({
products,
similarProducts,
}: RecommendedProductsCarouselProps) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "RecommendedProductsCarousel" --type=ts --type=tsx -g '!node_modules'

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 94


🏁 Script executed:

rg "RecommendedProductsCarousel" -t ts -g '!node_modules' -A 2 -B 2

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 2794


🏁 Script executed:

rg "from.*components/product" -t ts -A 1 | grep -i "recommended"

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 49


export default function 패턴 필수

src/components/**/*.tsx 코딩 가이드라인에 따라 named export 대신 default export를 사용해야 합니다. 이 변경 시 다음 3개 파일도 함께 수정이 필요합니다:

  1. recommended-products-carousel.tsx: 라인 23 export 선언
  2. product-description.tsx: import 문 수정
  3. product/index.tsx: re-export 문 수정
♻️ 수정 범위
-export function RecommendedProductsCarousel({
+export default function RecommendedProductsCarousel({
   similarProducts,
 }: RecommendedProductsCarouselProps) {
-import { RecommendedProductsCarousel } from './recommended-products-carousel';
+import RecommendedProductsCarousel from './recommended-products-carousel';
-export { RecommendedProductsCarousel } from './descriptions/recommended-products-carousel';
+export { default as RecommendedProductsCarousel } from './descriptions/recommended-products-carousel';
📝 Committable suggestion

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

Suggested change
export function RecommendedProductsCarousel({
products,
similarProducts,
}: RecommendedProductsCarouselProps) {
export default function RecommendedProductsCarousel({
similarProducts,
}: RecommendedProductsCarouselProps) {
🤖 Prompt for AI Agents
In `@src/components/product/descriptions/recommended-products-carousel.tsx` around
lines 23 - 25, Change the named export to a default export for the component by
updating the declaration to "export default function
RecommendedProductsCarousel(...)" in RecommendedProductsCarousel, then update
any imports: in product-description.tsx replace the named import (e.g. import {
RecommendedProductsCarousel } ...) with a default import (import
RecommendedProductsCarousel ...), and update the re-export in product/index.tsx
to export the default (e.g. export { default as RecommendedProductsCarousel }
from '...') so all references use the default export.

Comment thread src/components/product/descriptions/recommended-products-carousel.tsx Outdated
Comment on lines 55 to 67
{images.map((src, index) => (
<CarouselItem key={index} className="relative aspect-square w-full">
<Image
src={src}
alt={`상품 이미지 ${index + 1}`}
fill
className="object-cover"
priority={index === 0}
sizes="(max-width: 768px) 100vw, 40vw"
/>
{src && (
<Image
src={src}
alt={`상품 이미지 ${index + 1}`}
fill
className="object-cover"
priority={index === 0}
sizes="(max-width: 768px) 100vw, 40vw"
/>
)}
</CarouselItem>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

imageUrl이 빈 문자열이면 빈 슬라이드 5개가 렌더링됩니다.

Array(5).fill(imageUrl)에서 imageUrl이 falsy이면 모든 CarouselItem이 빈 상태로 렌더링되고, 카운터는 1 / 5로 표시됩니다. 상위에서 guard하거나, 컴포넌트 레벨에서 fallback UI(placeholder 이미지 등)를 제공하는 것이 UX상 낫습니다.

🤖 Prompt for AI Agents
In `@src/components/product/product-image-slider.tsx` around lines 55 - 67, The
component currently renders empty CarouselItem slides when image URLs are falsy;
update product-image-slider to filter out falsy entries before mapping (use
images.filter(Boolean) or similar) and map over that filtered array in the
images.map block so only valid Image components render; if the filtered array is
empty, render a fallback UI (single placeholder CarouselItem/Image or a “no
image” placeholder) and ensure any counter or pagination logic uses the filtered
array length instead of the original Array(5).fill(...) source so the slide
count and visuals stay consistent.

Comment thread src/mocks/product-data.ts
Comment on lines +87 to +90
const reviewCount = ((startId + i) * 37 + 13) % 200;
const reviewRating = parseFloat(
((((startId + i) * 53 + 7) % 50) / 10).toFixed(1),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

reviewRating 범위가 0.0~4.9로, 리뷰가 있는데 평점 0.0이 나올 수 있음

((startId + i) * 53 + 7) % 50 결과가 0이 되는 경우 reviewCount > 0인데 reviewRating = 0.0인 비정상 데이터가 생성됩니다. 또한 최대값이 4.9로 5.0에 도달하지 못합니다.

Mock 데이터라 치명적이지는 않지만, 하류 UI 컴포넌트에서 별점 0.0을 렌더링할 때 의도치 않은 결과가 나올 수 있습니다.

제안: 1.0~5.0 범위로 보정
-    const reviewRating = parseFloat(
-      ((((startId + i) * 53 + 7) % 50) / 10).toFixed(1),
-    );
+    const reviewRating = parseFloat(
+      (((((startId + i) * 53 + 7) % 40) + 10) / 10).toFixed(1),
+    );

이렇게 하면 10..49 / 10 = 1.0..4.9 범위로 최소 1.0이 보장됩니다.

🤖 Prompt for AI Agents
In `@src/mocks/product-data.ts` around lines 87 - 90, The code can produce
reviewRating = 0.0 even when reviewCount > 0 and never reaches 5.0; update the
reviewRating computation (the symbol reviewRating that uses startId and i and
the derived raw value) so that when reviewCount > 0 you clamp the computed
rating to a minimum of 1.0 (e.g., compute the raw rating from (((startId + i) *
53 + 7) % 50) / 10 and then set reviewRating = Math.max(1.0, parsedRaw) after
rounding to one decimal), ensuring no positive-review product gets 0.0 while
preserving the existing distribution.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (4)
src/components/product-option-sheet/use-product-option.ts (2)

98-106: ⚠️ Potential issue | 🟠 Major

stockStatus 미검증 — 품절 옵션도 선택 가능합니다.

ProductOptionstockStatus: StockStatus 필드가 존재하지만(src/types/domain/product.ts:29), addItemToSelection 호출 전에 재고 상태를 확인하지 않습니다. 품절(OUT_OF_STOCK) 옵션이 selectedItems에 추가되어 결제/장바구니 요청 시 서버 에러가 발생할 수 있습니다.

수정 제안
      if (targetOption) {
+       if (targetOption.stockStatus === 'OUT_OF_STOCK') {
+         alert('품절된 옵션입니다.');
+         setCurrentSize('');
+         return;
+       }
        addItemToSelection(targetOption);
      }

170-172: ⚠️ Potential issue | 🟡 Minor

console.error 잔존 — PR 체크리스트 위반.

PR 템플릿 체크리스트에 console.log 제거 항목이 있습니다. 프로덕션에서는 에러 리포팅 서비스(Sentry 등)로 대체하거나, 최소한 개발 환경 분기를 고려하세요.

src/components/product/product-detail-view.tsx (1)

27-32: 🛠️ Refactor suggestion | 🟠 Major

코딩 가이드라인 위반: 화살표 함수로 선언된 컴포넌트

ProductInquiryContent는 JSX를 반환하는 컴포넌트이지만 const 화살표 함수로 선언되어 있습니다. 가이드라인 및 learnings에 따르면 const는 내부 로직과 훅에만 사용하고, 컴포넌트는 반드시 function 선언으로 작성해야 합니다.

♻️ function 선언으로 변환
-const ProductInquiryContent = () => (
-  <div className="rounded-lg bg-gray-50 py-20 text-center text-gray-500">
-    <p>문의 영역입니다.</p>
-    <p className="mt-2 text-sm">이곳에 문의 리스트 컴포넌트가 들어갑니다.</p>
-  </div>
-);
+function ProductInquiryContent() {
+  return (
+    <div className="rounded-lg bg-gray-50 py-20 text-center text-gray-500">
+      <p>문의 영역입니다.</p>
+      <p className="mt-2 text-sm">이곳에 문의 리스트 컴포넌트가 들어갑니다.</p>
+    </div>
+  );
+}

As per coding guidelines, src/components/**/*.tsx: "export default function 패턴 (화살표 함수 금지)". Based on learnings, "Use const only for internal logic and hooks within components. Do not use const for component declarations."

src/components/product/descriptions/product-description.tsx (1)

23-26: ⚠️ Potential issue | 🟡 Minor

상세 이미지 목 데이터를 실제 API 응답 데이터로 교체하세요

Array(10).fill()로 썸네일 이미지를 반복 복제하는 임시 로직이 프로덕션 코드에 남아 있습니다. product props에서 실제 상세 이미지 배열을 받아 사용하거나, 필요시 별도 API 호출로 가져와야 합니다. 현재는 동일 이미지가 계속 노출됩니다.

유사한 패턴이 src/components/product/product-image-slider.tsx에도 있으니 함께 처리하세요.

🧹 Nitpick comments (11)
src/components/product-option-sheet/use-product-option.ts (4)

82-92: Date.now() 기반 ID — 빠른 연속 호출 시 충돌 가능.

동일 옵션을 빠르게 두 번 추가하는 경우는 findIndex로 방어되지만, 서로 다른 옵션을 밀리초 단위로 추가하면 동일 id가 생성될 수 있습니다. crypto.randomUUID() 또는 카운터 사용을 권장합니다.

수정 제안
-            id: `${option.optionId}-${Date.now()}`,
+            id: crypto.randomUUID(),

58-66: 중복 제거 패턴 불일치 — Set vs indexOf.

availableColorsnew Set으로 중복을 제거하고, availableSizes.filter((val, idx, arr) => arr.indexOf(val) === idx)를 사용합니다. 일관성을 위해 동일 패턴을 적용하세요.

수정 제안
- const availableSizes = options
-   .filter((o) => !currentColor || o.color === currentColor)
-   .map((o) => o.size)
-   .filter(Boolean)
-   .filter((val, idx, arr) => arr.indexOf(val) === idx);
+ const availableSizes = Array.from(
+   new Set(
+     options
+       .filter((o) => !currentColor || o.color === currentColor)
+       .map((o) => o.size)
+       .filter(Boolean),
+   ),
+ );

111-119: updateQuantity, removeItem, setIsOpenuseCallback 미적용.

addItemToSelectionuseCallback으로 감쌌지만 나머지 핸들러들은 매 렌더마다 새 참조가 생성됩니다. 자식 컴포넌트에 props로 전달될 경우 불필요한 리렌더링을 유발합니다.

Also applies to: 121-123, 42-49


159-162: URL params에 JSON 직렬화 — 길이 제한 주의.

selectionsJSON.stringify하여 query string에 넣고 있습니다. 선택 항목이 많아지면 URL 길이 제한(~2048 chars)에 도달할 수 있습니다. sessionStorage나 서버 상태를 통한 전달을 검토하세요.

src/components/product/descriptions/product-description.tsx (2)

13-20: 인라인 props 타입이 ProductDetailView와 중복됩니다.

product-detail-view.tsxProductDetailProps와 동일한 Omit<Product, ...> & Partial<Pick<Product, ...>> 패턴이 반복되고 있습니다. 하나의 도메인 타입으로 추출하면 두 곳 모두 일관성 있게 관리할 수 있습니다.

♻️ 타입 추출 제안

src/types/domain/product.ts에 공유 타입을 정의:

export type ProductSummary = Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> &
  Partial<Pick<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'>>;

그런 다음 두 파일 모두에서 사용:

-  product: Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> &
-    Partial<Pick<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'>>;
+  product: ProductSummary;

32-44: 이미지 리스트의 key={idx} 사용은 현재 구조에서 허용되나, 접근성 개선 여지가 있습니다.

정적 배열이므로 인덱스 키는 괜찮습니다. 다만, src가 falsy일 때 빈 <div>만 렌더링되어 빈 영역이 레이아웃 공간을 차지합니다. aspect-3/4가 적용된 빈 컨테이너가 시각적 공백을 만들 수 있습니다.

♻️ falsy src일 때 div 자체를 렌더링하지 않는 방안
-        {visibleImages.map((src, idx) => (
-          <div key={idx} className="relative aspect-3/4 w-full">
-            {src && (
+        {visibleImages.filter(Boolean).map((src, idx) => (
+          <div key={idx} className="relative aspect-3/4 w-full">
+            {
               <Image
                 src={src}
                 alt={`상세 설명 이미지 ${idx + 1}`}
                 fill
                 className="object-cover"
                 sizes="(max-width: 768px) 100vw, 50vw"
               />
-            )}
+            }
           </div>
         ))}
src/components/product/product-detail-view.tsx (2)

74-77: 하드코딩된 sizes/colors 배열 — ProductOption[] 데이터 활용 가능

options prop으로 ProductOption[]을 받을 수 있는 구조인데, availableOptions의 sizes와 colors가 하드코딩되어 있습니다. 실제 옵션 데이터에서 추출하면 정확한 값을 반영할 수 있습니다.

♻️ options에서 파생하는 방안
     availableOptions: {
-      sizes: ['XS', 'S', 'M', 'L', 'XL'],
-      colors: ['Black', 'White', 'Navy', 'Gray', 'Beige'],
+      sizes: [...new Set(product.options?.map((o) => o.size) ?? [])],
+      colors: [...new Set(product.options?.map((o) => o.color) ?? [])],
     },

15-15: import 경로 불일치: 배럴 vs 직접 모듈 경로

Lines 4-13에서는 @/components/product 배럴을 사용하지만, ProductDescription만 직접 모듈 경로로 import하고 있습니다. 배럴에서 이미 re-export되므로 통일하는 것이 일관성에 좋습니다.

♻️ 배럴 import로 통일
-import ProductDescription from '@/components/product/descriptions/product-description';

Lines 4-13의 배럴 import에 추가:

 import {
   ProductImageSlider,
   ProductInfo,
   ProductBottomBar,
   CompactProductHeader,
   ProductTab,
   ProductInteractionProvider,
   ProductStickyContainer,
   ProductHeader,
+  ProductDescription,
 } from '@/components/product';
src/components/cart/cart-option-change-sheet.tsx (1)

39-49: useEffectstartTransition 비동기 호출 시 클린업 누락

item이 빠르게 변경되면(예: 다른 상품 옵션 편집을 연속 클릭) 이전 getProductOptions 응답이 나중에 도착해 setOptions가 잘못된 상품 옵션으로 덮어쓸 수 있습니다. useEffect 클린업에서 stale 응답을 무시하는 패턴을 권장합니다.

♻️ stale 응답 방지 패턴
  useEffect(() => {
    if (!item) return;
+   let cancelled = false;

    setCurrentColor(item.selectedColor);
    setCurrentSize(item.selectedSize);

    startTransition(async () => {
      const fetched = await getProductOptions(item.productId);
-     setOptions(fetched);
+     if (!cancelled) setOptions(fetched);
    });
+
+   return () => { cancelled = true; };
  }, [item]);
src/components/cart/cart-item.tsx (2)

40-50: 컴포넌트 언마운트 시 debounce 타이머 클린업 누락

debounceRef의 타이머가 컴포넌트 언마운트 시 정리되지 않아, 삭제 등으로 카트 아이템이 제거된 후에도 onQuantityChange가 실행될 수 있습니다. 불필요한 서버 액션 호출이나 stale 데이터 업데이트로 이어질 수 있습니다.

♻️ useEffect 클린업 추가
+ useEffect(() => {
+   return () => {
+     if (debounceRef.current) clearTimeout(debounceRef.current);
+   };
+ }, []);
+
  useEffect(() => {
    setInputValue(String(quantity));
  }, [quantity]);

128-146: 시니어 대상 앱에서 수량 입력 필드 터치 영역이 작을 수 있음

w-6(24px)은 시니어 친화적 UX 기준(최소 44px 권장, WCAG 2.5.5)에 비해 상당히 작습니다. 실제 렌더링에서 flex 컨테이너에 의해 확장될 수 있지만 확인이 필요합니다. w-12 이상으로 넓히는 것을 고려해주세요.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/product-detail-view.tsx (1)

70-79: ⚠️ Potential issue | 🟠 Major

하드코딩된 availableOptions — mock 데이터 잔존 가능성

커밋 메시지에서 "mock 데이터용 임시 로직 제거"를 언급했지만, sizes: ['XS', 'S', 'M', 'L', 'XL']colors: ['Black', 'White', 'Navy', 'Gray', 'Beige']가 하드코딩되어 있습니다. product.options (Line 46의 ProductOption[])에서 실제 사이즈/색상을 추출해야 할 것으로 보입니다.

제안: options에서 동적으로 추출
     availableOptions: {
-      sizes: ['XS', 'S', 'M', 'L', 'XL'],
-      colors: ['Black', 'White', 'Navy', 'Gray', 'Beige'],
+      sizes: [...new Set(product.options?.map((o) => o.size) ?? [])],
+      colors: [...new Set(product.options?.map((o) => o.color) ?? [])],
     },
🤖 Fix all issues with AI agents
In `@src/components/product/product-image-slider.tsx`:
- Line 18: 컴포넌트 선언인 ProductImageSlider을 named export에서 default export로 변경하세요: 바꿀
대상은 function ProductImageSlider -> export default function ProductImageSlider({
imageUrls }: ProductImageSliderProps) (파일: ProductImageSlider), 그리고 해당 default로
바뀐 export에 맞춰 re-export를 수정하세요: src/components/product/index.tsx에서 named
re-export를 사용 중인 부분을 default re-export로 변경하여 import/export 불일치를 제거하세요.
🧹 Nitpick comments (7)
src/components/product/product-image-slider.tsx (1)

19-19: 불필요한 별칭 제거

const images = imageUrls는 단순 별칭으로 가독성에 기여하지 않습니다.

제안
-export default function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
-  const images = imageUrls;
+export default function ProductImageSlider({ imageUrls: images }: ProductImageSliderProps) {

또는 그냥 imageUrls를 직접 사용하세요.

src/components/product/descriptions/product-description.tsx (1)

16-21: 인라인 타입이 ProductDetailProps와 중복됨 — 공유 타입 추출 권장

Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> & Partial<Pick<...>> & { imageUrls?: string[] } 구조가 product-detail-view.tsxProductDetailProps와 거의 동일합니다. src/types/domain/product.ts에 공유 타입을 정의하면 두 곳 모두 단순화됩니다.

// src/types/domain/product.ts
export type ProductDetailData = Omit<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'> &
  Partial<Pick<Product, 'viewCount' | 'purchaseCount' | 'reviewCount'>> & {
    imageUrls?: string[];
  };
src/components/product/product-detail-view.tsx (1)

27-32: 내부 플레이스홀더 컴포넌트도 function 선언 권장

const ProductInquiryContent = () => (...) 패턴은 가이드라인의 화살표 함수 금지 규칙에 해당합니다. 내부 컴포넌트라도 일관성을 위해 function 선언이 낫습니다.

제안
-const ProductInquiryContent = () => (
+function ProductInquiryContent() {
+  return (
     <div className="rounded-lg bg-gray-50 py-20 text-center text-gray-500">
       <p>문의 영역입니다.</p>
       <p className="mt-2 text-sm">이곳에 문의 리스트 컴포넌트가 들어갑니다.</p>
     </div>
-);
+  );
+}

As per coding guidelines, src/components/**/*.tsx: "export default function 패턴 (화살표 함수 금지)". Based on learnings, "Do not use const for component declarations."

src/components/product-option-sheet/use-product-option.ts (4)

104-108: alert()는 접근성(a11y) 측면에서 비권장

alert('품절된 옵션입니다.')는 네이티브 블로킹 다이얼로그로, 스크린 리더 사용자 경험이 좋지 않고 스타일링도 불가합니다. 토스트 알림이나 인라인 메시지로 대체하면 UX와 접근성 모두 개선됩니다.

지금 당장은 아니더라도 추후 개선 대상으로 참고해 주세요.


56-56: 불필요한 별칭 할당

const options = initialOptions;는 의미 없는 중간 변수입니다. 이전에 더미 데이터 생성 로직이 있었던 흔적으로 보이는데, 리팩터링이 완료되었으므로 제거하거나 파라미터 이름 자체를 options로 변경하는 게 깔끔합니다.

제안: 파라미터 직접 사용
 export default function useProductOption({
   productId,
   price,
-  options: initialOptions = [],
+  options = [],
   open: externalOpen,
   onOpenChange: externalOnOpenChange,
 }: UseProductOptionProps) {
   ...
-  const options = initialOptions;

134-146: 다수 옵션의 병렬 addToCart 호출 — 부분 실패 시 정합성 문제

Promise.all은 하나라도 실패하면 전체가 reject됩니다. 3개 옵션 중 2개만 성공하면 사용자에게는 에러 메시지가 뜨지만, 서버에는 2개가 이미 추가된 상태입니다. 멱등성이 보장되지 않는다면 Promise.allSettled로 변경하고 부분 실패를 핸들링하거나, 배치 API를 사용하는 것이 안전합니다.


162-165: URL 쿼리 파라미터에 JSON을 직접 넣는 방식 주의

selectionsJSON.stringify하여 URL 파라미터로 전달하면, 옵션이 많을 경우 URL 길이 제한(~2048자)에 걸릴 수 있습니다. 또한 브라우저 히스토리에 주문 정보가 노출됩니다. 세션 스토리지나 서버 사이드 임시 저장을 검토해 보세요.


export function ProductImageSlider({ imageUrl }: ProductImageSliderProps) {
const images = Array(5).fill(imageUrl);
export function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the export pattern in product-image-slider.tsx
echo "=== product-image-slider.tsx export pattern ==="
rg -n 'export.*ProductImageSlider' --type=tsx src/components/product/

# Check re-export in index.tsx
echo -e "\n=== product/index.tsx re-export pattern ==="
cat src/components/product/index.tsx

# Check other component export patterns for reference
echo -e "\n=== Other component export patterns ==="
rg -n '^export (default )?function' --type=tsx src/components/product/ | head -20

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 2881


🏁 Script executed:

#!/bin/bash
# Check the actual content of product-image-slider.tsx
echo "=== product-image-slider.tsx content (first 30 lines) ==="
head -30 src/components/product/product-image-slider.tsx

# Check other components' export patterns
echo -e "\n=== product-card.tsx export pattern ==="
head -20 src/components/product/product-card.tsx | grep -E '^export|^import'

echo -e "\n=== product-info.tsx export pattern ==="
head -20 src/components/product/product-info.tsx | grep -E '^export|^import'

echo -e "\n=== product-filter-bar.tsx export pattern ==="
head -20 src/components/product/product-filter-bar.tsx | grep -E '^export|^import'

echo -e "\n=== product-list.tsx export pattern ==="
head -20 src/components/product/product-list.tsx | grep -E '^export|^import'

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1625


🏁 Script executed:

#!/bin/bash
# Check more lines of product-image-slider.tsx to see the full implementation
echo "=== product-image-slider.tsx (lines 15-75) ==="
sed -n '15,75p' src/components/product/product-image-slider.tsx

# Verify if ProductImageSlider is used as default export anywhere
echo -e "\n=== Check if ProductImageSlider is imported as default ==="
rg 'import.*ProductImageSlider' --type=ts

Repository: IT-Cotato/12th-OnGil-FE

Length of output: 1856


export default function 패턴 필수 적용

src/components/**/*.tsx 코딩 가이드라인에서 컴포넌트는 반드시 export default function을 사용해야 합니다. 현재 코드는 export function 패턴을 사용 중입니다.

이 변경 시 src/components/product/index.tsx의 re-export도 함께 수정이 필요합니다:

적용할 변경사항
-export function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
+export default function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
-export { ProductImageSlider } from './product-image-slider';
+export { default as ProductImageSlider } from './product-image-slider';
📝 Committable suggestion

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

Suggested change
export function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
export default function ProductImageSlider({ imageUrls }: ProductImageSliderProps) {
🤖 Prompt for AI Agents
In `@src/components/product/product-image-slider.tsx` at line 18, 컴포넌트 선언인
ProductImageSlider을 named export에서 default export로 변경하세요: 바꿀 대상은 function
ProductImageSlider -> export default function ProductImageSlider({ imageUrls }:
ProductImageSliderProps) (파일: ProductImageSlider), 그리고 해당 default로 바뀐 export에 맞춰
re-export를 수정하세요: src/components/product/index.tsx에서 named re-export를 사용 중인 부분을
default re-export로 변경하여 import/export 불일치를 제거하세요.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant