Feat : TJ 공식 차트 API 연동으로 popular 페이지 개편 (#295) - #296
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGv6Vf3GKCCogvBmEgZb2g
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
/describe |
|
/review |
|
/improve |
PR Summary by QodoIntegrate TJ official chart pipeline and revamp /popular with monthly/genre rankings
AI Description
Diagram
High-Level Assessment
Files changed (13)
|
Code Review by Qodo
1. Incomplete month list
|
| for (const targetMonth of targetMonths) { | ||
| const searchStartDate = format(startOfMonth(targetMonth), 'yyyy-MM-dd'); | ||
| const searchEndDate = format(endOfMonth(targetMonth), 'yyyy-MM-dd'); | ||
| const chartMonth = format(startOfMonth(targetMonth), 'yyyy-MM-dd'); |
There was a problem hiding this comment.
1. crawltjchartbackfill.ts missing checkpoint 📘 Rule violation ☼ Reliability
The new TJ 차트 백필 스크립트는 여러 개월/장르를 순회하는 장시간 작업인데, 중단 시 재개할 수 있는 체크포인트를 src/assets/에 저장/로드하지 않습니다. 실행 중단 시 전체 재처리 또는 누락 가능성이 있어 요구사항을 충족하지 못합니다.
Agent Prompt
## Issue description
`packages/crawling/src/cron/crawlTjChartBackfill.ts`는 월/장르를 대량 순회하는 장시간 스크립트인데, 중단 시 재개(resume)할 체크포인트를 `src/assets/` 하위 텍스트 파일로 저장/로드하지 않습니다.
## Issue Context
컴플라이언스 규칙은 장시간 스크립트가 (1) 시작 시 체크포인트를 읽고, (2) 처리 진행에 따라 주기적으로 체크포인트를 갱신하여, (3) 다음 실행 시 이미 처리한 구간을 건너뛰도록 요구합니다.
## Fix Focus Areas
- packages/crawling/src/cron/crawlTjChartBackfill.ts[18-77]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // 1) 데이터가 존재하는 월 목록 조회 | ||
| const { data: monthRows, error: monthError } = await supabase | ||
| .from('chart_rankings') | ||
| .select('chart_month') | ||
| .order('chart_month', { ascending: false }); |
There was a problem hiding this comment.
2. Incomplete month list 🐞 Bug ≡ Correctness
GET /api/tj-chart가 chart_rankings의 모든 row에서 chart_month를 가져와 availableMonths를 만들기 때문에, Supabase 응답 row 제한/트렁케이션으로 오래된 월이 누락될 수 있습니다. 그 결과 월 선택 UI가 실제 DB에 있는 과거 월 데이터를 숨길 수 있습니다.
Agent Prompt
### Issue description
`/api/tj-chart`에서 `availableMonths`를 만들기 위해 `chart_rankings` 전체에서 `chart_month`를 조회하고 `Set`으로 중복 제거하고 있습니다. `chart_rankings`는 월/장르/순위 단위로 row 수가 많아지기 때문에, Supabase가 한 번의 select에서 반환하는 row 수 제한에 걸려 과거 월이 응답에 포함되지 않을 수 있고, 그 상태로 `availableMonths`를 계산하면 월 목록이 잘못됩니다.
### Issue Context
- 크롤러는 `StrType` 전체에 대해 TOP 차트를 수집/저장하므로 월 단위로 다수의 row가 생성됩니다.
- 레포 내 Supabase 조회 코드에서도 row 제한 존재를 인지하고 `.limit(...)`를 사용하는 패턴이 이미 있습니다.
### Fix Focus Areas
- apps/web/src/app/api/tj-chart/route.ts[25-45]
### Suggested fix
- 월 목록 조회 쿼리를 **중복이 구조적으로 발생하지 않도록** 바꾸세요. 예를 들어 월당 1개 row만 나오도록 고정 조건을 추가하면 됩니다.
- 예: `type = StrType.All` AND `rank = 1`만 조회
- 또는 DB view/RPC로 `select distinct chart_month ...`를 제공
예시(개념):
```ts
const { data: monthRows, error: monthError } = await supabase
.from('chart_rankings')
.select('chart_month')
.eq('type', StrType.All)
.eq('rank', 1)
.order('chart_month', { ascending: false });
const availableMonths = (monthRows ?? []).map(r => r.chart_month as string);
```
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| const { data } = await axios.get<TjChartApiResponse>( | ||
| 'https://www.tjmedia.com/legacy/api/topAndHot100', | ||
| { | ||
| params: { |
There was a problem hiding this comment.
3. No crawl request timeout 🐞 Bug ☼ Reliability
fetchTjChart의 axios.get에 timeout이 없어 네트워크 지연/서버 응답 정지 시 크롤링이 외부 러너 타임아웃까지 장시간 블록될 수 있습니다. 이 경우 월간 크롤링이 완료되지 않아 차트 데이터가 갱신되지 않을 수 있습니다.
Agent Prompt
### Issue description
`fetchTjChart()`가 `axios.get(...)`을 timeout 없이 호출합니다. axios 기본은 애플리케이션 레벨 타임아웃이 없기 때문에, 요청이 지연되면 해당 genre 이후 로직이 모두 멈춘 상태로 오래 지속될 수 있습니다.
### Issue Context
웹앱 axios 클라이언트는 이미 timeout을 명시하고 있어(10초) 레포의 표준 패턴과도 불일치합니다.
### Fix Focus Areas
- packages/crawling/src/utils/tjChart.ts[13-35]
### Suggested fix
- `axios.get` 옵션에 `timeout`을 추가하세요(예: 10~30초).
- (선택) 특정 genre 실패 시 전체를 즉시 종료할지/해당 genre만 스킵할지 정책을 정하고, 재시도(최대 N회, 백오프)와 함께 로그를 남기세요.
예시(개념):
```ts
const { data } = await axios.get<TjChartApiResponse>(URL, {
params: {...},
timeout: 30_000,
});
```
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (unmatched.length > 0) { | ||
| fs.appendFileSync(UNMATCHED_LOG_FILE, unmatched.join('\n') + '\n', 'utf-8'); | ||
| console.log(`📝 미매칭 목록 기록: ${UNMATCHED_LOG_FILE}`); |
There was a problem hiding this comment.
4. Unmatched log not retained 🐞 Bug ◔ Observability
crawlTjChart가 미매칭 곡 목록을 로컬 txt 파일로 기록하지만, GitHub Actions 워크플로우가 해당 파일을 업로드/커밋하지 않아 실행 종료 후 데이터가 사라집니다. 이로 인해 정기 크롤링에서 매칭 실패 원인을 추적하기 어렵습니다.
Agent Prompt
### Issue description
정기 크롤링에서 `tjChartUnmatched.txt`에 미매칭 목록을 append 하지만, Actions 러너는 ephemeral이므로 워크플로우에서 아티팩트 업로드나 커밋을 하지 않으면 파일이 보존되지 않습니다.
### Issue Context
현재 워크플로우는 의존성 설치 후 크롤 스크립트만 실행하고 종료합니다.
### Fix Focus Areas
- packages/crawling/src/cron/crawlTjChart.ts[18-19]
- packages/crawling/src/cron/crawlTjChart.ts[58-60]
- .github/workflows/crawl_tj_chart.yml[26-38]
### Suggested fix
- 워크플로우에 `actions/upload-artifact@v4` 스텝을 추가하여 `packages/crawling/src/assets/tjChartUnmatched.txt`가 존재할 때 업로드하세요.
- 또는 파일 대신 콘솔에 미매칭 목록을 요약/샘플링 출력하고, 전체 목록은 Supabase 테이블/스토리지(S3 등)에 적재하는 방식으로 영구 보관하세요.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
- 접속 시 KST 기준 전월 차트를 기본으로 조회 - 좌우 화살표로 월 이동, 수집된 월 범위를 벗어나면 비활성화 - 장르는 셀렉트 대신 한 줄 가로 스크롤 뱃지로 렌더링해 하나씩 선택 - 월/장르 전환 시 목록이 사라지지 않도록 keepPreviousData 적용 - TjChartRankingList -> ChartRankingList 리네임 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- fetchTjChart 에 timeout 10초 추가 (웹앱 axios 클라이언트와 동일) - /api/tj-chart 가 month 파라미터를 형식 검증만 하고 그대로 조회하도록 변경 (수집되지 않은 월도 빈 목록으로 응답해 월 이동이 자연스럽게 동작) - availableMonths 조회를 종합 차트 상위 10위로 좁혀 불필요한 row 전송 제거 (14434건 -> 129건, 월 목록 결과는 동일) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Commands 에 tj-chart, tj-chart-backfill, tj-all-number, format 추가 - GitHub Actions 표에 crawl_tj_chart.yml 추가 및 실제 스케줄/파일명과 불일치 정정 - Supabase 테이블 표에 chart_rankings 추가 - TJ 공식 차트 파이프라인 섹션 신설 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 카드 헤더의 차트 제목을 페이지 h1으로 빼고 조회 월을 중앙 정렬 - 장르 12종에 이모지 추가 (Windows에서 깨지는 국기·Unicode 14 이후 문자는 제외) - 데스크톱 마우스 휠로 뱃지 가로 스크롤. 양 끝에서는 페이지 세로 스크롤을 가로채지 않음 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TJ는 한 곡에 반주 번호를 여러 개 등록한다(일반/MR/라이브/60이상 전용). 곡 검색 페이지 아이콘을 파싱해 songs.badges(text[])에 저장한다. - utils/tjBadge.ts: 뱃지 파서. not_found/num_mismatch를 구분해 반환하고 매핑에 없는 클래스는 원문 보존 + unknownBadges로 보고 - crawlAllTJSongByNumber.ts: puppeteer(건당 1.5~2초) → fetch+cheerio(건당 24ms)로 전환. 뱃지 수집을 겸하고 블록 단위 진행 파일로 재개를 지원 - crawlTjBadges.ts: badges가 null인 곡만 채우는 증분 수집 (pnpm tj-badges) - postSongsBatchDB: 신규 곡 청크 insert, 실패 시 행 단위 재시도 - updateSongBadgesDB: 뱃지 조합별로 묶어 갱신 (URL 길이 제한 때문에 id 100개씩) - 워크플로우에 upload-artifact 추가 (러너 종료 시 로그가 사라지던 문제) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 백필 대상 구간 2025-01~2026-01 → 2026-02~2026-07 (데이터 공백 구간) - 전수 순회 결과 로그: 신규 곡 1,373건, 제목 표기 정정 957건 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
matchChartRows가 미매칭 항목의 원본을 함께 반환하고, cron이 TJ 곡 검색 페이지에서 곡 정보를 받아 songs에 추가한 뒤 다시 매칭한다. 이 단계가 없으면 MR·라이브처럼 DB에 없는 버전이 매달 미매칭으로 다시 쌓인다. 곡 정보를 차트 API가 아닌 검색 페이지에서 받는 이유는 전수 순회가 검색 페이지 표기로 제목을 맞춰뒀기 때문이다. 뱃지는 차트 API로도 만들 수 있어(badgesFromChartItem) 두 값을 대조하고 불일치만 기록한다. - 차트 API 타임아웃 10초 → 30초. 실측 응답이 2.7~11.4초라 정상 응답이 잘려나갔다 - 지수 백오프 재시도 3회 추가. 228회 요청 중 한 번만 실패해도 전체가 죽던 구조였다 - 곡 검색 페이지는 100ms 내외라 타임아웃을 10초로 분리 - postSongsBatchDB가 select()로 id를 반환해 삽입 직후 재매칭이 가능하게 함 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1,646줄이 전부 매칭 가능해져 파일을 비운다. 내용은 git 이력에 남는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
로그가 러너 파일시스템에만 쌓여 잡 종료 시 사라지던 문제. 곡 자동 보충이 붙으면서 매달 무엇이 추가됐는지 확인할 수단이 필요해졌다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TJ가 한 곡을 버전별로 따로 차트에 올리므로 같은 곡이 여러 순위에 보인다. 무엇이 다른지 알 수 있도록 MR / LIVE / 60↑ 뱃지를 제목 위에 표시한다. - MV(뮤직비디오 유무)는 어느 버전을 부를지 고르는 데 도움이 안 되므로 노출하지 않음 - 60은 구형 반주기에서 재생되지 않는다는 뜻이라 실사용에 중요해 포함 - title 속성으로 각 뱃지의 의미를 설명 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- SongBadges 공용 컴포넌트로 인기차트·검색 결과가 같은 모양을 쓰게 함 - 뱃지 상수를 types/tjChart → types/song 으로 이동 (차트 전용이 아니라 곡의 속성) - /api/search 응답 매핑에 badges 누락돼 있어 추가 (select는 * 라 이미 내려오고 있었음) - 렌더 경로가 끊긴 PopularRankingList.tsx 삭제 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
badges가 계속 null인 곡 = TJ가 번호 조회에 응답하지 않는 곡. 검색에 나와도 반주기에서 재생되지 않는다. 2,082곡을 삭제했다. - 차트에 오른 곡(임영웅메들리)과 num_ky 보유 24곡은 제외 - song_tags / invalid_ky_songs / verify_ky_songs를 먼저 지워야 FK에 걸리지 않는다. 뒤 두 테이블은 song_id 컬럼 없이 PK인 id가 곧 songs.id다 - 기본은 미리보기. DEAD_SONGS_APPLY=true 로만 실제 삭제 - 삭제분은 src/assets/deadSongsRemoved.json 에 백업 crawlTjBadges: 실패한 곡이 정렬 앞자리를 계속 차지해 그 뒤로 진행하지 못하던 문제를 num_tj 커서 방식으로 수정 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
카드에 TJ·금영 번호가 나란히 있어 '60↑'만으로는 금영 기기 얘기로 읽힐 수 있다. '60 이상 반주기'는 TJ 기종을 가리키므로 라벨과 설명에 TJ를 박는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
라벨마다 브랜드를 넣는 대신(TJ 60↑) 묶음 앞에 TJ를 한 번만 표시한다. 뱃지 3종 모두 TJ 등록 정보에서 나오므로 묶음 단위로 밝히는 편이 정확하고, 라벨도 짧게 유지된다. 번호 영역과 같은 brand-tj 색을 써서 같은 출처임을 시각적으로 연결했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
뱃지 밖에 따로 두면 별개 요소로 보여, 테두리 안에 함께 넣어 한 덩어리로 읽히게 한다. icongubun이 단일값이라 표시되는 뱃지는 항상 최대 1개여서 TJ가 중복되지 않는다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
crawlRecentTJ가 넣는 신곡은 badges가 null인데, removeDeadTjSongs가 badges is null을 'TJ에서 사라진 곡'으로 판정했다. 그대로 두면 매일 들어오는 멀쩡한 신곡이 삭제된다. 1. crawl_recent_tj.yml이 recent-tj 직후 tj-badges를 실행해 신곡 뱃지를 채운다 2. removeDeadTjSongs가 삭제 직전 TJ로 재조회한다 - 살아있음 → 지우지 않고 뱃지를 채움 - 조회 실패 → 일시적 오류를 '사라짐'으로 오인하지 않도록 건드리지 않음 - not_found → 그때만 삭제 부수 정리: - fetchBadgeRow를 utils/tjBadge로 공용화 (crawlTjBadges와 판정 로직 일치 보장) - 백업 파일명에 타임스탬프. 재실행이 앞선 백업을 덮어써 태그 기록이 유실됐던 문제 - 삭제 대상이 0건이면 빈 백업 파일을 만들지 않음 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 가로 스크롤바를 thin으로 노출. 더 훑을 장르가 남았다는 걸 알 수 있게 한다. thumb 색은 border 토큰이 다크 테마에서 배경과 거의 같아 muted-foreground를 쓴다. - 트로트 🎺 → 📻, 랩/힙합 🧢 → 🔥 트럼펫은 트로트와, 모자는 음악 장르와 연결이 약했다. 아코디언(🪗)은 Windows Segoe UI Emoji에 글리프가 없어 두부로 깨져 제외했다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📌 PR 제목
Feat : TJ 공식 차트 API 연동으로 popular 페이지 개편
📌 변경 사항
topAndHot100API를 장르별로 크롤링하는crawlTjChart.ts추가 (매달 지난달 기준, GitHub Actions로 매달 1일 KST 10시 자동 실행)crawlTjChartBackfill.ts추가, 조회 시마다 순위 표(console.table)와 저장 결과를 즉시 로그로 확인 가능packages/crawling/src/utils/tjChart.ts로 추출해 두 크론 스크립트에서 재사용chart_rankings테이블 upsert용postTjChartRankingsDB추가,StrTypeenum 및 TJ API 응답 타입 정의GET /api/tj-chart라우트(월/장르 파라미터로chart_rankings+songs조인 조회) 및lib/api/tjChart.ts,queries/tjChartQuery.ts,types/tjChart.ts추가/popular페이지의 기존PopularRankingList(포인트 기반 추천)를 월/장르 선택 가능한TjChartRankingList로 교체 (기존 엄지척/thumb 시스템 코드는 그대로 유지)💬 추가 참고 사항
num_tj매칭 실패 곡은 저장하지 않고src/assets/tjChartUnmatched.txt(정기 크롤링) /tjChartBackfillUnmatched.txt(백필)에만 기록