Skip to content

feat: 카테고리 필터 바텀시트 UI 및 API 파라미터 정렬#79

Merged
Seoje1405 merged 4 commits intoIT-Cotato:developfrom
bk-git-hub:feat/product-filter
Feb 19, 2026
Merged

feat: 카테고리 필터 바텀시트 UI 및 API 파라미터 정렬#79
Seoje1405 merged 4 commits intoIT-Cotato:developfrom
bk-git-hub:feat/product-filter

Conversation

@bk-git-hub
Copy link
Copy Markdown
Contributor

📝 개요

카테고리 상품 목록 필터 UI를 바텀시트 기반으로 확장하고, API 파라미터를 백엔드 스펙(brandIds, clothingSizes, priceRange=min-max)에 맞게 정렬했습니다.

🚀 주요 변경 사항

  • 정렬/사이즈/브랜드/가격 필터 칩 + 바텀시트 UI 구현
  • 적용된 필터 태그(칩) 노출 및 개별 해제 동작 추가
  • 가격 필터를 단일 선택 + 직접입력(min-max) 방식으로 변경
  • 카테고리 목록 API 요청 파라미터를 brandIds, clothingSizes, priceRange 형식으로 변경
  • 페이지/컨테이너/필터바 간 searchParams 타입 및 쿼리 동기화 로직 정리

📸 스크린샷 (선택)

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

✅ 체크리스트

  • 빌드 테스트를 완료했나요? (pnpm -s exec tsc --noEmit)
  • 코드 컨벤션을 준수했나요?
  • 불필요한 console.log는 제거했나요?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 19, 2026

Walkthrough

상품 카테고리 페이지의 필터링 아키텍처를 개편합니다. ProductFilterBarContainer 컨테이너를 도입하여 필터 데이터(브랜드, 하위 카테고리명) 페칭을 담당하고, ProductFilterBar를 정적 컴포넌트에서 의류 사이즈·가격대·브랜드 필터를 지원하는 동적 UI 시스템으로 확장합니다. 쿼리 파라미터 검증 및 정규화 로직을 추가합니다.

Changes

Cohort / File(s) Summary
Filter Container Architecture
src/components/product/product-filter-bar-container.tsx
새 비동기 컨테이너 컴포넌트로 subcategory 및 product 데이터 페칭, 브랜드 정규화, availableBrands 도출을 담당합니다. 캐시 레이어(Promise.all 병렬화)로 성능 최적화합니다.
Filter UI System
src/components/product/product-filter-bar.tsx
멀티탭 필터 UI(사이즈, 브랜드, 가격)와 정렬 시트를 추가합니다. 임시 필터 상태, 동적 사이즈 옵션(TOP/BOTTOM), 브랜드 검색, 활성 필터 칩 표시, 커스텀 가격 입력 검증 등을 지원합니다.
Query Parameter Pipeline
src/app/category/[parentId]/[id]/page.tsx, src/components/product/product-list-container.tsx
searchParams 타입을 clothingSizes, priceRange, brandIds로 확장합니다. PRICE_RANGE_PATTERN 정규식, normalizeArray, normalizePriceRange 유틸로 입력값 검증 및 정규화를 수행합니다.

Sequence Diagram

sequenceDiagram
    participant Page as Page<br/>[parentId]/[id]
    participant Container as ProductFilterBar<br/>Container
    participant Fetcher as API Layer<br/>(subcategories,<br/>products)
    participant FilterBar as ProductFilterBar<br/>Component
    participant ListContainer as ProductList<br/>Container

    Page->>Container: params, searchParams<br/>(clothingSizes,<br/>priceRange, brandIds)
    Container->>Fetcher: fetch subcategories,<br/>fetch products<br/>(Promise.all)
    Fetcher-->>Container: subcategories[],<br/>products[]
    Container->>Container: derive parentCategoryName,<br/>extract & merge brands,<br/>resolve brandIds
    Container->>FilterBar: parentCategoryName,<br/>availableBrands[]
    FilterBar->>FilterBar: render filter tabs,<br/>sort sheet, bottom sheet
    FilterBar->>Page: navigateWithParams()<br/>(updated URL)
    Page->>ListContainer: searchParams update
    ListContainer->>Fetcher: fetch products<br/>(clothingSizes,<br/>priceRange, brandIds)
    Fetcher-->>ListContainer: filtered products[]
    ListContainer->>ListContainer: render product list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

