Skip to content

[URECA-76] Feat: 추천 페이지 구현#75

Merged
joonhyong merged 6 commits intodevelopfrom
URECA-76/Feat/recommend-api
Jan 29, 2026
Merged

[URECA-76] Feat: 추천 페이지 구현#75
joonhyong merged 6 commits intodevelopfrom
URECA-76/Feat/recommend-api

Conversation

@Chaejy
Copy link
Copy Markdown
Contributor

@Chaejy Chaejy commented Jan 29, 2026

Key Changes

  • 추천 페이지 및 상세 추천 작업

작업 내역

  • close:

📸 스크린샷

💬 공유사항 to 리뷰어

비고

PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • Eslint 및 Prettier 규칙을 준수했습니다.
  • origin/develop 브랜치에서의 최신 커밋을 확인하고 반영했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트)

Summary by CodeRabbit

  • 새로운 기능

    • 개인화 추천 조회(사용자/요약별)와 요약별 추천 상품 페이지 추가
    • 추천 카드 목록의 응답형 그리드 컴포넌트 도입
  • 개선사항

    • 데이터 기반 렌더링으로 실시간 추천 반영
    • 로딩·오류 상태 표시 및 재시도 버튼 추가
    • 탐색 전 추천 데이터 사전조회(prefetch)로 빠른 전환
    • 가격 표기 통일 및 카드‧리스트의 레이아웃·반응형 개선
  • 제거

    • 하드코딩된 목업 데이터 삭제

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
unity Ready Ready Preview, Comment Jan 29, 2026 8:52am
unity-client Ready Ready Preview, Comment Jan 29, 2026 8:52am

@github-actions github-actions bot changed the title 추천 페이지 구현 [URECA-76] Feat: 추천 페이지 구현 Jan 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

이번 PR은 추천 상품 기능을 더미 데이터에서 실제 API 기반으로 전환합니다. React Query 훅(useRecommendMe, useRecommendSummary)과 API 클라이언트(fetchRecommendMe, fetchRecommendSummary)가 추가되고, 상품/카테고리 타입(ProductProps, CategoryId 등) 및 매퍼(getCategoryInfo, categoryMapper)가 도입되었습니다. 컴포넌트들은 데이터 기반 렌더링, 로딩/에러/빈 상태 처리와 프리페치(queryClient.prefetchQuery)를 추가했으며, 여러 컴포넌트 및 경로(import)명이 케밥 케이스로 정리되고 mock 데이터 모듈이 삭제되었습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경사항의 핵심을 명확하게 나타내고 있습니다. 추천 페이지 구현이라는 주요 작업을 직관적으로 전달합니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@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: 10

Caution

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

⚠️ Outside diff range comments (2)
src/hooks/summary/useSummaryList.ts (1)

32-41: 키워드 파싱 로직 일관성 확인 필요

이 파일에서는 JSON 파싱 실패 시 []를 반환하지만, useBookmarkedSummaryList.ts에서는 [item.keywords]를 반환합니다. 동일한 데이터 소스를 처리하므로 일관된 동작이 필요합니다.

🔧 일관성 수정 제안
         keywords:
           typeof item.keywords === 'string'
             ? (() => {
                 try {
                   return JSON.parse(item.keywords);
                 } catch {
-                  return [];
+                  return [item.keywords];
                 }
               })()
             : (item.keywords ?? []),
src/app/(private)/summary/bookmarks/page.tsx (1)

107-120: 훅의 타입 계약을 신뢰하고 불필요한 변환 제거하기

useBookmarkedSummaryList 훅의 BookmarkedSummaryListItem 인터페이스에서 keywords: string[]로 명확히 정의하고 있으며, 훅의 구현부(queryFn)에서 이미 모든 경우의 수를 처리하여 항상 string[]을 반환합니다. 따라서 page.tsx line 108-109의 typeof summary.keywords === 'string' 체크는 불필요합니다.

// 현재 코드 (불필요한 중복 체크)
const badges =
  typeof summary.keywords === 'string' ? [summary.keywords] : summary.keywords;

// 개선 코드 (훅의 타입 계약 신뢰)
const badges = summary.keywords;

이유:

  • 단일 책임 원칙: keywords 정규화는 훅의 책임이며, page에서 재검증하면 관심사가 분산됩니다
  • 타입 안정성: TypeScript의 타입 시스템을 완전히 활용하면 런타임 체크 불필요
  • 가독성: 불필요한 조건부 로직이 제거되어 코드가 간결해집니다
🤖 Fix all issues with AI agents
In `@src/app/`(private)/recommend/page.tsx:
- Around line 32-39: When !isLoading && !isError and categories is empty the
page currently renders nothing; update the render logic in page.tsx around the
block using categories.map(...) to detect categories.length === 0 and render a
concise empty-state UI (e.g., an EmptyState component or a message/card with
icon and a call-to-action) instead of mapping; keep the existing ProductList
rendering for the non-empty case and reuse products.filter((p) => p.categoryId
=== i.id) as before.

In `@src/app/`(private)/summary/_components/SummarySuccessPage.tsx:
- Around line 156-163: The error UI currently renders when recommendError is
true but calls refetchRecommend() without handling its returned Promise and the
retry button lacks accessibility labeling; update the onClick handler in
SummarySuccessPage to call refetchRecommend and handle the Promise (e.g., await
in an async handler or use .catch to handle errors and optionally set loading
state), and add an appropriate aria-label to the retry button (e.g.,
aria-label="Retry loading recommended products") so screen readers can describe
the action.

