Skip to content

feat: #276 탐색 페이지 리디자인 - 통합검색, ExploreSection 개선#281

Merged
FLYLIKEB merged 4 commits intomainfrom
feature/issue-276-search-redesign
Mar 18, 2026
Merged

feat: #276 탐색 페이지 리디자인 - 통합검색, ExploreSection 개선#281
FLYLIKEB merged 4 commits intomainfrom
feature/issue-276-search-redesign

Conversation

@FLYLIKEB
Copy link
Copy Markdown
Owner

@FLYLIKEB FLYLIKEB commented Mar 18, 2026

Summary

  • Closes feat: 탐색 페이지(/sasaek) UI/UX 리디자인 - 미니멀 & 에디토리얼 방향 #276
  • 탭 토글 제거: 검색바 입력 시 SearchResults/ExploreSection 자동 전환
  • 통합검색(all) 카테고리 추가: 차/차록/찻집/향미태그 동시 검색, Promise.allSettled
  • ExploreSection 리디자인: 향미 태그 비례 크기, 에디토리얼 랭킹, 이니셜 배지
  • 검색 결과 없을 때 카테고리별 추가하기 버튼 표시 (CategoryCreateButton)
  • CtaButton 공통 컴포넌트 추출 (HomeBanner, WeeklyStreak, SearchResults 적용)
  • SearchCategory 타입 단일화 (SearchResults에서 export)
  • 비인증 사용자 note/cellar fetch 가드 추가

Test plan

  • npm run build
  • npm run test:run Search.test.tsx 13/13 ✅
  • code-reviewer 에이전트 검토 완료 (HIGH 이슈 0건)

🤖 Generated with Claude Code

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 태그 생성 기능 추가
    • 전체 검색 카테고리 추가 및 통합 검색 지원
    • 검색 결과에 키보드 네비게이션(화살표 키, Enter, Escape) 추가
    • 고급 필터 기능을 하단 시트로 개선
    • 새로운 CTA 버튼 컴포넌트 추가
  • 개선 사항

    • 검색 페이지 UI 및 탐색 흐름 최적화
    • 필터 패널에 정렬 옵션 추가
    • 탐색 섹션 구조 개선 및 향미 필터링 기능 강화

