feat: #276 탐색 페이지 리디자인 - 통합검색, ExploreSection 개선#281
Conversation
- 탭 토글 제거: 검색바 입력 시 자동으로 검색/탐색 전환 - 통합검색(all) 카테고리 추가: 차/차록/찻집/향미태그 동시 검색 - ExploreSection 리디자인: 섹션 순서 개편, 에디토리얼 랭킹, 향미태그 크기 비례 - 검색 결과 없을 때 CategoryCreateButton(새 차 등록하기 등) 표시 - CtaButton 공통 컴포넌트 추출 (HomeBanner, WeeklyStreak에도 적용) - 최근 검색어 없어도 빈 상태 표시 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- all 카테고리 빈 결과 시 EmptyState + 새 차 등록하기 버튼 표시 - all 카테고리 필터 적용 시 getWithFilters 호출되도록 수정 - SearchResults에 selectedIndex prop 추가 (키보드 탐색 준비) - Search에 키보드 화살표/Enter 탐색 기능 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- note/cellar 탐색 시 user 인증 여부 확인 후 fetch (비인증 노출 방지) - Promise.all → Promise.allSettled로 교체 (categoryLoading stuck 방지) - SearchCategory 타입 SearchResults에서 export, FilterPanel/Search에서 import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
WalkthroughThis PR redesigns the search/explore page UI and adds tag creation functionality. Backend adds tag creation DTO, controller endpoint, and service logic. Frontend introduces CtaButton component, redesigns ExploreSection with TeaFlatRow layout, updates FilterPanel with bottom sheet UI, implements auto-switching between explore/search modes with keyboard navigation, and adds tag creation support to TagManager. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SearchPage as Search Page
participant ExploreSection as Explore Section
participant SearchResults as Search Results
participant API as Search API
User->>SearchPage: 페이지 진입 (빈 검색바)
Note over SearchPage: selectedIndex = -1, category = 'all'
SearchPage->>ExploreSection: 렌더링
User->>SearchPage: 검색바에 입력
activate SearchPage
SearchPage->>SearchPage: 입력 감지 (searchTerm ≠ '')
SearchPage->>API: 카테고리별 데이터 페칭
API-->>SearchPage: 검색 결과 반환
SearchPage->>SearchResults: 카테고리 chips + 결과 렌더링
deactivate SearchPage
User->>SearchPage: 화살표 키 입력
activate SearchPage
SearchPage->>SearchPage: selectedIndex 업데이트
SearchPage->>SearchResults: selectedIndex prop 전달
SearchResults->>SearchResults: 선택 항목 ring 스타일 적용
SearchResults->>SearchResults: 선택 항목으로 자동 스크롤
deactivate SearchPage
User->>SearchPage: Enter 키 입력
activate SearchPage
SearchPage->>SearchPage: navigableItems[selectedIndex] 조회
SearchPage->>User: 선택 항목으로 네비게이션
deactivate SearchPage
User->>SearchPage: 검색바 삭제 (빈 상태로)
activate SearchPage
SearchPage->>SearchPage: 검색 모드 해제
SearchPage->>ExploreSection: 자동으로 다시 렌더링
deactivate SearchPage
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/Search.tsx (1)
92-154:⚠️ Potential issue | 🟡 MinoruseEffect 의존성 배열에
searchQuery누락 — 의존성 규칙 위반Line 154의 의존성 배열(
[searchCategory, user?.id, noteSort])에searchQuery가 빠져있으나, Line 95에서searchQuery.trim().toLowerCase()를 사용하고 Lines 104, 113, 120, 132, 140, 149에서 필터링에 활용합니다.이로 인해 검색어 변경 시 이 useEffect가 재실행되지 않으므로 React 의존성 규칙을 위반합니다. 현재는 Line 169+의 별도 useEffect가 필터링을 담당하므로 실제 최종 결과는 올바르지만, 의도하지 않은 동작이거나 불필요한 state 업데이트가 발생할 수 있습니다. 의존성 배열에
searchQuery를 추가하거나 필터링 로직을 명확히 재구조화하기 바랍니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/Search.tsx` around lines 92 - 154, The useEffect that reads searchQuery (computes q = searchQuery.trim().toLowerCase()) must include searchQuery in its dependency array to satisfy React rules and ensure re-run when the query changes; update the dependency array for the useEffect containing the notes/tags/cellar fetches (the effect that references searchCategory, user?.id, noteSort and computes q) to also include searchQuery, so q and subsequent setNoteResults/setTagResults/setCellarResults are recalculated when the query changes.
🧹 Nitpick comments (3)
src/pages/__tests__/Search.test.tsx (1)
110-119: 테스트 이름이 현재 UX와 어긋나 있어 유지보수 시 혼동됩니다.
Line 110,Line 142,Line 201의 “탐색 탭” 표현은 탭 제거 이후 시나리오와 맞지 않아서, “검색어 비어있을 때 ExploreSection 표시”처럼 현재 동작 기준으로 이름을 맞추는 편이 좋습니다.Also applies to: 142-150, 201-208
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/__tests__/Search.test.tsx` around lines 110 - 119, The test titles in Search.test.tsx currently say "탐색 탭으로 전환 시 ..." but the behavior tested is "when the search input is empty, ExploreSection is shown" — update the it() descriptions at the three occurrences (the tests whose current names appear at lines referenced in the review) to reflect the actual behavior, e.g. rename them to "검색어 비어있을 때 ExploreSection이 표시된다" or a similar phrase; keep the test implementations (renderWithRouter(<Search />,...), waitFor and expect assertions in the tests unchanged, only change the string passed to it(...) so the descriptions match current UX.src/components/search/SearchResults.tsx (1)
1-1: 불필요한React기본 importReact 18에서는 JSX 변환이 자동으로 처리되므로
React기본 import가 필요하지 않습니다.useEffect만 named import로 사용하면 됩니다.♻️ import 정리 제안
-import React, { useEffect } from 'react'; +import { useEffect } from 'react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/search/SearchResults.tsx` at line 1, The import currently pulls in the default React export unnecessarily; remove the default React import and import only the named hook(s) used (e.g., change "import React, { useEffect } from 'react';" to a named import of useEffect) so JSX relies on the automatic runtime; update the import in SearchResults.tsx to import only useEffect (and any other used named exports) to eliminate the unused default React symbol.src/components/search/FilterPanel.tsx (1)
53-56: 사용되지 않는 props 선언
filterPriceRange,setFilterPriceRange,filterSellerName,setFilterSellerNameprops가 인터페이스에 정의되어 있지만 컴포넌트 내부에서 사용되지 않습니다. 향후 기능 확장을 위한 것이라면 주석으로 의도를 명시하거나, 그렇지 않다면 제거를 고려하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/search/FilterPanel.tsx` around lines 53 - 56, The props filterPriceRange, setFilterPriceRange, filterSellerName, and setFilterSellerName declared on the FilterPanel component are unused; either remove them from the component props/interface or keep them but annotate intent with a comment and mark them as TODO/experimental to avoid dead API. Locate the FilterPanel props/interface (the declarations named filterPriceRange, setFilterPriceRange, filterSellerName, setFilterSellerName) and either delete those four entries and any related exports, or add a clear comment above the interface and the prop typings indicating they are reserved for future features (and consider keeping them optional) so lint/maintainers understand they are intentionally present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/tags/dto/create-tag.dto.ts`:
- Around line 5-9: Move the `@Transform` decorator above the validation decorators
so trimming occurs before validation: on the DTO property "name" in the
CreateTag DTO, place `@Transform`(({ value }) => (typeof value === 'string' ?
value.trim() : value)) as the first decorator before `@IsString`(), `@MinLength`(1)
and `@MaxLength`(50) so the string is trimmed prior to running Min/Max length
checks.
In `@backend/src/tags/tags.service.ts`:
- Around line 34-39: The current create flow (using this.tagsRepository.findOne
-> this.tagsRepository.create -> this.tagsRepository.save) has a race where
concurrent requests can cause a DB unique-constraint error; wrap the save call
in a try/catch and, when the repository save throws a unique-violation error
(e.g., Postgres error code '23505' or ORM-specific duplicate-key indicator),
convert it to a ConflictException with the same message used earlier (e.g., `태그
'${trimmed}'이(가) 이미 존재합니다.`); keep the optimistic pre-check with findOne but
rely on the catch around this.tagsRepository.save to map DB unique-constraint
errors to ConflictException so races return 409 instead of 500.
In `@src/components/search/ExploreSection.tsx`:
- Around line 142-144: The loading message in ExploreSection is shown whenever
popularTags is falsy or empty; change the conditional to distinguish loading vs
empty data by using the actual loading flag (e.g., isLoadingPopularTags or
similar) or check for undefined/null vs length === 0: render "인기 향미를 불러오는 중..."
only when the loading flag is true (or popularTags === undefined), and render an
empty-state message when popularTags is an empty array (popularTags &&
popularTags.length === 0). Update the JSX branch that currently renders the
paragraph to use that explicit check so empty data doesn't display the loading
text.
In `@src/pages/__tests__/Search.test.tsx`:
- Around line 231-235: The test currently only fails if both tabs exist because
it asserts expect(hasSearchTab && hasExploreTab).toBe(false); instead assert
each tab is absent independently: compute tabContainer, tabButtons as before and
then assert that hasSearchTab is false and hasExploreTab is false (e.g., replace
the single combined expectation with two expectations against hasSearchTab and
hasExploreTab so the test fails if either button reappears); reference the
existing identifiers tabContainer, tabButtons, hasSearchTab, hasExploreTab and
update the expect calls accordingly.
---
Outside diff comments:
In `@src/pages/Search.tsx`:
- Around line 92-154: The useEffect that reads searchQuery (computes q =
searchQuery.trim().toLowerCase()) must include searchQuery in its dependency
array to satisfy React rules and ensure re-run when the query changes; update
the dependency array for the useEffect containing the notes/tags/cellar fetches
(the effect that references searchCategory, user?.id, noteSort and computes q)
to also include searchQuery, so q and subsequent
setNoteResults/setTagResults/setCellarResults are recalculated when the query
changes.
---
Nitpick comments:
In `@src/components/search/FilterPanel.tsx`:
- Around line 53-56: The props filterPriceRange, setFilterPriceRange,
filterSellerName, and setFilterSellerName declared on the FilterPanel component
are unused; either remove them from the component props/interface or keep them
but annotate intent with a comment and mark them as TODO/experimental to avoid
dead API. Locate the FilterPanel props/interface (the declarations named
filterPriceRange, setFilterPriceRange, filterSellerName, setFilterSellerName)
and either delete those four entries and any related exports, or add a clear
comment above the interface and the prop typings indicating they are reserved
for future features (and consider keeping them optional) so lint/maintainers
understand they are intentionally present.
In `@src/components/search/SearchResults.tsx`:
- Line 1: The import currently pulls in the default React export unnecessarily;
remove the default React import and import only the named hook(s) used (e.g.,
change "import React, { useEffect } from 'react';" to a named import of
useEffect) so JSX relies on the automatic runtime; update the import in
SearchResults.tsx to import only useEffect (and any other used named exports) to
eliminate the unused default React symbol.
In `@src/pages/__tests__/Search.test.tsx`:
- Around line 110-119: The test titles in Search.test.tsx currently say "탐색 탭으로
전환 시 ..." but the behavior tested is "when the search input is empty,
ExploreSection is shown" — update the it() descriptions at the three occurrences
(the tests whose current names appear at lines referenced in the review) to
reflect the actual behavior, e.g. rename them to "검색어 비어있을 때 ExploreSection이
표시된다" or a similar phrase; keep the test implementations
(renderWithRouter(<Search />,...), waitFor and expect assertions in the tests
unchanged, only change the string passed to it(...) so the descriptions match
current UX.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 74f439ec-b49b-4e90-a144-8c49199a964a
📒 Files selected for processing (13)
backend/src/tags/dto/create-tag.dto.tsbackend/src/tags/tags.controller.tsbackend/src/tags/tags.service.tssrc/components/home/HomeBanner.tsxsrc/components/home/WeeklyStreak.tsxsrc/components/search/ExploreSection.tsxsrc/components/search/FilterPanel.tsxsrc/components/search/SearchResults.tsxsrc/components/ui/CtaButton.tsxsrc/lib/api/social.api.tssrc/pages/Search.tsxsrc/pages/TagManager.tsxsrc/pages/__tests__/Search.test.tsx
| @IsString() | ||
| @MinLength(1) | ||
| @MaxLength(50) | ||
| @Transform(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value)) | ||
| name: string; |
There was a problem hiding this comment.
데코레이터 순서 문제: @Transform이 검증 전에 실행되어야 합니다.
현재 순서에서는 @MaxLength(50) 검증이 trim 전에 수행됩니다. 만약 입력값이 " tag " (공백 포함 56자)라면, trim 후에는 50자 이하가 될 수 있지만 검증 단계에서 먼저 실패합니다. @Transform을 최상단으로 이동하세요.
🔧 데코레이터 순서 수정 제안
+ `@Transform`(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value))
`@IsString`()
`@MinLength`(1)
`@MaxLength`(50)
- `@Transform`(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value))
name: string;📝 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.
| @IsString() | |
| @MinLength(1) | |
| @MaxLength(50) | |
| @Transform(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value)) | |
| name: string; | |
| `@Transform`(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value)) | |
| `@IsString`() | |
| `@MinLength`(1) | |
| `@MaxLength`(50) | |
| name: string; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/tags/dto/create-tag.dto.ts` around lines 5 - 9, Move the
`@Transform` decorator above the validation decorators so trimming occurs before
validation: on the DTO property "name" in the CreateTag DTO, place `@Transform`(({
value }) => (typeof value === 'string' ? value.trim() : value)) as the first
decorator before `@IsString`(), `@MinLength`(1) and `@MaxLength`(50) so the string is
trimmed prior to running Min/Max length checks.
| const existing = await this.tagsRepository.findOne({ where: { name: trimmed } }); | ||
| if (existing) throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`); | ||
|
|
||
| const tag = this.tagsRepository.create({ name: trimmed, category }); | ||
| await this.tagsRepository.save(tag); | ||
| return { name: tag.name, noteCount: 0, isFollowing: false }; |
There was a problem hiding this comment.
중복 태그 생성 경합 시 409 대신 내부 오류로 떨어질 수 있습니다.
Line 34 선조회 후 Line 38 저장 사이에 동시 요청이 들어오면 경쟁 상태가 생깁니다. 이 경우 DB 유니크 충돌을 ConflictException으로 변환해 주는 방어가 필요합니다.
🔧 제안 수정
async createTag(name: string, category: 'general' | 'flavor' = 'general'): Promise<PopularTagDto> {
const trimmed = name.trim();
if (!trimmed) throw new BadRequestException('태그 이름을 입력해주세요.');
- const existing = await this.tagsRepository.findOne({ where: { name: trimmed } });
- if (existing) throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`);
-
- const tag = this.tagsRepository.create({ name: trimmed, category });
- await this.tagsRepository.save(tag);
- return { name: tag.name, noteCount: 0, isFollowing: false };
+ const existing = await this.tagsRepository.findOne({ where: { name: trimmed } });
+ if (existing) throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`);
+
+ try {
+ const tag = this.tagsRepository.create({ name: trimmed, category });
+ await this.tagsRepository.save(tag);
+ return { name: tag.name, noteCount: 0, isFollowing: false };
+ } catch (error: unknown) {
+ const code = (error as { code?: string; driverError?: { code?: string } })?.code
+ ?? (error as { driverError?: { code?: string } })?.driverError?.code;
+ if (code === 'ER_DUP_ENTRY') {
+ throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`);
+ }
+ throw error;
+ }
}📝 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 existing = await this.tagsRepository.findOne({ where: { name: trimmed } }); | |
| if (existing) throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`); | |
| const tag = this.tagsRepository.create({ name: trimmed, category }); | |
| await this.tagsRepository.save(tag); | |
| return { name: tag.name, noteCount: 0, isFollowing: false }; | |
| const existing = await this.tagsRepository.findOne({ where: { name: trimmed } }); | |
| if (existing) throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`); | |
| try { | |
| const tag = this.tagsRepository.create({ name: trimmed, category }); | |
| await this.tagsRepository.save(tag); | |
| return { name: tag.name, noteCount: 0, isFollowing: false }; | |
| } catch (error: unknown) { | |
| const code = (error as { code?: string; driverError?: { code?: string } })?.code | |
| ?? (error as { driverError?: { code?: string } })?.driverError?.code; | |
| if (code === 'ER_DUP_ENTRY') { | |
| throw new ConflictException(`태그 '${trimmed}'이(가) 이미 존재합니다.`); | |
| } | |
| throw error; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/tags/tags.service.ts` around lines 34 - 39, The current create
flow (using this.tagsRepository.findOne -> this.tagsRepository.create ->
this.tagsRepository.save) has a race where concurrent requests can cause a DB
unique-constraint error; wrap the save call in a try/catch and, when the
repository save throws a unique-violation error (e.g., Postgres error code
'23505' or ORM-specific duplicate-key indicator), convert it to a
ConflictException with the same message used earlier (e.g., `태그 '${trimmed}'이(가)
이미 존재합니다.`); keep the optimistic pre-check with findOne but rely on the catch
around this.tagsRepository.save to map DB unique-constraint errors to
ConflictException so races return 409 instead of 500.
| ) : ( | ||
| <p className="text-sm text-muted-foreground">인기 향미를 불러오는 중...</p> | ||
| )} |
There was a problem hiding this comment.
빈 데이터 상태에서 로딩 문구가 잘못 노출됩니다.
Line 143은 popularTags가 단순히 비어있는 경우에도 계속 “불러오는 중...”으로 보입니다. 실제 빈 상태 문구로 분리해 주세요.
🔧 제안 수정
- <p className="text-sm text-muted-foreground">인기 향미를 불러오는 중...</p>
+ <p className="text-sm text-muted-foreground">아직 등록된 향미 태그가 없습니다.</p>📝 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.
| ) : ( | |
| <p className="text-sm text-muted-foreground">인기 향미를 불러오는 중...</p> | |
| )} | |
| ) : ( | |
| <p className="text-sm text-muted-foreground">아직 등록된 향미 태그가 없습니다.</p> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/search/ExploreSection.tsx` around lines 142 - 144, The loading
message in ExploreSection is shown whenever popularTags is falsy or empty;
change the conditional to distinguish loading vs empty data by using the actual
loading flag (e.g., isLoadingPopularTags or similar) or check for undefined/null
vs length === 0: render "인기 향미를 불러오는 중..." only when the loading flag is true
(or popularTags === undefined), and render an empty-state message when
popularTags is an empty array (popularTags && popularTags.length === 0). Update
the JSX branch that currently renders the paragraph to use that explicit check
so empty data doesn't display the loading text.
| const tabContainer = container.querySelector('.bg-muted.rounded-lg'); | ||
| const tabButtons = tabContainer ? Array.from(tabContainer.querySelectorAll('button')) : []; | ||
| const hasSearchTab = tabButtons.some((b) => b.textContent?.includes('검색')); | ||
| const hasExploreTab = tabButtons.some((b) => b.textContent?.includes('탐색')); | ||
| expect(hasSearchTab && hasExploreTab).toBe(false); |
There was a problem hiding this comment.
탭 제거 검증 조건이 약해서 회귀를 놓칠 수 있습니다.
Line 235의 hasSearchTab && hasExploreTab는 둘 다 있을 때만 실패합니다. 하나만 다시 생겨도 테스트가 통과해 버립니다. 버튼 자체를 직접 부정 검증하는 방식이 안전합니다.
🔧 제안 수정
- const tabContainer = container.querySelector('.bg-muted.rounded-lg');
- const tabButtons = tabContainer ? Array.from(tabContainer.querySelectorAll('button')) : [];
- const hasSearchTab = tabButtons.some((b) => b.textContent?.includes('검색'));
- const hasExploreTab = tabButtons.some((b) => b.textContent?.includes('탐색'));
- expect(hasSearchTab && hasExploreTab).toBe(false);
+ expect(screen.queryByRole('button', { name: '검색' })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: '탐색' })).not.toBeInTheDocument();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/__tests__/Search.test.tsx` around lines 231 - 235, The test
currently only fails if both tabs exist because it asserts expect(hasSearchTab
&& hasExploreTab).toBe(false); instead assert each tab is absent independently:
compute tabContainer, tabButtons as before and then assert that hasSearchTab is
false and hasExploreTab is false (e.g., replace the single combined expectation
with two expectations against hasSearchTab and hasExploreTab so the test fails
if either button reappears); reference the existing identifiers tabContainer,
tabButtons, hasSearchTab, hasExploreTab and update the expect calls accordingly.
- FilterPanel: 정렬 칩 인라인 + 상세 필터 Bottom Sheet로 공간 절약 - ExploreSection: 모든 섹션 미니멀 플랫 리스트 통일, 초록 accent만 사용 - TeaSearchSection: Enter 키 선택, 연도 표시, variant/showAllWhenEmpty/isLoading props 추가 - SessionNew/BlindSessionNew/NewCellarItem/EditCellarItem → TeaSearchSection 공통 컴포넌트 사용 - 모든 신규 작성 페이지(NewNote/NewShop/SessionNew/BlindSessionNew/TagManager/NewCellarItem 등) 자동 포커스 추가 - 검색 결과 없음 CTA 버튼 primary 초록색으로 통일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Test plan
npm run build✅npm run test:runSearch.test.tsx 13/13 ✅🤖 Generated with Claude Code
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항