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
19 changes: 14 additions & 5 deletions src/app/category/[parentId]/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import ProductListContainer from '@/components/product/product-list-container';
import { ProductFilterBar } from '@/components/product/product-filter-bar';
import ProductFilterBarContainer from '@/components/product/product-filter-bar-container';
import { Suspense } from 'react';

interface PageProps {
params: Promise<{ parentId: string; id: string }>;
searchParams: Promise<{ sortType?: string; page?: string }>;
searchParams: Promise<{
sortType?: string;
page?: string;
clothingSizes?: string | string[];
priceRange?: string | string[];
brandIds?: string | string[];
}>;
}

export default function ProductListPage({ params, searchParams }: PageProps) {
return (
<div className="mx-auto max-w-7xl px-4">
<Suspense
fallback={
<div className="mb-4 flex items-center justify-end">
<div className="h-10 w-24 animate-pulse rounded-md bg-gray-200" />
<div className="mb-4 flex items-center gap-2">
<div className="h-10 w-20 animate-pulse rounded-full bg-gray-200" />
<div className="h-10 w-20 animate-pulse rounded-full bg-gray-200" />
<div className="h-10 w-20 animate-pulse rounded-full bg-gray-200" />
<div className="h-10 w-20 animate-pulse rounded-full bg-gray-200" />
</div>
}
>
<ProductFilterBar />
<ProductFilterBarContainer params={params} searchParams={searchParams} />
</Suspense>

<Suspense
Expand Down
150 changes: 150 additions & 0 deletions src/components/product/product-filter-bar-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { getSubCategories } from '@/app/actions/category';
import { api } from '@/lib/api-client';
import { BrandWithProducts } from '@/types/domain/brand';
import { ProductSearchResult } from '@/types/domain/product';
import { ProductSortType } from '@/types/enums';
import { BrandFilterOption, ProductFilterBar } from './product-filter-bar';

interface ProductFilterBarContainerProps {
params: Promise<{ parentId: string; id: string }>;
searchParams: Promise<{
sortType?: string;
page?: string;
clothingSizes?: string | string[];
priceRange?: string | string[];
brandIds?: string | string[];
}>;
}

const PRICE_RANGE_PATTERN = /^\d+-\d+$/;

function normalizeBrandName(value: string) {
return value.trim().toLowerCase().replace(/\s+/g, '');
}

function normalizeArray(value?: string | string[]) {
if (Array.isArray(value)) {
return value.filter((item) => item.trim().length > 0);
}
if (typeof value === 'string' && value.trim().length > 0) {
return [value];
}
return [];
}

function normalizePriceRange(value?: string | string[]) {
if (Array.isArray(value)) {
return value[0] ?? '';
}
return value ?? '';
}

export default async function ProductFilterBarContainer({
params,
searchParams,
}: ProductFilterBarContainerProps) {
const [{ parentId, id: subCategoryId }, query] = await Promise.all([
params,
searchParams,
]);

const parsedCategoryId = Number(subCategoryId);
const parsedParentId = Number(parentId);
const safeCategoryId =
Number.isFinite(parsedCategoryId) && parsedCategoryId > 0
? parsedCategoryId
: null;

if (safeCategoryId === null) {
return <ProductFilterBar parentCategoryName="" availableBrands={[]} />;
}

const sortValues = Object.values(ProductSortType);
const safeSortType = sortValues.includes(query.sortType as ProductSortType)
? (query.sortType as ProductSortType)
: ProductSortType.POPULAR;

const sizeOptions = normalizeArray(query.clothingSizes).filter((size) =>
['XS', 'S', 'M', 'L', 'XL'].includes(size),
);

const rawPriceRange = normalizePriceRange(query.priceRange);
const safePriceRange = PRICE_RANGE_PATTERN.test(rawPriceRange)
? rawPriceRange
: '';

const [subCategories, result] = await Promise.all([
Number.isFinite(parsedParentId)
? getSubCategories(parsedParentId)
: Promise.resolve([]),
api.get<ProductSearchResult>('/products', {
params: {
categoryId: safeCategoryId,
sortType: safeSortType,
page: 0,
size: 36,
clothingSizes: sizeOptions.length > 0 ? sizeOptions : undefined,
priceRange: safePriceRange || undefined,
},
}),
]);

const parentCategoryName = subCategories[0]?.parentCategoryName ?? '';
const productItems = result.products.content as Array<{
brandName: string;
brandId?: number;
}>;
Comment on lines +93 to +96
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

🧩 Analysis chain

🏁 Script executed:

# Find Product type definition
find . -type f \( -name "*.ts" -o -name "*.tsx" \) | head -20 | xargs grep -l "interface Product\|type Product"

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

Length of output: 94


🏁 Script executed:

# Check the specific file and surrounding context
cat -n src/components/product/product-filter-bar-container.tsx | sed -n '85,105p'

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

Length of output: 789


🏁 Script executed:

# Search for brandName and brandId usage patterns
rg "brandName|brandId" -t ts -t tsx -B2 -A2 | head -50

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

Length of output: 94


🏁 Script executed:

# Look for type definitions directory
fd -t d "types|models|@types" --max-depth 3

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

Length of output: 77


🏁 Script executed:

# List types directory
ls -la src/types/

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

Length of output: 412


🏁 Script executed:

# Search for Product type in types directory
rg "interface Product|type Product" src/types/ -A 10

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

Length of output: 4525


🏁 Script executed:

# Check imports and setup in the product-filter-bar-container.tsx file
head -50 src/components/product/product-filter-bar-container.tsx

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

Length of output: 1461


🏁 Script executed:

# Search for result.products structure definition
rg "result\.products" src/components/product/product-filter-bar-container.tsx -B 5 -A 5

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

Length of output: 326


🏁 Script executed:

# Check Page type definition and how productItems is used
rg "interface Page|type Page" src/types/ -A 5

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

Length of output: 1184


🏁 Script executed:

# Check all uses of productItems in the file
cat -n src/components/product/product-filter-bar-container.tsx | sed -n '93,120p'

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

Length of output: 1281


불필요한 타입 단언 제거 및 Product 타입 활용

result.products.contentProduct[] 타입입니다. brandName은 이미 Product에 존재하므로 타입 단언이 필요 없습니다. 다만 brandIdProduct에 없고, 현재 코드의 Number.isFinite() 체크와 as number 캐스팅(113~114줄)은 필드 부재를 우회하는 패턴입니다.

선택지:

  • API 응답에 brandId가 포함되면 Product 타입을 확장하고 단언 제거
  • 포함되지 않으면 단언과 brandId 접근 로직 제거
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar-container.tsx` around lines 93 -
96, result.products.content is already Product[] so remove the unnecessary type
assertion on productItems and rely on the Product type; then either (A) if the
API truly returns brandId, extend the Product interface/type to include brandId
and remove the ad-hoc Number.isFinite() and "as number" casts around brandId
access, or (B) if brandId is not part of the response, remove any code that
accesses brandId (including the Number.isFinite() check and "as number" cast)
and use existing Product.brandName directly; update the productItems declaration
and adjust logic that references brandId (search for productItems,
result.products.content, and the Number.isFinite()/as number usage) accordingly.

const brandsFromProducts = Array.from(
new Set(
productItems
.map((product) => product.brandName.trim())
.filter((brandName) => brandName.length > 0),
),
);

const recommendedBrands = await api
.get<BrandWithProducts[]>('/brands/recommend', { params: { count: 200 } })
.catch(() => []);

const brandIdByNormalizedName = new Map<string, number>();
productItems.forEach((product) => {
const normalized = normalizeBrandName(product.brandName);
if (!normalized) return;
if (Number.isFinite(product.brandId) && (product.brandId as number) > 0) {
brandIdByNormalizedName.set(normalized, product.brandId as number);
}
});
recommendedBrands.forEach((brand) => {
const normalized = normalizeBrandName(brand.name);
if (!normalized || brandIdByNormalizedName.has(normalized)) return;
brandIdByNormalizedName.set(normalized, brand.id);
});

const mappedFromProducts = brandsFromProducts
.map((name) => {
const id = brandIdByNormalizedName.get(normalizeBrandName(name));
if (!id) return null;
return { id, name };
})
.filter((item): item is BrandFilterOption => item !== null);

const mappedFromRecommended: BrandFilterOption[] = recommendedBrands.map(
(brand) => ({ id: brand.id, name: brand.name }),
);

const availableBrands = Array.from(
new Map(
[...mappedFromProducts, ...mappedFromRecommended].map((brand) => [
String(brand.id),
brand,
]),
).values(),
).sort((a, b) => a.name.localeCompare(b.name, 'ko'));

return (
<ProductFilterBar
parentCategoryName={parentCategoryName}
availableBrands={availableBrands}
/>
);
}
Loading