In `@src/app/`(private)/summary/[summaryId]/recommended-products/page.tsx:
- Around line 25-35: Wrap the status messages rendered in the conditional blocks
for isLoading, isError, and empty products (the JSX shown under the isLoading,
isError, and products.length === 0 checks in page.tsx) in accessible live
regions by adding appropriate ARIA attributes: use role="status" with
aria-live="polite" (and aria-atomic="true") for non-critical info like the
loading and empty messages, and use role="alert" with aria-live="assertive" for
the error block so screen readers announce it immediately; update the
corresponding div elements (the ones containing "추천 상품 불러오는 중...", the error
message with the retry button, and "추천 상품이 없습니다.") to include these attributes.
- Around line 11-20: Currently safeId defaults to 0 which triggers
useSummaryDetail and useRecommendSummary to call APIs for id 0; change the logic
to treat invalid summaryId as disabled by deriving a boolean like isValidId =
Number.isFinite(summaryId) && summaryId > 0 and pass that to the hooks (e.g.,
via query options or an "enabled" prop) so neither useSummaryDetail(safeId) nor
useRecommendSummary(safeId) runs when isValidId is false, and render an
immediate UX fallback (redirect, error message, or NotFound) when isValidId is
false instead of letting the hooks fetch with id 0; update references to rawId,
summaryId, safeId, useSummaryDetail and useRecommendSummary accordingly.

In `@src/components/counseling-recommend/recommend-card/index.tsx`:
- Around line 7-11: Remove the unused is_monthly prop from the RecommendCard
API: delete is_monthly from the RecommendCardProps type and remove it from the
destructuring in the RecommendCard component (where props are unpacked) so the
component no longer declares or expects it; then remove is_monthly={true} from
the parent component recommend-cardlist where RecommendCard is instantiated.
Also search for any other references to is_monthly in this component and the
parent and delete or refactor them if present to ensure no dangling references
remain.