FLYLIKEB and others added 3 commits March 18, 2026 17:56
- 탭 토글 제거: 검색바 입력 시 자동으로 검색/탐색 전환
- 통합검색(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>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cha-log Ready Ready Preview, Comment Mar 18, 2026 9:27am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 18, 2026

Warning

Rate limit exceeded

@FLYLIKEB has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 22 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31f5fbb8-0c3c-43a3-8049-8f9e74770e83

📥 Commits

Reviewing files that changed from the base of the PR and between 14811a9 and 0cc75aa.

📒 Files selected for processing (13)
  • src/components/ImageUploader.tsx
  • src/components/TeaSearchSection.tsx
  • src/components/search/ExploreSection.tsx
  • src/pages/BlindSessionNew.tsx
  • src/pages/Cellar.tsx
  • src/pages/EditCellarItem.tsx
  • src/pages/EditNote.tsx
  • src/pages/NewCellarItem.tsx
  • src/pages/NewNote.tsx
  • src/pages/NewShop.tsx
  • src/pages/SessionNew.tsx
  • src/pages/TagManager.tsx
  • src/pages/TeaDetail.tsx

Walkthrough

This 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

구성 / 파일(s) 요약
태그 생성 백엔드
backend/src/tags/dto/create-tag.dto.ts, backend/src/tags/tags.controller.ts, backend/src/tags/tags.service.ts
태그 생성을 위한 DTO(이름 1-50자, 선택적 카테고리), 인증된 POST /tags 엔드포인트, 트림/중복 검증 및 TagDto 반환 로직 추가.
태그 생성 프론트엔드
src/lib/api/social.api.ts, src/pages/TagManager.tsx
tagsApi.createTag() 메서드 추가, TagManager에 입력창 및 생성 버튼 UI, Enter/버튼 트리거, 성공 토스트 및 최근 탭 목록 업데이트 구현.
UI 컴포넌트
src/components/ui/CtaButton.tsx
새로운 재사용 가능한 CTA 버튼 컴포넌트(전폭, 중앙 정렬, 아이콘 지원, muted/primary 변형).
탐색 섹션 재설계
src/components/search/ExploreSection.tsx
새로운 TeaFlatRow 내부 컴포넌트로 풀폭 행 렌더링, 향미 탐색 섹션 상단 배치, 섹션 순서 재편(인기→사랑받는→신규→찻집), 선택자 카드 및 상단 CTA 영역 개선.
필터 패널 개선
src/components/search/FilterPanel.tsx
Sheet 기반 하단 시트 UI, 가로 스크롤 정렬 칩(동적 선택), 가격 범위/판매자명 필터 props 추가, 필터 적용 후 시트 자동 닫기.
검색 결과 업데이트
src/components/search/SearchResults.tsx
SearchCategory 타입 내보내기('all' 카테고리 추가), CATEGORY_CREATE_CONFIG에 아이콘 필드, CtaButton 지원, selectedIndex prop 및 자동 스크롤, 선택 항목 ring 스타일 추가.
검색 페이지 재구성
src/pages/Search.tsx
탭 토글 제거, 검색바 공백 시 자동으로 ExploreSection 표시, 입력 시 검색 모드 자동 전환, 키보드 네비게이션(화살표/Enter/Esc), 'all' 카테고리 통합 데이터 페칭, 최근 검색 UI 개선.
테스트 업데이트
src/pages/__tests__/Search.test.tsx
탭 토글 제거에 따른 테스트 재작성, ExploreSection 자동 표시 확인, 검색 입력 시 자동 전환 검증.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항인 탐색 페이지 리디자인(탭 제거, 통합검색, ExploreSection 개선)을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 #276의 모든 주요 목표를 충족합니다: 탭 토글 제거 및 자동 전환, '전체' 통합검색 카테고리, ExploreSection 리디자인(섹션 순서 재편, 에디토리얼 랭킹 스타일), CtaButton 추출, 키보드 네비게이션 추가, 인증 보호 추가.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #276 리디자인 범위 내에 있습니다. 백엔드 태그 생성 API 추가는 향미 태그 선택 후 결과 표시 기능 구현을 위한 필요 변경사항이며, CtaButton 추출은 ExploreSection 개선의 일환으로 제안된 범위 내입니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-276-search-redesign
📝 Coding Plan
  • Generate coding plan for human review comments

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.

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: 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 | 🟡 Minor

useEffect 의존성 배열에 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 기본 import

React 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, setFilterSellerName props가 인터페이스에 정의되어 있지만 컴포넌트 내부에서 사용되지 않습니다. 향후 기능 확장을 위한 것이라면 주석으로 의도를 명시하거나, 그렇지 않다면 제거를 고려하세요.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between c223d2b and 14811a9.

📒 Files selected for processing (13)
  • backend/src/tags/dto/create-tag.dto.ts
  • backend/src/tags/tags.controller.ts
  • backend/src/tags/tags.service.ts
  • src/components/home/HomeBanner.tsx
  • src/components/home/WeeklyStreak.tsx
  • src/components/search/ExploreSection.tsx
  • src/components/search/FilterPanel.tsx
  • src/components/search/SearchResults.tsx
  • src/components/ui/CtaButton.tsx
  • src/lib/api/social.api.ts
  • src/pages/Search.tsx
  • src/pages/TagManager.tsx
  • src/pages/__tests__/Search.test.tsx

Comment on lines +5 to +9
@IsString()
@MinLength(1)
@MaxLength(50)
@Transform(({ value }: { value: unknown }) => (typeof value === 'string' ? value.trim() : value))
name: string;
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

데코레이터 순서 문제: @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.

Suggested change
@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.

Comment on lines +34 to +39
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 };
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

중복 태그 생성 경합 시 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.

Suggested change
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.

Comment on lines +142 to +144
) : (
<p className="text-sm text-muted-foreground">인기 향미를 불러오는 중...</p>
)}
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

빈 데이터 상태에서 로딩 문구가 잘못 노출됩니다.

Line 143popularTags가 단순히 비어있는 경우에도 계속 “불러오는 중...”으로 보입니다. 실제 빈 상태 문구로 분리해 주세요.

🔧 제안 수정
-          <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.

Suggested change
) : (
<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.

Comment on lines +231 to +235
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);
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

탭 제거 검증 조건이 약해서 회귀를 놓칠 수 있습니다.

Line 235hasSearchTab && 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>
@FLYLIKEB FLYLIKEB merged commit d611d2c into main Mar 18, 2026
3 checks passed
@FLYLIKEB FLYLIKEB deleted the feature/issue-276-search-redesign branch March 18, 2026 09:56
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.

feat: 탐색 페이지(/sasaek) UI/UX 리디자인 - 미니멀 & 에디토리얼 방향

1 participant