✨ FEATURE, ♻️ REFACTOR

🚥 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
Title check ✅ Passed 제목은 변경사항의 핵심을 정확히 반영합니다. 바텀시트 UI 확장과 API 파라미터 정렬이 주요 변경이며, 제목이 이를 명확히 요약하고 있습니다.
Description check ✅ Passed 설명은 변경사항과 일관되게 바텀시트 기반 필터 UI 구현, API 파라미터 형식 변경(brandIds, clothingSizes, priceRange)을 다루고 있습니다.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@Seoje1405 Seoje1405 merged commit c151271 into IT-Cotato:develop Feb 19, 2026
1 check was pending
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (1)
src/components/product/product-filter-bar.tsx (1)

484-552: ⚠️ Potential issue | 🟠 Major

접근성(a11y) 이슈: 바텀시트에 키보드 dismiss 및 포커스 트랩 없음

두 바텀시트(sort, filters) 모두 Escape 키 핸들러와 포커스 트랩이 없습니다. 키보드 사용자가 시트를 닫을 수 없고, 포커스가 시트 밖으로 빠져나갈 수 있습니다.

최소한 Escape 키로 시트를 닫는 useEffect를 추가하세요:

🔧 Escape 키 핸들러 추가 예시
+  useEffect(() => {
+    if (!openSheet) return;
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') setOpenSheet(null);
+    };
+    document.addEventListener('keydown', handleKeyDown);
+    return () => document.removeEventListener('keydown', handleKeyDown);
+  }, [openSheet]);
+
   return (
     <>

또한 바텀시트 컨테이너에 role="dialog", aria-modal="true" 속성을 추가하여 스크린 리더 지원을 개선하세요.

Also applies to: 554-665

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar.tsx` around lines 484 - 552, The
bottom-sheet panes for openSheet === 'sort' (and the similar 'filters' pane)
lack keyboard dismiss and a focus trap; add a useEffect in the ProductFilterBar
component that listens for Escape and calls setOpenSheet(null), add
role="dialog" and aria-modal="true" to the sheet container element, and
implement a focus-trap around the sheet (use a ref for the sheet container and
on keydown handle Tab/Shift+Tab to keep focus cycling inside, or integrate a
small focus-trap utility/library) so keyboard users cannot tab out of the open
sheet and can close it with Escape; ensure this change covers the same logic
used by handleSortChange and the setOpenSheet calls for both 'sort' and
'filters'.
🧹 Nitpick comments (2)
src/components/product/product-filter-bar.tsx (1)

253-420: 대형 컴포넌트 분리 권장: renderFilterContent 및 바텀시트 JSX

이 파일은 약 668줄이며, 필터 탭 콘텐츠(size, price, brand), 정렬 바텀시트, 필터 바텀시트, 칩 표시 등 여러 책임이 하나의 컴포넌트에 집중되어 있습니다. renderFilterContent 내부의 각 탭을 별도 컴포넌트(SizeFilterPanel, PriceFilterPanel, BrandFilterPanel)로 추출하면 가독성과 테스트 용이성이 크게 향상됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar.tsx` around lines 253 - 420, The
renderFilterContent function in this large component centralizes three separate
UI responsibilities (size, price, brand); split each tab into its own
presentational component—create SizeFilterPanel, PriceFilterPanel, and
BrandFilterPanel (or similarly named) and move the JSX/logic for sizeOptions,
PRICE_OPTIONS + custom price inputs, and brand list/filtering respectively out
of renderFilterContent, keeping only a simple switch in renderFilterContent that
returns <SizeFilterPanel ... />, <PriceFilterPanel ... />, or <BrandFilterPanel
... /> and pass down needed props/state setters (e.g., tempSizes, setTempSizes,
tempPriceRange, setTempPriceRange, customMinPrice, setCustomMinPrice,
customMaxPrice, setCustomMaxPrice, brandKeyword, filteredBrands, tempBrandIds,
setTempBrandIds, toggleValue, normalizeNumberInput) to preserve behavior and
tests; ensure each new component is functional, contained in the same folder or
a new subfile, and that all existing handlers (onClick/onChange) are wired
through props so behavior remains identical.
src/components/product/product-filter-bar-container.tsx (1)

76-107: 성능: recommendedBrands 요청이 순차적 워터폴 발생

recommendedBrands API 호출(Line 105-107)은 첫 번째 Promise.all 완료 후 실행됩니다. 이 요청은 subCategoriesresult에 의존하지 않으므로 Promise.all에 포함하여 병렬로 실행할 수 있습니다.

🔧 병렬 fetch로 변경
-  const [subCategories, result] = await Promise.all([
+  const [subCategories, result, recommendedBrands] = 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,
       },
     }),