In `@src/components/recommend/product-goods/index.tsx`:
- Around line 11-12: Replace the unconditional Link wrapper around the product
card with conditional rendering based on product.link: when product.link is
truthy, render the Link (preserving target="_blank" and rel="noopener
noreferrer") wrapping the card markup; when product.link is null/undefined,
render the same product card markup without Link and apply a
disabled/visually-distinct style and ARIA hint (e.g., aria-disabled or role) to
the outer div so the card remains accessible—update the component using the
existing Link usage and the product.link check around the card container.

In `@src/components/recommend/product-plans/index.tsx`:
- Around line 9-10: The component signature exposes a monthly prop but it is
unused—either remove the monthly prop from the ProductPlans component props or
implement its intended behavior (e.g., adjust displayed price or CSS) wherever
pricing is rendered; also change how Link is rendered so when product.link is
falsy you do not set href="#" with target="_blank"/rel (use a plain non-anchor
wrapper or render Link without target/rel when product.link is missing) — locate
the Link usage and the product.link reference in the component and update
handling accordingly.
- Line 5: The ProductPlans component declares a monthly prop that is never used
and the component itself appears unused; either remove the entire ProductPlans
export to eliminate dead code (delete the function and its export) or implement
monthly into the rendering logic of ProductPlans (update the ProductPlans
function to accept { product, monthly } and use monthly to toggle monthly vs
yearly pricing display or conditional JSX where pricing is rendered), and ensure
any callers/imports are updated or the export removed; reference the
ProductPlans component and the monthly prop when making the change.

In `@src/hooks/recommend/useRecommendSummary.ts`:
- Around line 16-37: mapRecommendSummary currently calls
getCategoryInfo(item.categoryId) without guarding against unknown/new category
IDs which can throw or return invalid data; update mapRecommendSummary to
validate the result of getCategoryInfo (or wrap the call in try/catch) and skip
or provide a safe fallback when a category is missing so the page won't crash:
only call categories.set(category.id, category) when category is non-null/has an
id, and handle products for which category lookup failed (e.g., skip adding the
product, set categoryId to null/safe default, and/or log a warning). Ensure
changes are made inside mapRecommendSummary and reference getCategoryInfo,
categories (Map), and products (ProductProps[]) so runtime errors are prevented.
🧹 Nitpick comments (9)
src/types/product/mapper.ts (1)

14-23: CategoryInfo 타입과 실제 반환 필드가 어긋납니다.
getCategoryInfo...metaisPlan/isMonthly까지 반환하지만 타입에는 없어 호출부에서 타입 안전하게 사용하기 어렵습니다. 타입을 확장하거나(아래) 반환을 label만으로 축소해 일관성을 맞춰 주세요.

♻️ 한 가지 정리안(타입 확장)
 export type CategoryInfo = {
   id: CategoryId;
   key: CategoryKey;
   label: string;
+  isPlan: boolean;
+  isMonthly: boolean;
 };

As per coding guidelines, 가독성과 안정성(에러/예외 처리)을 최우선으로 검토하고, 로직이 단순하고 직관적으로 읽히는지 확인.

src/lib/queryKeys.ts (1)

7-10: recommend.summary 쿼리키에 명시적인 스코프 세그먼트를 추가해 주세요.

현재 ['recommend', summaryId]는 TanStack Query 공식 가이드에서 권장하는 ['resource', 'scope', ...identifiers] 패턴을 따르지 않습니다. 같은 recommend 객체 내 me() 메서드가 이미 ['recommend', 'me']로 스코프를 명시하고 있는 만큼, summary도 동일하게 ['recommend', 'summary', summaryId]로 통일하면:

  • 캐시 무효화 전략이 명확해집니다invalidateQueries({ queryKey: ['recommend', 'summary'] })로 요약 관련 쿼리만 선택적으로 무효화 가능
  • 쿼리 계층 구조가 일관성 있게 유지됩니다 — 향후 새로운 스코프 추가 시 확장이 용이
  • 세그먼트 의미가 분명해집니다 — summaryId가 요약의 ID임이 명확함
♻️ 제안 변경
-    summary: (summaryId: number) => ['recommend', summaryId] as const,
+    summary: (summaryId: number) => ['recommend', 'summary', summaryId] as const,
src/services/recommend/recommendApi.ts (1)

25-34: axios 응답 타입을 명시해 타입 안정성을 높이세요.
axios 타입 정의에서 get<T>() 제네릭을 제공하므로, 응답 스키마를 컴파일 타임에 검증할 수 있습니다.

🔧 제안 변경
-export const fetchRecommendMe = async (): Promise<RecommendMeResponse> => {
-  const res = await apiClient.get('/api/v1/recommend/me');
+export const fetchRecommendMe = async (): Promise<RecommendMeResponse> => {
+  const res = await apiClient.get<RecommendMeResponse>('/api/v1/recommend/me');
   return res.data;
 };
 
 export const fetchRecommendSummary = async (
   summaryId: number,
 ): Promise<RecommendSummaryResponse> => {
-  const res = await apiClient.get(`/api/v1/recommend/${summaryId}`);
+  const res = await apiClient.get<RecommendSummaryResponse>(
+    `/api/v1/recommend/${summaryId}`,
+  );
   return res.data;
 };
src/hooks/recommend/useRecommendMe.ts (1)

10-48: 매핑 로직 중복을 분리해 확장성을 높여주세요.
useRecommendMeuseRecommendSummary의 매핑 로직이 거의 동일해 변경 시 누락 위험이 큽니다. 공통 mapper로 분리하고, 카테고리/상품 매핑에 대한 간단한 단위 테스트를 추가하는 것을 권장합니다.

src/app/(private)/recommend/page.tsx (1)

11-46: 컴포넌트 이름을 PascalCase로 통일해주세요.
React DevTools 가독성과 컨벤션 측면에서 page보다 RecommendPage 같은 이름이 더 명확합니다.

✏️ 제안 변경
-const page = () => {
+const RecommendPage = () => {
   const { data, isLoading, isError, refetch } = useRecommendMe();
   const categories = data?.categories ?? [];
   const products = data?.products ?? [];
@@
-export default page;
+export default RecommendPage;
src/hooks/summary/useBookmarkedSummaryList.ts (1)

32-43: 키워드 파싱 로직 유틸리티 함수로 추출 권장

이 키워드 파싱 IIFE가 useSummaryList.tssrc/app/(private)/bookmark/page.tsx에서도 유사하게 반복됩니다. 유지보수성과 DRY 원칙을 위해 공통 유틸리티 함수로 추출하면 좋겠습니다.

♻️ 유틸리티 함수 추출 제안
// src/utils/parseKeywords.ts
export const parseKeywords = (keywords: unknown): string[] => {
  if (Array.isArray(keywords)) return keywords;
  if (typeof keywords === 'string') {
    try {
      const parsed = JSON.parse(keywords);
      return Array.isArray(parsed) ? parsed : [keywords];
    } catch {
      return [keywords];
    }
  }
  return [];
};

그 후 훅에서:

-        keywords: (() => {
-          if (Array.isArray(item.keywords)) return item.keywords;
-          if (typeof item.keywords === 'string') {
-            try {
-              const parsed = JSON.parse(item.keywords);
-              return Array.isArray(parsed) ? parsed : [item.keywords];
-            } catch {
-              return [item.keywords];
-            }
-          }
-          return [];
-        })(),
+        keywords: parseKeywords(item.keywords),
src/app/(private)/summary/_components/SummarySuccessPage.tsx (1)

144-174: 추천 상품 섹션 컴포넌트 분리 고려

SummarySuccessPage가 북마크, 상담 주제, 핵심 요약, 추천 상품 등 여러 섹션을 담당하고 있습니다. Clean Architecture 관점에서 추천 상품 섹션을 별도 컴포넌트로 분리하면 테스트와 유지보수가 용이해집니다.

♻️ 컴포넌트 분리 예시
// RecommendProductsSection.tsx
interface RecommendProductsSectionProps {
  summaryId: number;
}

export const RecommendProductsSection = ({ summaryId }: RecommendProductsSectionProps) => {
  const { data, isLoading, isError, refetch } = useRecommendSummary(summaryId);
  const products = data?.products ?? [];

  return (
    <div className="mt-6 px-4">
      <div className="mb-2 flex items-center gap-2">
        <Image src={BoxText} alt="" width={15} height={15} />
        <h2 className="text-sm font-semibold">추천 상품</h2>
      </div>
      {/* ... 조건부 렌더링 로직 */}
    </div>
  );
};
src/app/(private)/summary/bookmarks/page.tsx (1)

26-36: 헤더 컴포넌트 중복 제거 권장

동일한 헤더 JSX가 4개의 조건부 렌더링 분기에서 반복됩니다. 헤더를 한 번만 렌더링하고 콘텐츠 영역만 조건부로 처리하면 유지보수성이 향상됩니다.

♻️ 리팩토링 제안
 export default function BookmarkedSummaryPage() {
   const router = useRouter();
   const { data: me, isLoading: meLoading, isError: meError } = useMe();
   const userId = me?.id;

   const {
     data: bookmarked,
     isLoading: listLoading,
     isError: listError,
   } = useBookmarkedSummaryList(userId);

+  const renderContent = () => {
+    if (meLoading || listLoading) {
+      return <div className="py-12 text-center text-sm text-gray-400">북마크 불러오는 중...</div>;
+    }
+    if (meError || !Number.isFinite(userId)) {
+      return <div className="py-12 text-center text-sm text-gray-400">로그인이 필요합니다</div>;
+    }
+    if (listError) {
+      return <div className="py-12 text-center text-sm text-gray-400">북마크한 상담내역을 불러오지 못했습니다...</div>;
+    }
+    return (
+      <div className="flex flex-col gap-6 pt-6 pb-24">
+        {/* 정상 렌더링 로직 */}
+      </div>
+    );
+  };

   return (
     <>
       <div className="relative flex h-14 items-center bg-white">
         <button
           type="button"
           aria-label="뒤로가기"
           onClick={() => router.push('/mypage')}
           className="ml-4 flex items-center"
         >
           <GoBack />
         </button>
         <p className="absolute left-1/2 -translate-x-1/2 text-sm font-semibold">북마크 상담</p>
       </div>
+      {renderContent()}
       <BottomNav />
     </>
   );
 }
src/app/(private)/summary/[summaryId]/recommended-products/page.tsx (1)

22-37: 상태 분기(로딩/에러/빈/성공) 테스트가 필요합니다.

UI 상태가 많아 회귀 위험이 있습니다. 대안: React Testing Library로 각 상태 스냅샷/동작 테스트를 추가하세요. 장점: 회귀 방지, 안정성 향상. 단점: 테스트 코드가 늘어납니다.

Comment on lines +24 to +30
{isError && (
<div className="px-6 py-4 text-sm text-red-600">
Failed to load recommendations.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
Retry
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

에러 상태 메시지 i18n + 접근성 알림을 추가해주세요.
현재 문구가 영어라 UI 언어와 불일치하고, 스크린리더 알림도 없습니다. role="alert"/aria-live 적용과 한글 문구를 권장합니다.

♿ 제안 변경
-      {isError && (
-        <div className="px-6 py-4 text-sm text-red-600">
-          Failed to load recommendations.
-          <button type="button" className="ml-2 underline" onClick={() => refetch()}>
-            Retry
-          </button>
-        </div>
-      )}
+      {isError && (
+        <div className="px-6 py-4 text-sm text-red-600" role="alert" aria-live="polite">
+          추천 항목을 불러오지 못했습니다.
+          <button type="button" className="ml-2 underline" onClick={() => refetch()}>
+            다시 시도
+          </button>
+        </div>
+      )}
📝 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
{isError && (
<div className="px-6 py-4 text-sm text-red-600">
Failed to load recommendations.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
Retry
</button>
</div>
{isError && (
<div className="px-6 py-4 text-sm text-red-600" role="alert" aria-live="polite">
추천 항목을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
다시 시도
</button>
</div>
)}

Comment on lines +32 to +39
{!isLoading &&
!isError &&
categories.map((i) => (
<ProductList
key={i.id}
category={i}
products={products.filter((p) => p.categoryId === i.id)}
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

데이터가 비어 있을 때 빈 상태 UI를 추가해주세요.
현재는 categories가 비어 있으면 아무 것도 렌더링되지 않아 사용자에게 원인을 전달하기 어렵습니다.

🧭 제안 변경
-      {!isLoading &&
-        !isError &&
-        categories.map((i) => (
+      {!isLoading && !isError && categories.length === 0 && (
+        <div className="px-6 py-4 text-sm text-gray-400">추천 항목이 없습니다.</div>
+      )}
+      {!isLoading &&
+        !isError &&
+        categories.map((i) => (
           <ProductList
             key={i.id}
             category={i}
             products={products.filter((p) => p.categoryId === i.id)}
           />
         ))}
📝 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
{!isLoading &&
!isError &&
categories.map((i) => (
<ProductList
key={i.id}
category={i}
products={products.filter((p) => p.categoryId === i.id)}
/>
{!isLoading && !isError && categories.length === 0 && (
<div className="px-6 py-4 text-sm text-gray-400">추천 항목이 없습니다.</div>
)}
{!isLoading &&
!isError &&
categories.map((i) => (
<ProductList
key={i.id}
category={i}
products={products.filter((p) => p.categoryId === i.id)}
/>
🤖 Prompt for AI Agents
In `@src/app/`(private)/recommend/page.tsx around lines 32 - 39, When !isLoading
&& !isError and categories is empty the page currently renders nothing; update
the render logic in page.tsx around the block using categories.map(...) to
detect categories.length === 0 and render a concise empty-state UI (e.g., an
EmptyState component or a message/card with icon and a call-to-action) instead
of mapping; keep the existing ProductList rendering for the non-empty case and
reuse products.filter((p) => p.categoryId === i.id) as before.

Comment on lines +156 to +163
{recommendError && (
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-red-500">
추천 상품을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetchRecommend()}>
다시 시도
</button>
</div>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

에러 상태 UI 개선 필요

  1. refetchRecommend()가 Promise를 반환하지만 핸들링되지 않았습니다.
  2. 접근성 향상을 위해 버튼에 aria-label을 추가하면 좋겠습니다.
🛠️ 개선 제안
 {recommendError && (
   <div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-red-500">
     추천 상품을 불러오지 못했습니다.
-    <button type="button" className="ml-2 underline" onClick={() => refetchRecommend()}>
+    <button
+      type="button"
+      className="ml-2 underline"
+      aria-label="추천 상품 다시 불러오기"
+      onClick={() => void refetchRecommend()}
+    >
       다시 시도
     </button>
   </div>
 )}
📝 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
{recommendError && (
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-red-500">
추천 상품을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetchRecommend()}>
다시 시도
</button>
</div>
)}
{recommendError && (
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-red-500">
추천 상품을 불러오지 못했습니다.
<button
type="button"
className="ml-2 underline"
aria-label="추천 상품 다시 불러오기"
onClick={() => void refetchRecommend()}
>
다시 시도
</button>
</div>
)}
🤖 Prompt for AI Agents
In `@src/app/`(private)/summary/_components/SummarySuccessPage.tsx around lines
156 - 163, The error UI currently renders when recommendError is true but calls
refetchRecommend() without handling its returned Promise and the retry button
lacks accessibility labeling; update the onClick handler in SummarySuccessPage
to call refetchRecommend and handle the Promise (e.g., await in an async handler
or use .catch to handle errors and optionally set loading state), and add an
appropriate aria-label to the retry button (e.g., aria-label="Retry loading
recommended products") so screen readers can describe the action.

Comment on lines +11 to +20
const params = useParams();
const rawId = Array.isArray(params?.summaryId) ? params.summaryId[0] : params?.summaryId;
const summaryId = Number(rawId);
const safeId = Number.isFinite(summaryId) ? summaryId : 0;

const { data: summaryData } = useSummaryDetail(safeId);
const { data: recommendData, isLoading, isError, refetch } = useRecommendSummary(safeId);
const products = recommendData?.products ?? [];

const title = summaryData?.title ?? '추천 상품';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

유효하지 않은 summaryId가 API 호출을 유발합니다.

safeId0일 때 useSummaryDetail/api/summaries/0을 호출할 수 있어 불필요한 에러/로그가 발생합니다. 대안: 유효성 플래그로 NaN을 전달해 쿼리를 비활성화하고, 잘못된 접근은 즉시 UX로 안내하세요. 장점: 잘못된 경로에서 불필요한 네트워크/에러를 차단합니다. 단점: 초기 가드 로직이 조금 늘어납니다.

✅ 제안 수정안
  const rawId = Array.isArray(params?.summaryId) ? params.summaryId[0] : params?.summaryId;
  const summaryId = Number(rawId);
- const safeId = Number.isFinite(summaryId) ? summaryId : 0;
+ const isValidId = Number.isInteger(summaryId) && summaryId > 0;
+ const safeId = isValidId ? summaryId : Number.NaN;

  const { data: summaryData } = useSummaryDetail(safeId);
  const { data: recommendData, isLoading, isError, refetch } = useRecommendSummary(safeId);
  const products = recommendData?.products ?? [];
+ 
+ if (!isValidId) {
+   return <div className="mx-7.5 my-4 text-sm text-red-500">잘못된 접근입니다.</div>;
+ }
📝 Committable suggestion

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

Suggested change
const params = useParams();
const rawId = Array.isArray(params?.summaryId) ? params.summaryId[0] : params?.summaryId;
const summaryId = Number(rawId);
const safeId = Number.isFinite(summaryId) ? summaryId : 0;
const { data: summaryData } = useSummaryDetail(safeId);
const { data: recommendData, isLoading, isError, refetch } = useRecommendSummary(safeId);
const products = recommendData?.products ?? [];
const title = summaryData?.title ?? '추천 상품';
const params = useParams();
const rawId = Array.isArray(params?.summaryId) ? params.summaryId[0] : params?.summaryId;
const summaryId = Number(rawId);
const isValidId = Number.isInteger(summaryId) && summaryId > 0;
const safeId = isValidId ? summaryId : Number.NaN;
const { data: summaryData } = useSummaryDetail(safeId);
const { data: recommendData, isLoading, isError, refetch } = useRecommendSummary(safeId);
const products = recommendData?.products ?? [];
if (!isValidId) {
return <div className="mx-7.5 my-4 text-sm text-red-500">잘못된 접근입니다.</div>;
}
const title = summaryData?.title ?? '추천 상품';
🤖 Prompt for AI Agents
In `@src/app/`(private)/summary/[summaryId]/recommended-products/page.tsx around
lines 11 - 20, Currently safeId defaults to 0 which triggers useSummaryDetail
and useRecommendSummary to call APIs for id 0; change the logic to treat invalid
summaryId as disabled by deriving a boolean like isValidId =
Number.isFinite(summaryId) && summaryId > 0 and pass that to the hooks (e.g.,
via query options or an "enabled" prop) so neither useSummaryDetail(safeId) nor
useRecommendSummary(safeId) runs when isValidId is false, and render an
immediate UX fallback (redirect, error message, or NotFound) when isValidId is
false instead of letting the hooks fetch with id 0; update references to rawId,
summaryId, safeId, useSummaryDetail and useRecommendSummary accordingly.

Comment on lines +25 to +35
{isLoading && <div className="mt-4 text-sm text-gray-400">추천 상품 불러오는 중...</div>}
{isError && (
<div className="mt-4 text-sm text-red-500">
추천 상품을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
다시 시도
</button>
</div>
)}
{!isLoading && !isError && products.length === 0 && (
<div className="mt-4 text-sm text-gray-400">추천 상품이 없습니다.</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

로딩/에러/빈 상태에 접근성 시그널이 부족합니다.

현재 메시지는 스크린 리더에 즉시 전달되지 않을 수 있습니다. 대안: role="status"/role="alert"aria-live를 추가하세요. 장점: 접근성 향상, 상태 변화 인지 개선. 단점: 마크업이 약간 길어집니다.

♿ 제안 수정안
- {isLoading && <div className="mt-4 text-sm text-gray-400">추천 상품 불러오는 중...</div>}
+ {isLoading && (
+   <div className="mt-4 text-sm text-gray-400" role="status" aria-live="polite">
+     추천 상품 불러오는 중...
+   </div>
+ )}

- {isError && (
-   <div className="mt-4 text-sm text-red-500">
+ {isError && (
+   <div className="mt-4 text-sm text-red-500" role="alert" aria-live="assertive">
      추천 상품을 불러오지 못했습니다.
      <button type="button" className="ml-2 underline" onClick={() => refetch()}>
        다시 시도
      </button>
    </div>
  )}

- {!isLoading && !isError && products.length === 0 && (
-   <div className="mt-4 text-sm text-gray-400">추천 상품이 없습니다.</div>
- )}
+ {!isLoading && !isError && products.length === 0 && (
+   <div className="mt-4 text-sm text-gray-400" role="status" aria-live="polite">
+     추천 상품이 없습니다.
+   </div>
+ )}
📝 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
{isLoading && <div className="mt-4 text-sm text-gray-400">추천 상품 불러오는 중...</div>}
{isError && (
<div className="mt-4 text-sm text-red-500">
추천 상품을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
다시 시도
</button>
</div>
)}
{!isLoading && !isError && products.length === 0 && (
<div className="mt-4 text-sm text-gray-400">추천 상품이 없습니다.</div>
{isLoading && (
<div className="mt-4 text-sm text-gray-400" role="status" aria-live="polite">
추천 상품 불러오는 ...
</div>
)}
{isError && (
<div className="mt-4 text-sm text-red-500" role="alert" aria-live="assertive">
추천 상품을 불러오지 못했습니다.
<button type="button" className="ml-2 underline" onClick={() => refetch()}>
다시 시도
</button>
</div>
)}
{!isLoading && !isError && products.length === 0 && (
<div className="mt-4 text-sm text-gray-400" role="status" aria-live="polite">
추천 상품이 없습니다.
</div>
)}
🤖 Prompt for AI Agents
In `@src/app/`(private)/summary/[summaryId]/recommended-products/page.tsx around
lines 25 - 35, Wrap the status messages rendered in the conditional blocks for
isLoading, isError, and empty products (the JSX shown under the isLoading,
isError, and products.length === 0 checks in page.tsx) in accessible live
regions by adding appropriate ARIA attributes: use role="status" with
aria-live="polite" (and aria-atomic="true") for non-critical info like the
loading and empty messages, and use role="alert" with aria-live="assertive" for
the error block so screen readers announce it immediately; update the
corresponding div elements (the ones containing "추천 상품 불러오는 중...", the error
message with the retry button, and "추천 상품이 없습니다.") to include these attributes.

Comment on lines +7 to +11
type RecommendCardProps = {
product: ProductProps;
best: string;
}) => {
return (
<div className={`flex w-200 max-w-200 flex-col rounded-2xl ${best}`}>
<div className="mx-10 my-5">
<div className="text-[14px] font-semibold">{product.name}</div>
<div className="text-primary-500 mb-2 text-[20px] font-bold">
{is_monthly ? '월 ' : null}
{product.price.toLocaleString()}원{product.is_sale ? '부터' : null}
is_monthly: boolean;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 1. is_monthly 사용처 전체 검색
rg -n "is_monthly" --type tsx --type ts

# 2. 해당 컴포넌트 파일의 전체 내용 확인
wc -l src/components/counseling-recommend/recommend-card/index.tsx

Repository: U-nify/Unity-client

Length of output: 171


🏁 Script executed:

# 1. Search for is_monthly without type filter
rg "is_monthly" -n

# 2. Read the complete component file
cat -n src/components/counseling-recommend/recommend-card/index.tsx

Repository: U-nify/Unity-client

Length of output: 1885


is_monthly 타입에만 정의되고 실제로는 사용하지 않습니다.

컴포넌트의 구조적 문제입니다. 13줄의 구조 분해(destructuring)에서 is_monthly가 빠져 있고, 컴포넌트 본문 어디에도 참조되지 않습니다. 반면 부모 컴포넌트(recommend-cardlist)에서는 이미 is_monthly={true}로 전달되고 있습니다.

이는 불필요한 props 규약이 유지되어 API가 혼란스러워지고, 향후 유지보수 시 개발자가 "이 prop이 뭐하는 건데?" 하며 불필요한 추적을 하게 됩니다.

제거 방법:

  1. 타입 정의에서 is_monthly 필드 삭제
  2. 부모 컴포넌트(recommend-cardlist)에서 is_monthly={true} 제거

이렇게 정리하면 API 계약이 명확해지고 재사용성이 높아집니다.

🤖 Prompt for AI Agents
In `@src/components/counseling-recommend/recommend-card/index.tsx` around lines 7
- 11, Remove the unused is_monthly prop from the RecommendCard API: delete
is_monthly from the RecommendCardProps type and remove it from the destructuring
in the RecommendCard component (where props are unpacked) so the component no
longer declares or expects it; then remove is_monthly={true} from the parent
component recommend-cardlist where RecommendCard is instantiated. Also search
for any other references to is_monthly in this component and the parent and
delete or refactor them if present to ensure no dangling references remain.

Comment on lines +11 to +12
<Link href={product.link || '#'} target="_blank" rel="noopener noreferrer">
<div className="bg-white-light flex h-full w-full cursor-pointer flex-col items-center justify-between gap-1 rounded-[20px] p-3 shadow-[0_0_4px_rgba(0,0,0,0.25)]">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# ProductGoods 컴포넌트 파일 확인
cat -n src/components/recommend/product-goods/index.tsx

Repository: U-nify/Unity-client

Length of output: 1584


🏁 Script executed:

# product 타입 정의 찾기
rg -n "interface.*[Pp]roduct\|type.*[Pp]roduct" src --type ts --type tsx -A 5

Repository: U-nify/Unity-client

Length of output: 90


🏁 Script executed:

# 파일 크기 확인
wc -l src/components/recommend/product-goods/index.tsx

Repository: U-nify/Unity-client

Length of output: 114


🏁 Script executed:

# ProductProps 타입 정의 찾기
cat -n src/types/product/dto.ts 2>/dev/null || find src -name "*.ts" -o -name "*.tsx" | xargs grep -l "ProductProps" | head -1 | xargs cat -n

Repository: U-nify/Unity-client

Length of output: 735


🏁 Script executed:

# ProductProps가 export된 파일 위치 정확히 찾기
find src -path "*types/product/dto*" -type f

Repository: U-nify/Unity-client

Length of output: 87


🏁 Script executed:

# 만약 dto 파일이 없다면 types 디렉토리 구조 확인
find src/types -type f -name "*.ts" 2>/dev/null | head -20

Repository: U-nify/Unity-client

Length of output: 279


product.link가 없을 때 #으로 폴백하는 것은 접근성과 사용자 경험에 문제가 있습니다.

link: string | null이므로 null 값이 발생할 수 있고, 현재 코드에서는 이때 #으로 새 탭을 여려고 시도합니다. 이는 스크린 리더에는 유효한 링크처럼 보이지만 실제 목적지가 없어서 사용자에게 혼란을 줍니다.

link 유무에 따라 조건부로 렌더링하세요:

// product.link가 있을 때만 Link 사용, 없으면 비활성 카드로
return product.link ? (
  <Link href={product.link} target="_blank" rel="noopener noreferrer">
    <ProductCard />
  </Link>
) : (
  <ProductCard />
);

이렇게 하면:

  • 접근성: 링크가 실제로 존재할 때만 <a> 태그 생성 → 스크린 리더가 정확히 인식
  • UX: 링크 없는 카드는 시각적으로 구분하거나 비활성 스타일 적용 가능
  • 유지보수성: 데이터 부재 상황이 명시적으로 처리됨
🤖 Prompt for AI Agents
In `@src/components/recommend/product-goods/index.tsx` around lines 11 - 12,
Replace the unconditional Link wrapper around the product card with conditional
rendering based on product.link: when product.link is truthy, render the Link
(preserving target="_blank" and rel="noopener noreferrer") wrapping the card
markup; when product.link is null/undefined, render the same product card markup
without Link and apply a disabled/visually-distinct style and ARIA hint (e.g.,
aria-disabled or role) to the outer div so the card remains accessible—update
the component using the existing Link usage and the product.link check around
the card container.

import type { ProductProps } from '@/types/product/dto';

export const ProductPlans = ({ product, monthly }: { product: ProductProps; monthly: boolean }) => {
export const ProductPlans = ({ product }: { product: ProductProps; monthly: boolean }) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the ProductPlans component file
find . -path "*/recommend/product-plans/index.tsx" -type f

Repository: U-nify/Unity-client

Length of output: 113


🏁 Script executed:

# Read the ProductPlans component file
cat -n "./src/components/recommend/product-plans/index.tsx"

Repository: U-nify/Unity-client

Length of output: 1220


🏁 Script executed:

# Search for ProductPlans usage
rg -n "ProductPlans" src

Repository: U-nify/Unity-client

Length of output: 205


🏁 Script executed:

# Check the product-plans directory structure
ls -la "src/components/recommend/product-plans/"

# Search for imports from product-plans directory
rg -n "from.*product-plans" src

# Search more broadly for any ProductPlans usage
rg -n "ProductPlans" . --include="*.tsx" --include="*.ts" --include="*.jsx" --include="*.js"

Repository: U-nify/Unity-client

Length of output: 688


🏁 Script executed:

# Correct rg syntax - search for ProductPlans usage with proper type filters
rg "ProductPlans" -t tsx -t ts -t jsx -t js .

Repository: U-nify/Unity-client

Length of output: 90


🏁 Script executed:

# Search for ProductPlans across all files
rg "ProductPlans" .

# Also search for the parent directory to see what uses product-plans
rg "product-plans" .

Repository: U-nify/Unity-client

Length of output: 205


🏁 Script executed:

# Check the recommend directory structure
ls -la "src/components/recommend/"

# Search for any exports from product-plans or recommend folder
rg "export.*product-plans" .
rg "export.*ProductPlans" .

# Check if there's an index file that might export these components
cat -n "src/components/recommend/index.ts" 2>/dev/null || cat -n "src/components/recommend/index.tsx" 2>/dev/null || echo "No index file found"

Repository: U-nify/Unity-client

Length of output: 524


🏁 Script executed:

# Check for any dynamic imports or lazy loading
rg "product-plans|ProductPlans" . --include="*.json" --include="*.config.*"

# Check if used in test files
fd -e "test.tsx" -e "test.ts" -e "spec.tsx" -e "spec.ts" . | head -20

# Search in all files more comprehensively
find . -type f \( -name "*.tsx" -o -name "*.ts" -o -name "*.jsx" -o -name "*.js" \) -exec grep -l "ProductPlans\|product-plans" {} \;

Repository: U-nify/Unity-client

Length of output: 572


monthly prop을 선언했지만 사용하지 않고 있습니다.

컴포넌트 본문에서 monthly prop을 참조하는 곳이 없습니다. 추가로 확인한 결과, 이 ProductPlans 컴포넌트 자체가 현재 코드베이스 어디에서도 import되거나 사용되지 않는 상태입니다.

다음 중 하나를 진행해 주세요:

  • 미사용 컴포넌트라면 제거
  • 향후 사용 예정이라면, monthly prop을 실제 렌더링 로직에 반영하여 구현 완성

미사용 prop과 컴포넌트는 유지보수 비용을 증가시키고 혼란을 야기하므로, 명확한 의도를 가지고 정리하는 것이 확장성과 코드 품질 관점에서 중요합니다.

🤖 Prompt for AI Agents
In `@src/components/recommend/product-plans/index.tsx` at line 5, The ProductPlans
component declares a monthly prop that is never used and the component itself
appears unused; either remove the entire ProductPlans export to eliminate dead
code (delete the function and its export) or implement monthly into the
rendering logic of ProductPlans (update the ProductPlans function to accept {
product, monthly } and use monthly to toggle monthly vs yearly pricing display
or conditional JSX where pricing is rendered), and ensure any callers/imports
are updated or the export removed; reference the ProductPlans component and the
monthly prop when making the change.

Comment on lines +9 to 10
<Link href={product.link || '#'} target="_blank" rel="noopener noreferrer">
<div className="bg-white-light border-primary-500 flex h-21.25 w-50 cursor-pointer flex-col justify-between gap-1 truncate rounded-[20px] border-2 p-3">
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# ProductPlans 컴포넌트 파일 확인
find src -name "*product-plans*" -o -name "*ProductPlans*" 2>/dev/null | head -20

Repository: U-nify/Unity-client

Length of output: 101


🏁 Script executed:

# Link null/빈 값 확인 및 ProductPlans 사용처 검색
rg -n "product\.link" src --type tsx --type ts -B 2 -A 2

Repository: U-nify/Unity-client

Length of output: 90


🏁 Script executed:

# monthly 프로퍼티 검색
rg -n "monthly" src/components/recommend/product-plans -B 2 -A 2

Repository: U-nify/Unity-client

Length of output: 564


🏁 Script executed:

# 컴포넌트 파일 읽기
cat -n src/components/recommend/product-plans/index.tsx | head -50

Repository: U-nify/Unity-client

Length of output: 1220


🏁 Script executed:

# ProductProps 타입 정의 확인
rg -n "type ProductProps" src --type ts -A 10

Repository: U-nify/Unity-client

Length of output: 45


🏁 Script executed:

# ProductPlans 컴포넌트 사용 위치 확인
rg -n "ProductPlans" src --type ts -B 2 -A 2

Repository: U-nify/Unity-client

Length of output: 564


🏁 Script executed:

# product.link 패턴 검색 (null일 수 있는지 확인)
rg -n "href.*#" src -B 2 -A 2

Repository: U-nify/Unity-client

Length of output: 50378


monthly 프로퍼티가 미사용 상태입니다.
컴포넌트 시그니처에 정의되었지만 컴포넌트 본문에서 실제로 사용되지 않고 있습니다. 불필요한 프로퍼티는 향후 개발자에게 혼란을 유발할 수 있으므로, 제거하거나 실제 사용 목적이 있다면 구현해야 합니다.

링크가 없는데도 #로 새 탭이 열립니다.
product.link이 없을 때 href='#'로 폴백되지만 target="_blank"가 여전히 적용되어 사용자가 현재 페이지를 새 탭에서 다시 열게 됩니다. 링크가 없는 경우 다음과 같이 개선하는 것이 안전합니다:

✅ 개선 예시
-return (
-  <Link href={product.link || '#'} target="_blank" rel="noopener noreferrer">
+const content = (
   <div className="bg-white-light border-primary-500 flex h-21.25 w-50 cursor-pointer flex-col justify-between gap-1 truncate rounded-[20px] border-2 p-3">
     {/* ... */}
   </div>
+);
+
+return product.link ? (
+  <Link href={product.link} target="_blank" rel="noopener noreferrer">
+    {content}
   </Link>
-);
+) : (
+  content
+);
🤖 Prompt for AI Agents
In `@src/components/recommend/product-plans/index.tsx` around lines 9 - 10, The
component signature exposes a monthly prop but it is unused—either remove the
monthly prop from the ProductPlans component props or implement its intended
behavior (e.g., adjust displayed price or CSS) wherever pricing is rendered;
also change how Link is rendered so when product.link is falsy you do not set
href="#" with target="_blank"/rel (use a plain non-anchor wrapper or render Link
without target/rel when product.link is missing) — locate the Link usage and the
product.link reference in the component and update handling accordingly.

Comment on lines +16 to +37
const mapRecommendSummary = (data: RecommendSummaryResponse): RecommendSummaryMapped => {
const categories = new Map<number, CategoryInfo>();
const products: ProductProps[] = [];

data.items.forEach((item) => {
const categoryId = item.categoryId;
const category = getCategoryInfo(categoryId);
categories.set(category.id, category);

products.push({
productId: item.productId,
name: item.name,
desc: null,
categoryId,
content: null,
score: item.score ?? null,
rankNo: item.rankNo ?? null,
link: item.link,
img: item.img,
price: item.price,
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

예상치 못한 categoryId에 대한 방어가 필요합니다.
API가 신규 카테고리 ID를 내려주면 getCategoryInfo 내부에서 런타임 오류가 발생해 페이지가 깨질 수 있습니다. 안전한 가드/스킵 처리를 추가해 주세요.

🛡️ 예시 개선안
   data.items.forEach((item) => {
     const categoryId = item.categoryId;
-    const category = getCategoryInfo(categoryId);
-    categories.set(category.id, category);
+    let category: CategoryInfo | null = null;
+    try {
+      category = getCategoryInfo(categoryId);
+    } catch {
+      return; // invalid categoryId → skip
+    }
+    categories.set(category.id, category);
📝 Committable suggestion

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

Suggested change
const mapRecommendSummary = (data: RecommendSummaryResponse): RecommendSummaryMapped => {
const categories = new Map<number, CategoryInfo>();
const products: ProductProps[] = [];
data.items.forEach((item) => {
const categoryId = item.categoryId;
const category = getCategoryInfo(categoryId);
categories.set(category.id, category);
products.push({
productId: item.productId,
name: item.name,
desc: null,
categoryId,
content: null,
score: item.score ?? null,
rankNo: item.rankNo ?? null,
link: item.link,
img: item.img,
price: item.price,
});
});
const mapRecommendSummary = (data: RecommendSummaryResponse): RecommendSummaryMapped => {
const categories = new Map<number, CategoryInfo>();
const products: ProductProps[] = [];
data.items.forEach((item) => {
const categoryId = item.categoryId;
let category: CategoryInfo | null = null;
try {
category = getCategoryInfo(categoryId);
} catch {
return; // invalid categoryId → skip
}
categories.set(category.id, category);
products.push({
productId: item.productId,
name: item.name,
desc: null,
categoryId,
content: null,
score: item.score ?? null,
rankNo: item.rankNo ?? null,
link: item.link,
img: item.img,
price: item.price,
});
});
🤖 Prompt for AI Agents
In `@src/hooks/recommend/useRecommendSummary.ts` around lines 16 - 37,
mapRecommendSummary currently calls getCategoryInfo(item.categoryId) without
guarding against unknown/new category IDs which can throw or return invalid
data; update mapRecommendSummary to validate the result of getCategoryInfo (or
wrap the call in try/catch) and skip or provide a safe fallback when a category
is missing so the page won't crash: only call categories.set(category.id,
category) when category is non-null/has an id, and handle products for which
category lookup failed (e.g., skip adding the product, set categoryId to
null/safe default, and/or log a warning). Ensure changes are made inside
mapRecommendSummary and reference getCategoryInfo, categories (Map), and
products (ProductProps[]) so runtime errors are prevented.

@joonhyong joonhyong merged commit 38764a1 into develop Jan 29, 2026
6 checks passed
@joonhyong joonhyong deleted the URECA-76/Feat/recommend-api branch January 29, 2026 08:53
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.

2 participants