feat: 카테고리 필터 바텀시트 UI 및 API 파라미터 정렬#79
Conversation
Walkthrough상품 카테고리 페이지의 필터링 아키텍처를 개편합니다. ProductFilterBarContainer 컨테이너를 도입하여 필터 데이터(브랜드, 하위 카테고리명) 페칭을 담당하고, ProductFilterBar를 정적 컴포넌트에서 의류 사이즈·가격대·브랜드 필터를 지원하는 동적 UI 시스템으로 확장합니다. 쿼리 파라미터 검증 및 정규화 로직을 추가합니다. Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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요청이 순차적 워터폴 발생
recommendedBrandsAPI 호출(Line 105-107)은 첫 번째Promise.all완료 후 실행됩니다. 이 요청은subCategories나result에 의존하지 않으므로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.
| const productItems = result.products.content as Array<{ | ||
| brandName: string; | ||
| brandId?: number; | ||
| }>; |
There was a problem hiding this comment.
🧩 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 -50Repository: IT-Cotato/12th-OnGil-FE
Length of output: 94
🏁 Script executed:
# Look for type definitions directory
fd -t d "types|models|@types" --max-depth 3Repository: 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 10Repository: 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.tsxRepository: 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 5Repository: 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 5Repository: 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.content는 Product[] 타입입니다. brandName은 이미 Product에 존재하므로 타입 단언이 필요 없습니다. 다만 brandId는 Product에 없고, 현재 코드의 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.
| export function ProductFilterBar({ | ||
| parentCategoryName, | ||
| availableBrands, | ||
| }: ProductFilterBarProps) { |
There was a problem hiding this comment.
🛠️ 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.
| const canApplyCustomPrice = | ||
| customMinPrice.length > 0 && | ||
| customMaxPrice.length > 0 && | ||
| Number(customMinPrice) <= Number(customMaxPrice); |
There was a problem hiding this comment.
커스텀 가격: "0" ~ "0" 범위가 유효하게 처리됨
canApplyCustomPrice는 Number("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.
| 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.
| 사이즈{selectedSizes.length > 0 ? selectedSizes.length : ''} | ||
| </button> |
There was a problem hiding this comment.
필터 버튼 활성 상태 표시: 선택된 필터 수가 공백 없이 라벨에 붙음
사이즈{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".
| 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 ?? ''; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
📝 개요
카테고리 상품 목록 필터 UI를 바텀시트 기반으로 확장하고, API 파라미터를 백엔드 스펙(
brandIds,clothingSizes,priceRange=min-max)에 맞게 정렬했습니다.🚀 주요 변경 사항
정렬/사이즈/브랜드/가격필터 칩 + 바텀시트 UI 구현min-max) 방식으로 변경brandIds,clothingSizes,priceRange형식으로 변경searchParams타입 및 쿼리 동기화 로직 정리📸 스크린샷 (선택)
✅ 체크리스트
pnpm -s exec tsc --noEmit)