+    api
+      .get<BrandWithProducts[]>('/brands/recommend', { params: { count: 200 } })
+      .catch(() => [] as BrandWithProducts[]),
   ]);
 
   const parentCategoryName = subCategories[0]?.parentCategoryName ?? '';
-  ...
-
-  const recommendedBrands = await api
-    .get<BrandWithProducts[]>('/brands/recommend', { params: { count: 200 } })
-    .catch(() => []);
🤖 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 76 -
107, The recommendedBrands fetch currently runs after the first Promise.all,
causing an unnecessary waterfall; move the api.get('/brands/recommend', {
params: { count: 200 } }) call into the initial Promise.all alongside the
getSubCategories(parsedParentId) and api.get('/products', ...) calls so all
three requests run in parallel, then destructure its result (e.g., const
[subCategories, result, recommendedBrands] = await Promise.all([...])) and
preserve the existing .catch behavior if desired or handle errors consistently
with the other requests; update any downstream references to recommendedBrands
to use the newly destructured variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/product/product-filter-bar-container.tsx`:
- Around line 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.

In `@src/components/product/product-filter-bar.tsx`:
- Around line 447-448: The label concatenation renders like "사이즈2" because
selectedSizes.length is appended directly; update the button labels in the
ProductFilterBar component to separate the count from the label (e.g., add a
trailing space or render the count in a badge/span) wherever you use patterns
like 사이즈{selectedSizes.length > 0 ? selectedSizes.length : ''} (also update the
similar usages for selectedBrands and selectedColors at the other occurrences) —
ensure the count is conditionally rendered with surrounding spacing or wrapped
in a styled element so the UI shows "사이즈 2" or "사이즈 [2]" instead of "사이즈2".
- Around line 101-104: The component ProductFilterBar is currently a named
export but must follow the project guideline to be a default-exported function;
change the declaration from "export function ProductFilterBar" to "export
default function ProductFilterBar" (keep BrandFilterOption as a named export)
and update all import sites accordingly (e.g., product-filter-bar-container.tsx)
to use default import syntax; ensure the function remains a plain function
declaration (not an arrow) and run a quick grep for "import.*ProductFilterBar"
to update every consumer.
- Around line 280-283: The current canApplyCustomPrice boolean allows the
degenerate "0-0" range because Number("0") <= Number("0") is true; update the
validation for customMinPrice/customMaxPrice so both inputs are present and the
numeric range is meaningful — e.g., require numeric max >= min AND disallow both
being zero (or require min > 0), by changing the condition that sets
canApplyCustomPrice (refer to canApplyCustomPrice, customMinPrice,
customMaxPrice) to include an additional check preventing a 0-0 range.

In `@src/components/product/product-list-container.tsx`:
- Around line 18-35: The three duplicated utilities (normalizeArray,
normalizePriceRange, PRICE_RANGE_PATTERN) and the hardcoded allowed sizes list
are used in both product-list-container.tsx and
product-filter-bar-container.tsx; extract them into a shared module (e.g.,
src/lib/filter-utils.ts or src/utils/search-params.ts) exporting
PRICE_RANGE_PATTERN, normalizeArray, normalizePriceRange and
ALLOWED_CLOTHING_SIZES, update both files to import those symbols instead of
redeclaring them, and run a quick ripgrep for
"normalizeArray|normalizePriceRange|PRICE_RANGE_PATTERN" to ensure no other
duplicates remain.

---

Outside diff comments:
In `@src/components/product/product-filter-bar.tsx`:
- Around line 484-552: The bottom-sheet panes for openSheet === 'sort' (and the
similar 'filters' pane) lack keyboard dismiss and a focus trap; add a useEffect
in the ProductFilterBar component that listens for Escape and calls
setOpenSheet(null), add role="dialog" and aria-modal="true" to the sheet
container element, and implement a focus-trap around the sheet (use a ref for
the sheet container and on keydown handle Tab/Shift+Tab to keep focus cycling
inside, or integrate a small focus-trap utility/library) so keyboard users
cannot tab out of the open sheet and can close it with Escape; ensure this
change covers the same logic used by handleSortChange and the setOpenSheet calls
for both 'sort' and 'filters'.

---

Nitpick comments:
In `@src/components/product/product-filter-bar-container.tsx`:
- Around line 76-107: The recommendedBrands fetch currently runs after the first
Promise.all, causing an unnecessary waterfall; move the
api.get('/brands/recommend', { params: { count: 200 } }) call into the initial
Promise.all alongside the getSubCategories(parsedParentId) and
api.get('/products', ...) calls so all three requests run in parallel, then
destructure its result (e.g., const [subCategories, result, recommendedBrands] =
await Promise.all([...])) and preserve the existing .catch behavior if desired
or handle errors consistently with the other requests; update any downstream
references to recommendedBrands to use the newly destructured variable.

In `@src/components/product/product-filter-bar.tsx`:
- Around line 253-420: The renderFilterContent function in this large component
centralizes three separate UI responsibilities (size, price, brand); split each
tab into its own presentational component—create SizeFilterPanel,
PriceFilterPanel, and BrandFilterPanel (or similarly named) and move the
JSX/logic for sizeOptions, PRICE_OPTIONS + custom price inputs, and brand
list/filtering respectively out of renderFilterContent, keeping only a simple
switch in renderFilterContent that returns <SizeFilterPanel ... />,
<PriceFilterPanel ... />, or <BrandFilterPanel ... /> and pass down needed
props/state setters (e.g., tempSizes, setTempSizes, tempPriceRange,
setTempPriceRange, customMinPrice, setCustomMinPrice, customMaxPrice,
setCustomMaxPrice, brandKeyword, filteredBrands, tempBrandIds, setTempBrandIds,
toggleValue, normalizeNumberInput) to preserve behavior and tests; ensure each
new component is functional, contained in the same folder or a new subfile, and
that all existing handlers (onClick/onChange) are wired through props so
behavior remains identical.

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

Comment on lines +101 to +104
export function ProductFilterBar({
parentCategoryName,
availableBrands,
}: ProductFilterBarProps) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

코딩 가이드라인 위반: export default function 패턴 필요

export function ProductFilterBar는 named export입니다. 가이드라인에 따라 export default function 패턴을 사용해야 합니다. BrandFilterOption 인터페이스는 named export로 유지하고, 컴포넌트만 default export로 변경하세요.

🔧 수정 제안
-export function ProductFilterBar({
+export default function ProductFilterBar({
   parentCategoryName,
   availableBrands,
 }: ProductFilterBarProps) {

product-filter-bar-container.tsx의 import도 함께 변경:

-import { BrandFilterOption, ProductFilterBar } from './product-filter-bar';
+import ProductFilterBar, { BrandFilterOption } from './product-filter-bar';

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

#!/bin/bash
# Verify all import sites for ProductFilterBar to assess impact of changing to default export
rg -n --type=ts --type-add 'tsx:*.tsx' --type=tsx "import.*ProductFilterBar" -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar.tsx` around lines 101 - 104, The
component ProductFilterBar is currently a named export but must follow the
project guideline to be a default-exported function; change the declaration from
"export function ProductFilterBar" to "export default function ProductFilterBar"
(keep BrandFilterOption as a named export) and update all import sites
accordingly (e.g., product-filter-bar-container.tsx) to use default import
syntax; ensure the function remains a plain function declaration (not an arrow)
and run a quick grep for "import.*ProductFilterBar" to update every consumer.

