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/components/layout/main-nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from 'next/link';

export default function MainNavBar() {
return (
<div className="font-pretendard sticky bottom-0 flex w-full items-center justify-around border-t border-[#c3c3c3] bg-white py-3 text-sm font-medium">
<div className="font-pretendard sticky bottom-0 z-50 flex w-full items-center justify-around border-t border-[#c3c3c3] bg-white py-3 text-sm font-medium">
<Link href={'/'} className="flex flex-col items-center">
<img src="/icons/home.svg" alt="홈으로 이동" width={30} height={30} />홈
</Link>
Expand Down
36 changes: 26 additions & 10 deletions src/components/product/product-card.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
import Link from 'next/link';
import { Product } from '@/types/domain/product';
import { ProductWithWishlist } from '@/types/domain/product';
import { WishlistButton } from './wishlist-button';

// 카드 UI 컴포넌트, 클릭 시 상세 페이지로 이동함.
interface ProductCardProps {
product: Product;
product: ProductWithWishlist;
detailFrom?: string;
showWishlistButton?: boolean;
}

export default function ProductCard({ product, detailFrom }: ProductCardProps) {
export default function ProductCard({
product,
detailFrom,
showWishlistButton = false,
}: ProductCardProps) {
const href = detailFrom
? `/product/${product.id}?from=${encodeURIComponent(detailFrom)}`
: `/product/${product.id}`;

return (
<Link href={href} className="block">
<div className="font-pretendard flex w-41 flex-col gap-1">
<img
src={product.thumbnailImageUrl}
alt={product.name}
width={164}
height={170}
className="h-[170px] w-[164px] object-cover"
/>
<div className="relative">
<img
src={product.thumbnailImageUrl}
alt={product.name}
width={164}
height={170}
className="h-[170px] w-[164px] object-cover"
/>
{showWishlistButton && (
<WishlistButton
productId={product.id}
initialIsLiked={product.isLiked || false}
initialWishlistId={product.wishlistId}
className="absolute top-2 right-2"
/>
)}
</div>
<span className="font-extrabold">{product.brandName}</span>
<span className="line-clamp-2 h-14 w-full overflow-hidden text-lg font-medium text-ellipsis">
{product.name}
Expand Down
7 changes: 3 additions & 4 deletions src/components/product/product-detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ProductReviewContent from '@/components/product/review/review-section';

import {
Product,
ProductWithWishlist,
MaterialDescription,
ProductOption,
} from '@/types/domain/product';
Expand Down Expand Up @@ -46,16 +47,14 @@ interface ProductDetailProps extends Omit<
imageUrls?: string[];
}

interface ProductDetailViewProps {
type ProductDetailViewProps = {
product: ProductDetailProps;
similarProducts: Product[];
userInfo: UserBodyInfo | null;
analysisData: SizeAnalysisResult | null;
productReviewSummary?: ReviewStatsData;
backHref?: string;
isLiked?: boolean;
wishlistId?: number;
}
} & Pick<ProductWithWishlist, 'isLiked' | 'wishlistId'>;

export default function ProductDetailView({
product,
Expand Down
32 changes: 23 additions & 9 deletions src/components/product/product-list-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { api } from '@/lib/api-client';
import { ProductSearchResult } from '@/types/domain/product';
import { ProductSortType } from '@/types/enums';
import { ProductList } from './product-list';
import { getMyWishlist } from '@/app/actions/wishlist';

interface ProductListContainerProps {
params: Promise<{ parentId: string; id: string }>;
Expand Down Expand Up @@ -35,22 +36,35 @@ export default async function ProductListContainer({
throw new Error('Invalid category ID');
}

const result = await api.get<ProductSearchResult>('/products', {
params: {
categoryId: safeCategoryId,
sortType: safeSortType,
page: safePage,
size: 20,
},
});
const [result, wishlistItems] = await Promise.all([
api.get<ProductSearchResult>('/products', {
params: {
categoryId: safeCategoryId,
sortType: safeSortType,
page: safePage,
size: 20,
},
}),
getMyWishlist(),
]);
Comment on lines +39 to +49
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

Promise.allPromise.allSettled 변경 권장 — 다른 컨테이너와 일관성 및 탄력성 확보.

recommend-product-container.tsxrecommended-brand-container.tsxPromise.allSettled를 사용하여 위시리스트 조회 실패 시에도 상품 목록을 정상 표시합니다. 이 파일만 Promise.all을 사용하면, 위시리스트 API 장애(또는 비로그인 시 인증 에러)가 상품 목록 전체를 깨뜨릴 수 있습니다.

getMyWishlist 내부에 try/catch가 있지만, rethrowNextError가 Next.js redirect/notFound 에러를 다시 던지므로 Promise.all에서 전파될 수 있습니다.

🛡️ Promise.allSettled 적용
-  const [result, wishlistItems] = await Promise.all([
-    api.get<ProductSearchResult>('/products', {
-      params: {
-        categoryId: safeCategoryId,
-        sortType: safeSortType,
-        page: safePage,
-        size: 20,
-      },
-    }),
-    getMyWishlist(),
-  ]);
+  const [productsResult, wishlistResult] = await Promise.allSettled([
+    api.get<ProductSearchResult>('/products', {
+      params: {
+        categoryId: safeCategoryId,
+        sortType: safeSortType,
+        page: safePage,
+        size: 20,
+      },
+    }),
+    getMyWishlist(),
+  ]);
+
+  if (productsResult.status === 'rejected') {
+    throw productsResult.reason;
+  }
+
+  const result = productsResult.value;
+  const wishlistItems =
+    wishlistResult.status === 'fulfilled' ? wishlistResult.value : [];
📝 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 [result, wishlistItems] = await Promise.all([
api.get<ProductSearchResult>('/products', {
params: {
categoryId: safeCategoryId,
sortType: safeSortType,
page: safePage,
size: 20,
},
}),
getMyWishlist(),
]);
const [productsResult, wishlistResult] = await Promise.allSettled([
api.get<ProductSearchResult>('/products', {
params: {
categoryId: safeCategoryId,
sortType: safeSortType,
page: safePage,
size: 20,
},
}),
getMyWishlist(),
]);
if (productsResult.status === 'rejected') {
throw productsResult.reason;
}
const result = productsResult.value;
const wishlistItems =
wishlistResult.status === 'fulfilled' ? wishlistResult.value : [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-list-container.tsx` around lines 39 - 49, The
current Promise.all call in product-list-container.tsx (const [result,
wishlistItems] = await Promise.all([...])) can fail if getMyWishlist rejects;
change to Promise.allSettled to match other containers and make wishlist errors
non-fatal. Replace the Promise.all with Promise.allSettled, then extract the
product response from the first settled result (e.g., results[0].value) and
handle the wishlist result by checking results[1].status — use the wishlist
value when status === "fulfilled" and fall back to an empty list (or previous
behavior) when rejected, while still rethrowing Next.js redirect/notFound errors
propagated by getMyWishlist (rethrowNextError) if needed.


const wishlistByProductId = new Map(
wishlistItems.map((item) => [item.productId, item.wishlistId]),
);
const productsWithWishlist = result.products.content.map((product) => ({
...product,
isLiked: wishlistByProductId.has(product.id),
wishlistId: wishlistByProductId.get(product.id),
}));

const totalElements = result.products.totalElements;

return (
<ProductList
products={result.products.content}
products={productsWithWishlist}
totalElements={totalElements}
productDetailFrom={`/category/${parentId}/${subCategoryId}`}
showWishlistButton
/>
);
}
9 changes: 5 additions & 4 deletions src/components/product/product-list.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { Product } from '@/types/domain/product';
import { ProductWithWishlist } from '@/types/domain/product';
import ProductCard from './product-card';

// 상품 목록 데이터를 받아서 그리드 형태로 보여주는 목록 페이지 컴포넌트

interface ProductListProps {
products: Product[];
products: ProductWithWishlist[];
title?: string;
totalElements?: number;
productDetailFrom?: string;
showWishlistButton?: boolean;
}

export function ProductList({
products,
totalElements,
productDetailFrom,
showWishlistButton = false,
}: ProductListProps) {
const count = totalElements ?? products.length;

Expand All @@ -37,6 +37,7 @@ export function ProductList({
key={product.id}
product={product}
detailFrom={productDetailFrom}
showWishlistButton={showWishlistButton}
/>
))}
</div>
Expand Down
11 changes: 8 additions & 3 deletions src/components/recommend-brand/recommended-brand-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,24 @@ import { BrandWithProducts } from '@/types/domain/brand';
import RecommendedBrandHeader from './recommended-brand-header';
import RecommendedBrandGridCard from './recommended-brand-grid-card';
import { getLocaleFromDocument, t } from '@/lib/i18n';
import { ProductWithWishlist } from '@/types/domain/product';

type BrandWithWishlistProducts = Omit<BrandWithProducts, 'products'> & {
products: ProductWithWishlist[];
};

interface RecommendedBrandClientProps {
brands: BrandWithProducts[];
brands: BrandWithWishlistProducts[];
}

export default function RecommendedBrandClient({
brands,
}: RecommendedBrandClientProps) {
if (brands.length === 0) return null;

const [selectedIndex, setSelectedIndex] = useState<number>(0);
const locale = getLocaleFromDocument();

if (brands.length === 0) return null;

const currentBrand = brands[selectedIndex];
const currentProducts = currentBrand?.products || [];

Expand Down
28 changes: 26 additions & 2 deletions src/components/recommend-brand/recommended-brand-container.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import { api } from '@/lib/api-client';
import { BrandWithProducts } from '@/types/domain/brand';
import RecommendedBrandClient from './recommended-brand-client';
import { getMyWishlist } from '@/app/actions/wishlist';

export default async function RecommendedBrandContainer() {
const brands = await api.get<BrandWithProducts[]>('/brands/recommend');
const [brandsResult, wishlistResult] = await Promise.allSettled([
api.get<BrandWithProducts[]>('/brands/recommend', {
cache: 'force-cache',
next: { revalidate: 60 },
}),
getMyWishlist(),
]);

return <RecommendedBrandClient brands={brands} />;
const brands = brandsResult.status === 'fulfilled' ? brandsResult.value : [];
const wishlistItems =
wishlistResult.status === 'fulfilled' ? wishlistResult.value : [];

const wishlistByProductId = new Map(
wishlistItems.map((item) => [item.productId, item.wishlistId]),
);

const brandsWithWishlist = brands.map((brand) => ({
...brand,
products: brand.products.map((product) => ({
...product,
isLiked: wishlistByProductId.has(product.id),
wishlistId: wishlistByProductId.get(product.id),
})),
}));

return <RecommendedBrandClient brands={brandsWithWishlist} />;
}
27 changes: 18 additions & 9 deletions src/components/recommend-brand/recommended-brand-grid-card.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
import Image from 'next/image';
import Link from 'next/link';
import { Product } from '@/types/domain/product';
import { ProductWithWishlist } from '@/types/domain/product';
import { WishlistButton } from '../product/wishlist-button';

export default function RecommendedBrandGridCard({
product,
}: {
product: Product;
product: ProductWithWishlist;
}) {
const href = `/product/${product.id}?from=${encodeURIComponent('/')}`;

return (
<Link href={href} className="block">
<div className="font-pretendard flex w-41 flex-col gap-1">
<Image
src={product.thumbnailImageUrl}
alt={product.name}
width={164}
height={170}
className="h-[170px] w-[164px] object-cover"
/>
<div className="relative">
<Image
src={product.thumbnailImageUrl}
alt={product.name}
width={164}
height={170}
className="h-[170px] w-[164px] object-cover"
/>
<WishlistButton
productId={product.id}
initialIsLiked={product.isLiked || false}
initialWishlistId={product.wishlistId}
className="absolute top-2 right-2"
/>
</div>
<span className="font-extrabold">{product.brandName}</span>
<span className="line-clamp-2 h-14 w-full overflow-hidden text-lg font-medium text-ellipsis">
{product.name}
Expand Down
24 changes: 22 additions & 2 deletions src/components/recommend-carousel/recommend-product-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Product } from '@/types/domain/product';
import RecommendedProductCard from './recommended-product-card';
import { RecommendCarouselItem } from './recommend-carousel-item';
import { RecommendCarousel } from './recommend-carousel';
import { getMyWishlist } from '@/app/actions/wishlist';

interface RecommendProductContainerProps {
endpoint: string;
Expand All @@ -13,12 +14,31 @@ export default async function RecommendProductContainer({
endpoint,
heading,
}: RecommendProductContainerProps) {
const products = await api.get<Product[]>(endpoint);
const [productsResult, wishlistResult] = await Promise.allSettled([
api.get<Product[]>(endpoint),
getMyWishlist(),
]);

const products =
productsResult.status === 'fulfilled' ? productsResult.value : [];
const wishlistItems =
wishlistResult.status === 'fulfilled' ? wishlistResult.value : [];

const wishlistByProductId = new Map(
wishlistItems.map((item) => [item.productId, item.wishlistId]),
);

return (
<RecommendCarousel heading={heading}>
{products.map((product) => (
<RecommendCarouselItem key={product.id}>
<RecommendedProductCard productInfo={product} />
<RecommendedProductCard
productInfo={{
...product,
isLiked: wishlistByProductId.has(product.id),
wishlistId: wishlistByProductId.get(product.id),
}}
/>
</RecommendCarouselItem>
))}
</RecommendCarousel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import Image from 'next/image';
import Link from 'next/link';
import { Product } from '@/types/domain/product';
import { ProductWithWishlist } from '@/types/domain/product';
import { WishlistButton } from '../product/wishlist-button';

interface RecommendedProduct extends Product {
isLiked?: boolean;
}

export default function RecommendedProductCard({
productInfo,
}: {
productInfo: RecommendedProduct;
productInfo: ProductWithWishlist;
}) {
const href = `/product/${productInfo.id}?from=${encodeURIComponent('/')}`;
const hasDiscount = productInfo.discountRate && productInfo.discountRate > 0;
Expand Down Expand Up @@ -72,6 +68,7 @@ export default function RecommendedProductCard({
<WishlistButton
productId={productInfo.id}
initialIsLiked={productInfo.isLiked || false}
initialWishlistId={productInfo.wishlistId}
className="absolute top-2 right-2"
/>
</div>
Expand Down
Loading