Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/(private)/call/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { CallComponent } from '@/components/call/CallComponent';
import { CallComponent } from '@/components/call/calling';

const page = () => {
return (
Expand Down
14 changes: 12 additions & 2 deletions src/app/(private)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useQueryClient } from '@tanstack/react-query';

import FileIcon from '@/assets/icons/summary/File1.png';
import ForwardIcon from '@/assets/icons/summary/Forward.png';
Expand All @@ -11,6 +12,8 @@ import Banner3 from '@/assets/images/banner/image3.png';
import { HomeBannerSlider } from '@/components/home/home-banner-slider';
import { BottomNav } from '@/components/layout/bottom-navigation';
import { Header } from '@/components/layout/header';
import { queryKeys } from '@/lib/queryKeys';
import { fetchRecommendMe } from '@/services/recommend/recommendApi';

const QUICK_ACTIONS = [
{
Expand Down Expand Up @@ -63,8 +66,15 @@ const HOME_BANNERS = [

const Home = () => {
const router = useRouter();

const handleRecommend = () => router.push('/recommend');
const queryClient = useQueryClient();

const handleRecommend = () => {
void queryClient.prefetchQuery({
queryKey: queryKeys.recommend.me(),
queryFn: fetchRecommendMe,
});
router.push('/recommend');
};
const handleSummary = () => router.push('/summary');

return (
Expand Down
44 changes: 30 additions & 14 deletions src/app/(private)/recommend/page.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
'use client';

import React from 'react';

import { BottomNav } from '@/components/layout/bottom-navigation';
import { Header } from '@/components/layout/header';
import { Loading } from '@/components/loading';
import { ProductList } from '@/components/recommend/product-list';
import {
dummyCategoriesApi,
dummyProductsApi,
} from '@/services/recommend/MockupRecommendApi.client';
import { useRecommendMe } from '@/hooks/recommend/useRecommendMe';

const page = () => {
const categories = dummyCategoriesApi;
const products = dummyProductsApi;
const { data, isLoading, isError, refetch } = useRecommendMe();
const categories = data?.categories ?? [];
const products = data?.products ?? [];

return (
<div className="mx-auto w-full overflow-hidden">
<div className="mx-auto w-full overflow-hidden pb-10">
<Header />
{categories.map((i) => (
<ProductList
key={i.category_id}
category={i}
products={products.filter((p) => p.category === i.code)}
/>
))}
{isLoading && (
<div className="mt-4">
<Loading />
</div>
)}
{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>
Comment on lines +24 to +30
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>
)}

)}
{!isLoading &&
!isError &&
categories.map((i) => (
<ProductList
key={i.id}
category={i}
products={products.filter((p) => p.categoryId === i.id)}
/>
Comment on lines +32 to +39
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.

))}
<BottomNav />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import { RecommendCardList } from '@/components/counseling-recommend/recommend-cardlist/RecommendCardList';
import { TitleCard } from '@/components/counseling-recommend/titlecard/TitleCard';
'use client';

import { useParams } from 'next/navigation';

import { RecommendCardList } from '@/components/counseling-recommend/recommend-cardlist';
import { TitleCard } from '@/components/counseling-recommend/title-card';
import { useRecommendSummary } from '@/hooks/recommend/useRecommendSummary';
import { useSummaryDetail } from '@/hooks/summary/useSummaryDetail';

const page = () => {
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 ?? '추천 상품';
Comment on lines +11 to +20
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.


return (
<div className="mx-7.5 my-4">
<TitleCard title="5G 요금제 변경 상담" />
<RecommendCardList />
<TitleCard title={title} />
{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>
Comment on lines +25 to +35
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.

)}
{!isLoading && !isError && products.length > 0 && <RecommendCardList products={products} />}
</div>
);
};
Expand Down
42 changes: 41 additions & 1 deletion src/app/(private)/summary/_components/SummarySuccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import Image from 'next/image';
import { useRouter } from 'next/navigation';

import { Bookmark } from 'lucide-react';

import BoxText from "@/assets/icons/summary/BoxText.png";
import GoBack from '@/assets/icons/header/chevron-left.svg';
import File1 from '@/assets/icons/summary/File1.png';
import Profile from '@/assets/icons/summary/Profile.png';
import Topic from '@/assets/icons/summary/Topic.png';
import { RecommendCardList } from '@/components/counseling-recommend/recommend-cardlist';
import { BottomNav } from '@/components/layout/bottom-navigation';
import { useMe } from '@/hooks/auth/useMe';
import { useRecommendSummary } from '@/hooks/recommend/useRecommendSummary';
import { useToggleBookmark } from '@/hooks/summary/useToggleBookmark';
import type { ApiSummaryDetail } from '@/types/summary/summary';

Expand All @@ -28,6 +30,13 @@ export const SummarySuccessPage = ({ data }: SummarySuccessPageProps) => {
const { summaryId, title, subject, keywords = [], points = [], createdAt, isBookmarked } = data;
const [bookmarked, setBookmarked] = useState(isBookmarked);
const { mutateAsync, isPending } = useToggleBookmark({ summaryId, userId });
const {
data: recommendData,
isLoading: recommendLoading,
isError: recommendError,
refetch: refetchRecommend,
} = useRecommendSummary(summaryId);
const recommendProducts = recommendData?.products ?? [];

const disabled = meLoading || meError || !Number.isFinite(userId) || isPending;

Expand Down Expand Up @@ -131,6 +140,37 @@ export const SummarySuccessPage = ({ data }: SummarySuccessPageProps) => {
</ul>
</div>
</div>
<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>

{recommendLoading && (
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-gray-400">
추천 상품 불러오는 중...
</div>
)}

{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>
)}
Comment on lines +155 to +162
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.


{!recommendLoading && !recommendError && recommendProducts.length === 0 && (
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] text-gray-400">
추천 상품이 없습니다.
</div>
)}

{!recommendLoading && !recommendError && recommendProducts.length > 0 && (
<RecommendCardList products={recommendProducts} />
)}
</div>
</div>

<BottomNav />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(private)/summary/bookmarks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';

import GoBack from '@/assets/icons/header/chevron-left.svg';
import { BottomNav } from '@/components/layout/bottom-navigation';
import { SummaryNavigateCard } from '@/components/summary/SummaryNavigateCard';
import { SummaryNavigateCard } from '@/components/summary/summary-navigate-card';
import { useMe } from '@/hooks/auth/useMe';
import { useBookmarkedSummaryList } from '@/hooks/summary/useBookmarkedSummaryList';

Expand Down
2 changes: 1 addition & 1 deletion src/app/(private)/summary/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Link from 'next/link';

import { BottomNav } from '@/components/layout/bottom-navigation';
import { Header } from '@/components/layout/header';
import { SummaryNavigateCard } from '@/components/summary/SummaryNavigateCard';
import { SummaryNavigateCard } from '@/components/summary/summary-navigate-card';
import { useMe } from '@/hooks/auth/useMe';
import { useSummaryList } from '@/hooks/summary/useSummaryList';
export default function SummaryPage() {
Expand Down
2 changes: 1 addition & 1 deletion src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import React, { useEffect } from 'react';

import { ErrorComponent } from '@/components/ui/fallback/ErrorComponent';
import { ErrorComponent } from '@/components/ui/fallback/error';

const error = ({ error }: { error: Error & { digest?: string }; reset: () => void }) => {
useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import React from 'react';

import { ErrorComponent } from '@/components/ui/fallback/ErrorComponent';
import { ErrorComponent } from '@/components/ui/fallback/error';

const NotFound = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import { useRouter } from 'next/navigation';

import Call from '@/assets/images/call/call.svg';
import Calling from '@/assets/images/call/mooner_calling.svg';
import { ErrorComponent } from '@/components/ui/fallback/error';
import { useAgora } from '@/hooks/call/useAgora';

import { Button } from '../ui/button';
import { ErrorComponent } from '../ui/fallback/ErrorComponent';
import { LoadingComponent } from '../ui/fallback/LoadingComponent';

import { AgoraFrequencyVisualizer } from './AgoraVisualizer';
import { Button } from '../../ui/button';
import { LoadingComponent } from '../../ui/fallback/loading';
import { AgoraFrequencyVisualizer } from '../agora-visualizer';

export const CallComponent = () => {
const [channel] = useState('room1');
Expand Down
57 changes: 29 additions & 28 deletions src/components/counseling-recommend/recommend-card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,39 @@ import Link from 'next/link';
import Check from '@/assets/images/recommend/check.svg';
import type { ProductProps } from '@/types/product/dto';

export const RecommendCard = ({
is_monthly,
product,
best,
}: {
is_monthly: boolean;
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;
};
Comment on lines +7 to +11
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.


export const RecommendCard = ({ product, best }: RecommendCardProps) => {
const priceLabel = typeof product.price === 'number' ? product.price.toLocaleString() : null;

const content = (
<div className={`grid w-[160px] rounded-2xl ${best} `}>
<div className="mx-4 my-4 grid gap-2">
<div className="line-clamp-3 text-[14px] font-semibold break-keep">{product.name}</div>
<div className="text-primary-500 text-[20px] font-bold">
{priceLabel ?? <span className="text-gray-400">TBD</span>}
</div>
<div className="grid gap-1">
{product.content?.split(', ').map((text, idx) => (
<div key={idx} className="grid grid-cols-[auto_1fr] items-start gap-1 text-sm">
<Check aria-hidden="true" focusable="false" />
<span>{text}</span>
</div>
))}
</div>
{product.content?.split(', ').map((text, idx) => (
<div key={idx} className="flex items-center">
<Check aria-hidden="true" focusable="false" />
&nbsp;{text}
</div>
))}
<Link
href={product.link}
target="_blank"
rel="noopener noreferrer"
className="hover:bg-gray-light border-gray-light mt-2 inline-flex w-full items-center justify-center rounded-md border bg-transparent px-4 py-2 text-sm text-black shadow-[0_0_2px_#eeeeee] transition-colors"
>
보러가기
</Link>
</div>
</div>
);

return product.link ? (
<Link href={product.link} target="_blank" rel="noopener noreferrer" className="block">
{content}
</Link>
) : (
content
);
};

This file was deleted.

20 changes: 20 additions & 0 deletions src/components/counseling-recommend/recommend-cardlist/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';

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

import { RecommendCard } from '../recommend-card';

export const RecommendCardList = ({ products }: { products: ProductProps[] }) => {
return (
<div className="mt-3 grid w-full grid-cols-3 justify-items-center gap-4 md:grid-cols-4 lg:grid-cols-5">
{products.map((product, idx) => (
<RecommendCard
key={product.productId}
is_monthly={true}
product={product}
best={idx === 0 ? 'border border-primary-500 bg-white' : 'border border-gray bg-white'}
/>
))}
</div>
);
};
Loading