Comment on lines +280 to +283
const canApplyCustomPrice =
customMinPrice.length > 0 &&
customMaxPrice.length > 0 &&
Number(customMinPrice) <= Number(customMaxPrice);
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

커스텀 가격: "0" ~ "0" 범위가 유효하게 처리됨

canApplyCustomPriceNumber("0") <= Number("0")true로 평가하여 "0-0" 같은 무의미한 범위가 API에 전달될 수 있습니다. 최소 금액이 0보다 크거나 최대 금액이 최소 금액보다 큰지 추가 검증을 고려하세요.

🔧 수정 제안
       const canApplyCustomPrice =
         customMinPrice.length > 0 &&
         customMaxPrice.length > 0 &&
-        Number(customMinPrice) <= Number(customMaxPrice);
+        Number(customMinPrice) < Number(customMaxPrice) &&
+        Number(customMaxPrice) > 0;
📝 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 canApplyCustomPrice =
customMinPrice.length > 0 &&
customMaxPrice.length > 0 &&
Number(customMinPrice) <= Number(customMaxPrice);
const canApplyCustomPrice =
customMinPrice.length > 0 &&
customMaxPrice.length > 0 &&
Number(customMinPrice) < Number(customMaxPrice) &&
Number(customMaxPrice) > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar.tsx` around lines 280 - 283, The
current canApplyCustomPrice boolean allows the degenerate "0-0" range because
Number("0") <= Number("0") is true; update the validation for
customMinPrice/customMaxPrice so both inputs are present and the numeric range
is meaningful — e.g., require numeric max >= min AND disallow both being zero
(or require min > 0), by changing the condition that sets canApplyCustomPrice
(refer to canApplyCustomPrice, customMinPrice, customMaxPrice) to include an
additional check preventing a 0-0 range.

Comment on lines +447 to +448
사이즈{selectedSizes.length > 0 ? selectedSizes.length : ''}
</button>
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

필터 버튼 활성 상태 표시: 선택된 필터 수가 공백 없이 라벨에 붙음

사이즈{selectedSizes.length > 0 ? selectedSizes.length : ''} 패턴에서 숫자가 라벨 텍스트에 바로 붙어서 "사이즈2", "브랜드3" 등으로 렌더링됩니다. 시각적 구분을 위해 공백이나 뱃지 형태로 표시하는 것을 권장합니다.

🔧 공백 추가 예시
-            사이즈{selectedSizes.length > 0 ? selectedSizes.length : ''}
+            사이즈{selectedSizes.length > 0 ? ` ${selectedSizes.length}` : ''}

Also applies to: 455-456, 463-464

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/product/product-filter-bar.tsx` around lines 447 - 448, The
label concatenation renders like "사이즈2" because selectedSizes.length is appended
directly; update the button labels in the ProductFilterBar component to separate
the count from the label (e.g., add a trailing space or render the count in a
badge/span) wherever you use patterns like 사이즈{selectedSizes.length > 0 ?
selectedSizes.length : ''} (also update the similar usages for selectedBrands
and selectedColors at the other occurrences) — ensure the count is conditionally
rendered with surrounding spacing or wrapped in a styled element so the UI shows
"사이즈 2" or "사이즈 [2]" instead of "사이즈2".

