Conversation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
[Feat] : 노래별 태그 추출 크롤링 코드 작성 (#173)
[Feat] : 검색 로그 기능 추가 및 UI 개선 (#174)
[Feat] : 검색어 히스토리 컴포넌트 UI 개선 (#175)
[Feat] : 네온 나이트 다크 테마 적용 (#179)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Review Summary by QodoVersion 2.4.0 - AI Song Tagging, Search Logs, and Neon Night Theme
WalkthroughsDescription• AI-powered automatic song tagging using GPT-4o-mini with tag caching • Search log API and popular search history feature with aggregation • Neon Night dark theme with vibrant accent colors and glow effects • Theme toggle in header with expandable button UI pattern • Terminology change from "좋아요" (like) to "즐겨찾기" (bookmark) throughout Diagramflowchart LR
A["Song Database"] -->|getSongsAllDB| B["Tagging Pipeline"]
B -->|autoTagSong| C["GPT-4o-mini AI"]
C -->|tag_ids| D["song_tags Table"]
E["Search Input"] -->|postSearchLog| F["search_logs Table"]
F -->|getSearchLog| G["Popular Search History"]
H["Theme Toggle"] -->|setTheme| I["Neon Night Dark Mode"]
I -->|CSS Variables| J["Updated UI Colors"]
File Changes1. packages/crawling/src/utils/getSongTag.ts
|
Code Review by Qodo
1. Workflow has no triggers
|
| on: | ||
| # schedule: | ||
| # - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00) | ||
| # workflow_dispatch: | ||
|
|
There was a problem hiding this comment.
1. Workflow has no triggers 🐞 Bug ☼ Reliability
.github/workflows/tagging_song.yml defines on: but all events are commented out, so the workflow cannot be triggered (and may be treated as invalid by GitHub Actions). This blocks the tagging pipeline from running at all.
Agent Prompt
### Issue description
The new `tagging_song.yml` workflow has no active triggers because `schedule` and `workflow_dispatch` are commented out under `on:`.
### Issue Context
Without at least one trigger, GitHub Actions will never run this workflow (no scheduled runs and no manual dispatch).
### Fix Focus Areas
- .github/workflows/tagging_song.yml[3-7]
### Suggested change
Enable at least `workflow_dispatch` (and optionally `schedule`) under `on:` so the workflow can run.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| export default function SearchHistory({ onHistoryClick }: SearchHistoryProps) { | ||
| const { searchHistory, removeFromHistory } = useSearchHistoryStore(); | ||
| const [isHydrated, setIsHydrated] = useState(false); | ||
|
|
||
| if (searchHistory.length === 0) return null; | ||
| useEffect(() => { | ||
| if (searchHistory.length > 0) { | ||
| setIsHydrated(true); | ||
| } | ||
| }, [searchHistory]); | ||
|
|
||
| return ( | ||
| <div className="flex gap-2 overflow-x-auto pb-4"> | ||
| {searchHistory.map((term, index) => ( | ||
| <div | ||
| key={`${term}-${index}`} | ||
| className="bg-background flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm" | ||
| > | ||
| <span | ||
| className="hover:text-primary max-w-30 cursor-pointer truncate text-left" | ||
| onClick={() => onHistoryClick(term)} | ||
| > | ||
| {term} | ||
| </span> | ||
| <span | ||
| className="hover:text-destructive cursor-pointer" | ||
| onClick={() => removeFromHistory(term)} | ||
| title="검색 기록 삭제" | ||
| > | ||
| <X className="h-4 w-4" /> | ||
| </span> | ||
| <div className="h-30 overflow-hidden"> | ||
| <div className="flex items-center gap-2"> | ||
| <Clock className="h-4 w-4" /> | ||
| <p className="m-2">최근 검색어</p> | ||
| </div> | ||
| {!isHydrated ? ( | ||
| <div className="flex items-center justify-center py-4"> | ||
| <Loader2 className="h-5 w-5 animate-spin" /> | ||
| </div> | ||
| ))} | ||
| ) : ( |
There was a problem hiding this comment.
2. Search history infinite spinner 🐞 Bug ≡ Correctness
SearchHistory sets isHydrated to true only when searchHistory.length > 0, so first-time users (or users with cleared history) will see a loading spinner forever. This breaks the empty-state UI where SearchHistory is rendered when query is empty.
Agent Prompt
### Issue description
`SearchHistory` shows a spinner forever when the persisted history is empty because `isHydrated` is only set when `searchHistory.length > 0`.
### Issue Context
The zustand store initializes `searchHistory` to `[]`. On a first visit (or after clearing history), it remains `[]` after hydration, so the effect never sets `isHydrated` and the UI stays in the loading branch.
### Fix Focus Areas
- apps/web/src/app/search/SearchHistory.tsx[10-53]
- apps/web/src/stores/useSearchHistoryStore.ts[14-16]
### Suggested fix
- Set `isHydrated` to `true` on mount (e.g., `useEffect(() => setIsHydrated(true), [])`) OR use zustand persist hydration helpers (`useSearchHistoryStore.persist.hasHydrated()` / `onRehydrateStorage`) to drive the flag.
- Optionally render an explicit empty state when hydrated but `searchHistory.length === 0`.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| export async function GET(): Promise<NextResponse<ApiResponse<SearchLogCount[]>>> { | ||
| try { | ||
| const supabase = await createClient(); | ||
| const { data, error } = await supabase.from('search_logs').select('text'); | ||
|
|
||
| if (error) throw error; | ||
|
|
||
| const countMap = new Map<string, number>(); | ||
| for (const row of data) { | ||
| countMap.set(row.text, (countMap.get(row.text) ?? 0) + 1); | ||
| } | ||
|
|
||
| const result: SearchLogCount[] = Array.from(countMap, ([text, count]) => ({ | ||
| text, | ||
| count, | ||
| })).sort((a, b) => b.count - a.count); | ||
|
|
||
| return NextResponse.json({ success: true, data: result }); |
There was a problem hiding this comment.
3. Search logs full-table scan 🐞 Bug ➹ Performance
GET /api/search/log selects all rows from search_logs, aggregates counts in memory, and returns the full sorted list; this will degrade/timeout as logs grow. The Search page calls this endpoint in the empty-query state via PopularSearchHistory, so it impacts real user page loads.
Agent Prompt
### Issue description
`GET /api/search/log` performs an unbounded read and in-memory aggregation over `search_logs`, which will slow down as the table grows.
### Issue Context
This endpoint is called from the Search page empty state (popular searches UI), so its latency directly affects perceived page performance.
### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[11-28]
- apps/web/src/app/search/PopularSearchHistory.tsx[11-13]
### Suggested fix
- Move aggregation to the database (e.g., a SQL view/RPC that does `GROUP BY text ORDER BY count DESC LIMIT 10`).
- Enforce server-side limits (top N only) and optionally a time window (e.g., last 7/30 days).
- Consider caching (short TTL) since this is read-heavy UI.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| export async function POST(request: Request): Promise<NextResponse<ApiResponse<void>>> { | ||
| try { | ||
| const { text } = await request.json(); | ||
|
|
||
| const supabase = await createClient(); | ||
| const { error } = await supabase.from('search_logs').insert({ text }); | ||
|
|
||
| if (error) throw error; | ||
|
|
||
| return NextResponse.json({ success: true }); |
There was a problem hiding this comment.
4. Search log insert poisoning 🐞 Bug ⛨ Security
POST /api/search/log inserts arbitrary text from the request body without type/length validation or throttling, allowing clients to spam the table and pollute popular-search output. This can cause DB growth, slower GET aggregation, and user-visible garbage terms.
Agent Prompt
### Issue description
`POST /api/search/log` allows unvalidated user input to be stored, making the endpoint easy to spam and poison.
### Issue Context
The stored values directly feed the popular-search UI. Also, uncontrolled growth worsens the GET endpoint performance.
### Fix Focus Areas
- apps/web/src/app/api/search/log/route.ts[45-52]
- apps/web/src/hooks/useSearchSong.ts[83-89]
### Suggested fix
- Validate `text`:
- must be a string
- trim and reject empty
- enforce max length (e.g., 50–100 chars)
- optionally allow only a safe character set
- Add basic abuse protection:
- rate limit per IP/session (middleware or server-side store)
- dedupe (e.g., upsert into a counter table instead of inserting a new row per search)
- optionally require authentication for POST if acceptable for product goals.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
📌 PR 제목
[Type] : 작업 내용 요약
📌 변경 사항
💬 추가 참고 사항