Skip to content

[Feat] : 노래 검색 시 song_tags 언어 태그 필터링 기능 추가 (#276)#278

Open
GulSam00 wants to merge 3 commits into
developfrom
feat/276-songTagsFilter
Open

[Feat] : 노래 검색 시 song_tags 언어 태그 필터링 기능 추가 (#276)#278
GulSam00 wants to merge 3 commits into
developfrom
feat/276-songTagsFilter

Conversation

@GulSam00

Copy link
Copy Markdown
Owner

📌 PR 제목

[Feat] : 노래 검색 시 song_tags 언어 태그 필터링 기능 추가

📌 변경 사항

  • /api/searchlanguageTag 쿼리 파라미터 추가 — 지정 시 song_tags!inner(tag_id) 조인으로 언어 태그 필터링, 미지정 시 기존 동작 그대로 유지(태그 없는 곡도 정상 노출)
  • 언어 태그 목록(한국노래/J-POP/POP/글로벌, tag_id 100~103)은 DB 조회 대신 src/constants/languageTags.ts에 하드코딩 상수로 관리
  • 검색 페이지에 LanguageTagFilter 컴포넌트 추가 — 검색 입력창 아래, Tabs 기반 네온 컬러 칩 UI(태그별 chart-1/2/4/5 색상, 선택 시 해당 색으로 배경 채움 + glow)
  • 태그 선택은 draft 상태로만 반영되고, "검색" 버튼/Enter로 handleSearch() 실행 시에만 실제 쿼리에 커밋되도록 처리 (기존 검색어/검색타입과 동일한 draft-commit 패턴)
  • useToggleToSingMutation/useToggleLikeMutation/useSaveMutation의 쿼리 키에도 languageTag를 포함시켜, 필터가 걸린 상태에서도 좋아요/부를곡 토글의 낙관적 업데이트가 정상 동작하도록 수정

💬 추가 참고 사항

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
singcode Ready Ready Preview, Comment Jul 12, 2026 4:58pm

@GulSam00

Copy link
Copy Markdown
Owner Author

/describe

@GulSam00

Copy link
Copy Markdown
Owner Author

/review

@GulSam00

Copy link
Copy Markdown
Owner Author

/improve

@qodo-code-review

qodo-code-review Bot commented Jul 12, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add languageTag filtering to song search (API + UI + query keys)

✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Add languageTag query param to /api/search to filter by song_tags language tags.
• Add neon Tabs-based language filter UI with draft→commit behavior on search.
• Include languageTag in React Query keys so optimistic updates stay consistent.
Diagram

graph TD
A["SearchPage + LanguageTagFilter"] --> B["useSearchSong"] --> C["React Query: searchSongQuery"] --> D["API client: searchSong.ts"] --> E["GET /api/search"] --> F{"languageTag set?"}
F -- "no" --> G["Supabase select songs"] --> H[("Postgres: songs + song_tags")]
F -- "yes (inner join song_tags)" --> G
subgraph Legend
direction LR
_ui["UI/Module"] ~~~ _dec{"Decision"} ~~~ _db[("Database")]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always select song_tags with left join, filter conditionally
  • ➕ Single select clause regardless of filter presence
  • ➕ Could enable showing tag metadata in results consistently
  • ➖ PostgREST filtering on related tables often behaves like inner join anyway
  • ➖ Risk of accidentally excluding untagged songs without careful OR/is.null logic
  • ➖ Potentially larger payload for the common unfiltered case
2. Server-driven language tag list (DB table or endpoint)
  • ➕ No redeploy needed when tags change
  • ➕ Avoids hardcoding tag IDs in the client
  • ➖ Adds another API/data dependency during search page load
  • ➖ More moving parts for a small, stable tag set
3. Model languageTag as a string enum end-to-end
  • ➕ Avoids number parsing/coercion across UI → query → API
  • ➕ More self-documenting than magic numeric IDs
  • ➖ Requires a schema/DB mapping change (or a translation layer) from ids to codes
  • ➖ Bigger migration than needed for this feature

Recommendation: The chosen approach (conditional song_tags!inner(tag_id) only when languageTag is provided) is a good fit for PostgREST/Supabase: it preserves the current behavior (untagged songs remain visible) while enabling precise filtering. Hardcoding the small tag set is reasonable if IDs are stable; revisit server-driven tags only if the list becomes dynamic or admin-managed.

Files changed (9) +238 / -79

Enhancement (8) +160 / -16
route.tsAdd 'languageTag' query param and conditional song_tags inner-join filter +58/-10

Add 'languageTag' query param and conditional song_tags inner-join filter

• Introduces 'languageTag' parsing from query params and applies a 'song_tags.tag_id' filter when present. Builds 'selectClause' conditionally to include 'song_tags!inner(tag_id)' only for filtered searches, preventing untagged songs from being excluded when no filter is selected.

apps/web/src/app/api/search/route.ts

HomePage.tsxRender LanguageTagFilter and plumb handler/state into SearchPage +7/-1

Render LanguageTagFilter and plumb handler/state into SearchPage

• Adds the 'LanguageTagFilter' component below the search controls. Passes selected 'languageTag' and the change handler from 'useSearchSong', and updates the save-song modal hook call to include the committed language tag.

apps/web/src/app/search/HomePage.tsx

LanguageTagFilter.tsxNew neon Tabs-based language tag filter UI +59/-0

New neon Tabs-based language tag filter UI

• Implements a chip-style Tabs control with an '전체' option plus predefined language tags. Uses explicit Tailwind class strings per tag to ensure correct static extraction and consistent light/dark active styling with glow effects.

apps/web/src/app/search/LanguageTagFilter.tsx

languageTags.tsAdd hardcoded language tag constants (ids 100–103) +8/-0

Add hardcoded language tag constants (ids 100–103)

• Defines the stable language tag list ('한국노래', 'J-POP', 'POP', '글로벌') as a shared constant rather than fetching from the DB. Intended for use by the filter UI and related logic.

apps/web/src/constants/languageTags.ts

useSaveSongModal.tsInclude languageTag when saving songs from filtered search +6/-2

Include languageTag when saving songs from filtered search

• Extends the hook signature to accept an optional 'languageTag'. Passes it through to the save mutation so optimistic updates and invalidation can target the correct filtered search cache.

apps/web/src/hooks/useSaveSongModal.ts

useSearchSong.tsAdd draft/committed languageTag state and expose handler +14/-1

Add draft/committed languageTag state and expose handler

• Adds 'languageTag' (draft) and 'queryLanguageTag' (committed) state to match the existing draft→commit search pattern. Wires 'queryLanguageTag' into infinite search queries and toggle mutations so caches align with the active filter.

apps/web/src/hooks/useSearchSong.ts

searchSong.tsSend languageTag to /search API +4/-2

Send languageTag to /search API

• Extends both search API wrappers to accept an optional 'languageTag' and include it in request params. Keeps existing params unchanged when the filter is unset.

apps/web/src/lib/api/searchSong.ts

tag.tsAdd Tag type for shared tag constants +4/-0

Add Tag type for shared tag constants

• Introduces a simple 'Tag' interface ('id', 'name') used by the language tag constants and UI.

apps/web/src/types/tag.ts

Bug fix (1) +78 / -63
searchSongQuery.tsKey search queries/mutations by languageTag for correct optimistic updates +78/-63

Key search queries/mutations by languageTag for correct optimistic updates

• Adds 'languageTag' to the infinite query key ('['searchSong', search, searchType, languageTag]') and threads it into fetchers. Updates toSing/like/save optimistic update logic to cancel, patch, rollback, and invalidate the correct cache entry when a filter is active.

apps/web/src/queries/searchSongQuery.ts

@qodo-code-review

qodo-code-review Bot commented Jul 12, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 27 rules

Grey Divider


Remediation recommended

1. Mismatched searchSong query keys ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
useInfiniteSearchSongQuery는 queryKey에 languageTag를 포함하지만(useInfiniteQuery: 4요소), useSearchSongQuery는
기존 3요소 키를 유지합니다. 이 상태에서 languageTag를 포함한 키를 기준으로 하는 낙관적 업데이트/롤백은 3요소 캐시를 갱신하지 못해, 해당 훅을 사용하는 화면에서는
좋아요/부를곡/저장 상태가 stale해질 수 있습니다.
Code

apps/web/src/queries/searchSongQuery.ts[R36-51]

export const useInfiniteSearchSongQuery = (
  search: string,
  searchType: string,
  isAuthenticated: boolean,
+  languageTag?: number,
) => {
  return useInfiniteQuery({
-    queryKey: ['searchSong', search, searchType],
+    queryKey: ['searchSong', search, searchType, languageTag],
    queryFn: async ({ pageParam }) => {
-      const response = await getInfiniteSearchSong(search, searchType, isAuthenticated, pageParam);
+      const response = await getInfiniteSearchSong(
+        search,
+        searchType,
+        isAuthenticated,
+        pageParam,
+        languageTag,
+      );
Relevance

⭐⭐⭐ High

They’ve accepted React Query optimistic-update/cache-consistency fixes before; likely to align
queryKeys to avoid stale state (PR231/117).

PR-#231
PR-#117

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
무한쿼리는 languageTag를 키에 포함하도록 변경되었지만, 동일 파일 내 일반 쿼리는 여전히 3요소 키를 사용하고, mutations는 4요소 키만 업데이트합니다.

apps/web/src/queries/searchSongQuery.ts[36-93]
apps/web/src/queries/searchSongQuery.ts[95-248]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`searchSong` 관련 queryKey 형태가 훅별로 달라(4요소 vs 3요소) 동일한 데이터임에도 캐시가 분리됩니다. 또한 이번 PR에서 mutation들이 4요소 키를 대상으로만 `cancelQueries/getQueryData/setQueryData/invalidateQueries`를 수행하므로, 3요소 키를 쓰는 훅 소비자는 optimistic update/rollback에서 제외될 수 있습니다.

### Issue Context
- `useInfiniteSearchSongQuery`: `['searchSong', search, searchType, languageTag]`
- `useSearchSongQuery`: `['searchSong', search, searchType]`
- `useToggleToSingMutation/useToggleLikeMutation/useSaveMutation`은 4요소 키를 사용

### Fix Focus Areas
- apps/web/src/queries/searchSongQuery.ts[36-93]
- apps/web/src/queries/searchSongQuery.ts[95-248]

### Suggested fix
옵션 중 하나를 선택:
1) `useSearchSongQuery`에도 `languageTag?: number` 파라미터를 추가하고 queryKey를 4요소로 통일(쿼리 함수도 languageTag 전달).
2) `useSearchSongQuery`가 더 이상 쓰이지 않는다면 제거(또는 deprecated 처리)하여 키 불일치 자체를 없애기.
3) `buildSearchSongQueryKey(search, searchType, languageTag)` 같은 헬퍼로 키 생성 로직을 단일화하고, `undefined` 대신 `null`을 사용해 키 형태를 항상 고정시키기(선택).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Invalid languageTag yields 500 ✓ Resolved 🐞 Bug ☼ Reliability
Description
GET /api/search가 languageTag를 문자열로 그대로 사용해 song_tags.tag_id에 eq 필터를 걸어, 비정상(non-numeric) 값이면
PostgREST 타입 캐스팅 에러로 500을 반환할 수 있습니다. 이는 클라이언트 입력 오류인데도 서버 오류로 처리되어 로그 노이즈/필터 기능 오동작을 유발합니다.
Code

apps/web/src/app/api/search/route.ts[R249-252]

    const type = searchParams.get('type') || 'title';
    const order = type === 'all' ? 'title' : type === 'number' ? 'num_tj' : type;
    const authenticated = searchParams.get('authenticated') === 'true';
+    const languageTag = searchParams.get('languageTag') || undefined;
Relevance

⭐ Low

Team previously rejected similar /api/search input-hardening (allowlist/sanitize) despite potential
500s (PR255).

PR-#255

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
languageTag를 별도 검증 없이 URL에서 가져와 필터에 바로 사용하고, Supabase 에러는 500으로 반환합니다.

apps/web/src/app/api/search/route.ts[59-66]
apps/web/src/app/api/search/route.ts[243-299]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`languageTag`가 URL에서 문자열로 그대로 읽혀 Supabase `.eq('song_tags.tag_id', languageTag)`에 전달됩니다. `tag_id`가 숫자 컬럼인 경우 `languageTag=abc` 같은 입력에서 PostgREST 캐스팅 에러가 발생하고, 현재 라우트는 이를 500으로 반환합니다.

### Issue Context
- 필터링은 정상 값(100~103)만 의도합니다.
- 비정상 값은 400으로 빠르게 거르거나, 명시적으로 무시(=필터 미적용)하는 계약을 정해야 합니다.

### Fix Focus Areas
- apps/web/src/app/api/search/route.ts[59-66]
- apps/web/src/app/api/search/route.ts[243-289]

### Suggested fix
1) `languageTagParam = searchParams.get('languageTag')`를 `trim()` 후,
2) `const parsed = languageTagParam ? Number.parseInt(languageTagParam, 10) : undefined;`
3) `languageTagParam`이 존재하는데 `Number.isNaN(parsed)` 또는 허용된 태그 집합(예: `{100,101,102,103}`)에 없으면 `400` 반환.
4) Supabase 필터에는 문자열 대신 숫자(`parsed`)를 전달하도록 `applyLanguageTagFilter`의 타입도 `value: number`로 맞추기.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

Comment thread apps/web/src/queries/searchSongQuery.ts
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.

노래 검색 시 song_tags 필터링 검색 기능 추가

1 participant