Comment on lines +18 to 35
const PRICE_RANGE_PATTERN = /^\d+-\d+$/;

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 ?? '';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

DRY 위반: normalizeArray, normalizePriceRange, PRICE_RANGE_PATTERN 중복

이 세 유틸리티가 product-filter-bar-container.tsx에 동일하게 정의되어 있습니다. 허용 사이즈 리스트 ['XS', 'S', 'M', 'L', 'XL']도 양쪽 파일에서 하드코딩되어 있습니다.

공유 유틸리티 모듈(예: src/lib/filter-utils.ts 또는 src/utils/search-params.ts)로 추출하여 단일 소스로 관리하세요.

🔧 추출 예시
// src/lib/filter-utils.ts
export const PRICE_RANGE_PATTERN = /^\d+-\d+$/;
export const ALLOWED_CLOTHING_SIZES = ['XS', 'S', 'M', 'L', 'XL'] as const;

export function normalizeArray(value?: string | 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 [];
}

export function normalizePriceRange(value?: string | string[]): string {
  if (Array.isArray(value)) {
    return value[0] ?? '';
  }
  return value ?? '';
}
#!/bin/bash
# Verify all locations where these utilities are duplicated
rg -n "normalizeArray|normalizePriceRange|PRICE_RANGE_PATTERN" --type-add 'tsx:*.tsx' --type=tsx --type=ts
🤖 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 18 - 35, The
three duplicated utilities (normalizeArray, normalizePriceRange,
PRICE_RANGE_PATTERN) and the hardcoded allowed sizes list are used in both
product-list-container.tsx and product-filter-bar-container.tsx; extract them
into a shared module (e.g., src/lib/filter-utils.ts or
src/utils/search-params.ts) exporting PRICE_RANGE_PATTERN, normalizeArray,
normalizePriceRange and ALLOWED_CLOTHING_SIZES, update both files to import
those symbols instead of redeclaring them, and run a quick ripgrep for
"normalizeArray|normalizePriceRange|PRICE_RANGE_PATTERN" to ensure no other
duplicates remain.

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