Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
01be06f
feat : 이달의 아티스트 투표/랭킹 DB 스키마 및 API 추가 (#307)
GulSam00 Aug 20, 2026
95aec2b
feat : 아티스트 마스터 테이블 백필 스크립트 추가 (#307)
GulSam00 Aug 20, 2026
9922a0e
feat : 아티스트 검색 API 추가 (#307)
GulSam00 Aug 20, 2026
d448f10
feat : 이달의 아티스트 투표/랭킹 UI 추가 (#307)
GulSam00 Aug 20, 2026
4e19605
doc : 이달의 아티스트 잔여 작업 CLAUDE.md에 기록 (#307)
GulSam00 Aug 20, 2026
00ce7ed
feat : 아티스트 백필 정규화 규칙 고도화 및 language_tag_id FK 적용 (#307)
GulSam00 Aug 21, 2026
52082d0
fix : 검색 언어 태그 필터 제거 (#307)
GulSam00 Aug 21, 2026
b539610
chore : 태그 자동 등록/참조 파이프라인 비활성화 (#307)
GulSam00 Aug 21, 2026
68c9cad
doc : 태그 기능 제거 내역 및 남은 작업 CLAUDE.md에 기록 (#307)
GulSam00 Aug 21, 2026
75fc2df
refactor : 아티스트 이름 정규화 규칙 @repo/constants로 이동 (#307)
GulSam00 Aug 23, 2026
bec7151
chore : recharts 도입 및 아티스트 이미지 백필 환경변수 선언 (#307)
GulSam00 Aug 23, 2026
10026b3
feat : 이달의 아티스트 랭킹 화면 액자·우승 별·득표 파이로 개편 (#307)
GulSam00 Aug 23, 2026
2f5fd93
fix : 곡 카드 이달의 아티스트 배지가 피처링·듀엣 곡에서 빠지던 문제 (#307)
GulSam00 Aug 23, 2026
9afd419
fix : 최신곡 카드 구분선·여백 인기 차트와 통일 (#307)
GulSam00 Aug 23, 2026
e8a1025
feat : 아티스트 사진 백필 스크립트 추가 및 월간 확정 워크플로 연동 (#307)
GulSam00 Aug 23, 2026
6a6bfaa
chore : 신고 내역 문구를 수정 요청 내역으로 변경 (#307)
GulSam00 Aug 23, 2026
6f0ef9e
chore : prettier 포맷 정리 (#307)
GulSam00 Aug 23, 2026
6614209
doc : 이달의 아티스트 진행 상황 및 크롤링 커맨드 문서 갱신 (#307)
GulSam00 Aug 23, 2026
f227db8
refactor : 이달의 아티스트 확정을 크롤링 배치로 이관하고 공개 엔드포인트 제거 (#307)
GulSam00 Aug 23, 2026
172f84c
doc : 확정 배치 이관 및 불필요해진 배포 설정 정리 (#307)
GulSam00 Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/backfill_artists.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Backfill Artists

on:
schedule:
- cron: "0 1 1 * *" # 매달 1일 KST 오전 10:00 실행 (UTC+9 → UTC 01:00) — 최근 35일치만 증분 반영
workflow_dispatch:
inputs:
since_days:
description: "최근 N일 이내 등록된 곡만 대상으로 함 (비워두면 전체 곡 대상 백필)"
required: false

jobs:
run-npm-task:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
run_install: false

- name: Install dependencies
working-directory: packages/crawling
run: pnpm install

- name: Create .env file
working-directory: packages/crawling
run: |
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env

# 스케줄 실행은 지난 한 달치만 증분 반영, 수동 실행은 입력값을 그대로 쓴다(비워두면 전체 백필).
- name: Determine backfill window
id: window
run: |
if [ "${{ github.event_name }}" = "schedule" ]; then
echo "since_days=35" >> "$GITHUB_OUTPUT"
else
echo "since_days=${{ inputs.since_days }}" >> "$GITHUB_OUTPUT"
fi

- name: run backfill script - backfillArtists.ts
working-directory: packages/crawling
env:
ARTIST_BACKFILL_SINCE_DAYS: ${{ steps.window.outputs.since_days }}
run: pnpm run backfill-artists

# 로그 파일은 러너 파일시스템에만 남고 잡이 끝나면 사라지므로, 실패해도(if: always()) 확인할 수 있게 올려둔다.
- name: Upload backfill log
if: always()
uses: actions/upload-artifact@v4
with:
name: artist-backfill-log-${{ github.run_id }}
path: packages/crawling/src/assets/artistBackfillLog.txt
if-no-files-found: ignore
retention-days: 30
53 changes: 53 additions & 0 deletions .github/workflows/finalize_artist_of_month.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: Finalize Artist of the Month

on:
schedule:
# 매일 00:10 KST(UTC 15:10) 실행. 확정 스크립트는 "지난달" 결과만 계산해 덮어쓰므로
# 하루에 한 번 이상 돌아도 안전하다 — GitHub Actions cron 지연으로 자정 실행을 놓치는 상황에 대비한다.
- cron: "10 15 * * *"
workflow_dispatch:

jobs:
finalize:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
run_install: false

- name: Install dependencies
working-directory: packages/crawling
run: pnpm install

- name: Create .env file
working-directory: packages/crawling
run: |
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env

- name: Finalize artist of the month
working-directory: packages/crawling
run: pnpm run finalize-artist

# 확정된 시상대(1~3위)의 사진만 채운다. 사진이 쓰이는 자리가 거기뿐이라
# 13,000명을 미리 채워둘 이유가 없다. 이 잡은 매일 돌지만 사진이 이미 있는 아티스트는
# 대상에서 빠지므로, 그 달 첫 성공 이후로는 대상이 0명이라 외부 API를 부르지 않는다.
#
# continue-on-error인 이유: 사진은 있으면 좋은 것이지 확정의 조건이 아니다.
# 외부 API(MusicBrainz·Deezer)가 죽어 있어도 순위 확정 자체는 성공으로 남겨야 한다.
- name: Backfill podium artist images
working-directory: packages/crawling
continue-on-error: true
env:
ARTIST_IMAGE_PODIUM: "3"
run: pnpm run backfill-artist-images
5 changes: 3 additions & 2 deletions .github/workflows/tagging_song.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Tagging Songs

on:
schedule:
- cron: "0 7 * * *" # 매일 한국 시간 16:00 실행 (UTC+9 → UTC 7:00)
# 태그 기능 비활성화로 스케줄 실행 중단. 필요하면 workflow_dispatch로 수동 실행.
# schedule:
# - cron: "0 7 * * *" # 매일 한국 시간 16:00 실행 (UTC+9 → UTC 7:00)
workflow_dispatch:

permissions:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/translation_jpn.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Translation J-POP Songs

on:
schedule:
- cron: "0 10 * * *" # 매일 한국 시간 19:00 실행 (UTC+9 → UTC 10:00)
# 태그 기능 비활성화로 스케줄 실행 중단. 필요하면 workflow_dispatch로 수동 실행.
# schedule:
# - cron: "0 10 * * *" # 매일 한국 시간 19:00 실행 (UTC+9 → UTC 10:00)
workflow_dispatch:

permissions:
Expand Down
75 changes: 64 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ packages/
query/ — Shared TanStack Query hooks for open-api (@repo/query)
api/ — Internal API utilities (@repo/api), built with tsup
ui/ — Shared UI components (@repo/ui)
constants/ — Shared domain constants & rules (@repo/constants). 아티스트 별칭(`artistAlias`),
이름 정규화(`artistName.ts`), 투표 집계 규칙(`rankArtistVotes.ts`), KST 월 계산
(`kstMonth.ts`)이 있다. 배치(`packages/crawling`)와 웹이 같은 규칙을 써야 해
여기 둔다 — 한쪽만 고치면 화면에 적힌 순위와 저장된 순위가 어긋난다.
eslint-config/ — Shared ESLint config (@repo/eslint-config)
format-config/ — Shared Prettier config (@repo/format-config)
typescript-config/ — Shared tsconfig bases
Expand Down Expand Up @@ -69,17 +73,17 @@ See [apps/web/CLAUDE.md](apps/web/CLAUDE.md) for full detail. Key points:
/start → /spsc → /red → /green → /refactor → /verify → /commit
```

| 커맨드 | 설명 | 필수 여부 |
| ----------- | --------------------------------------- | --------- |
| `/start` | GitHub Issue 생성 + 작업 브랜치 체크아웃 | 권장 |
| `/spsc` | 이슈 기반 작업 범위 정의 | 권장 |
| `/red` | 실패 테스트 먼저 작성 (TDD) | 생략 가능 |
| `/green` | 구현 코드 작성 | 필수 |
| `/refactor` | 코드 품질 개선 (동작 변경 X) | 생략 가능 |
| `/verify` | build, lint, format, test 전체 검증 | **필수** |
| `/commit` | 커밋 메시지 생성 및 커밋 | **필수** |
| `/pr` | PR 생성 및 Qodo AI 리뷰 요청 | 권장 |
| `/check-review` | Qodo 리뷰 코멘트 읽기 및 이슈 브리핑 | 권장 |
| 커맨드 | 설명 | 필수 여부 |
| --------------- | ---------------------------------------- | --------- |
| `/start` | GitHub Issue 생성 + 작업 브랜치 체크아웃 | 권장 |
| `/spsc` | 이슈 기반 작업 범위 정의 | 권장 |
| `/red` | 실패 테스트 먼저 작성 (TDD) | 생략 가능 |
| `/green` | 구현 코드 작성 | 필수 |
| `/refactor` | 코드 품질 개선 (동작 변경 X) | 생략 가능 |
| `/verify` | build, lint, format, test 전체 검증 | **필수** |
| `/commit` | 커밋 메시지 생성 및 커밋 | **필수** |
| `/pr` | PR 생성 및 Qodo AI 리뷰 요청 | 권장 |
| `/check-review` | Qodo 리뷰 코멘트 읽기 및 이슈 브리핑 | 권장 |

### 단축 사이클

Expand All @@ -94,6 +98,7 @@ Branch format: `<type>/<issue-number>-<camelCaseName>` — flow: `feat/*` → `d
Types: `feat`, `fix`, `hotfix`, `chore`, `refactor`, `doc`

Branch examples:

```
feat/42-addSearchFilter
fix/57-songCardCss
Expand All @@ -103,12 +108,60 @@ chore/61-versionBump
Commit format: `<type> : <Korean description> (#issue-number)` (space before and after colon)

Examples:

```
feat : MarqueeText 자동 스크롤 텍스트 적용 (#42)
fix : SongCard css 수정 (#57)
chore : 버전 2.3.0 (#61)
```

## 진행 중인 작업

### #307 이달의 아티스트 (포인트 투표)

코드는 완성됐다. 남은 건 배포 설정과 실데이터 QA다.

**완료**

- `apps/web/artist-vote-schema.sql` 실행 — 테이블 3종 + RLS 정책 + `artists.image_url`.
최초 버전에는 정책이 하나도 없어서 앱(anon 키)에서 모든 조회가 조용히 빈 배열로 돌아왔다.
파일 전체가 `if not exists` / `drop policy if exists`라 다시 실행해도 안전하다.
- `pnpm backfill-artists`로 `artists` 적재 (13,869명).
- 랭킹 화면: 1~3위 액자(1위 금 / 2·3위 황동) + 카드 위 우승 별(`WinStars`) + 득표 파이(1~10위).
화면 확인용 목업은 제거했다.

**남은 작업**

1. GitHub Actions repo secret 확인 — `SUPABASE_URL`, `SUPABASE_KEY`(service_role).
`finalize_artist_of_month.yml`이 확정과 사진 백필 양쪽에 쓴다. 다른 크롤링 워크플로가
이미 쓰는 값이라 등록돼 있을 가능성이 높지만, 이 워크플로에서는 처음 참조한다.
**Vercel 쪽에 추가로 넣을 환경변수는 없다.**
2. 실데이터 QA (테스트 스위트 없음)
- 아티스트 검색 자동완성 — RLS 정책이 실제로 붙었는지 확인하는 가장 빠른 길
- `artists(name_ko, image_url)` 임베드 첫 조회에서 `PGRST200`이 나면 FK 힌트 문법을 명시해야 한다
- 투표(포인트 차감) → 월간 확정 → 곡 카드 배지까지. 카카오 로그인이라 자동 검증이 어렵다
3. 아티스트 사진 — `artists.image_url`
- `finalize_artist_of_month.yml`의 마지막 단계가 채운다. 확정된 그달 1~3위 중 사진이 없는
사람만 `ARTIST_IMAGE_PODIUM=3`으로 처리한다. 사진이 쓰이는 자리가 시상대뿐이라
13,000명을 미리 채우지 않는다.
- 이름 매칭은 MusicBrainz(별칭 사전) → Deezer(사진) 순이다. 우리 DB는 TJ 표기(한글·원어)인데
음원 서비스 등록은 로마자라("방탄소년단" ↔ "BTS"), 이 다리가 없으면 사진이 있는데도 못 찾는다.
- 태연·잔나비·폴킴 3명은 이 다리를 붙이기 전에 채워져 팬 수 20~60짜리 중복 등록에서 온 사진이다.
정품으로 바꾸려면 `update public.artists set image_url = null where name in (...)` 후 다시 돌려야 한다
(`image_url is null`인 행만 대상이라 그냥 두면 갱신되지 않는다).

**월간 확정이 도는 자리**

`packages/crawling`의 `pnpm finalize-artist`다. 원래는 웹앱 API 라우트
(`/api/artist-vote/finalize`)였는데, 인증 없는 공개 URL이 순위를 지우고 다시 쓰는 구조라
공유 시크릿으로 잠가야 했다. 배치로 옮기면서 그 엔드포인트와 `ARTIST_VOTE_FINALIZE_SECRET`,
Vercel의 `SUPABASE_SERVICE_ROLE_KEY`가 모두 없어졌다. **확정 로직을 웹으로 되돌리지 말 것** —
공개 엔드포인트가 다시 생긴다.

### 태그 기능 제거 (완료 — 참고용)

검색 언어 태그 필터가 오작동해 관련 코드를 전부 걷어냈다: 프론트(`LanguageTagFilter.tsx` 등)와 `api/search`의 `song_tags` 조인, `artists.language_tag_id` 참조(추후 대시보드에서 컬럼 수동 삭제 예정)를 제거했고, `packages/crawling`의 `taggingSongs.ts`/`translationJpn.ts`는 삭제 대신 전체 주석처리했다. `tagging_song.yml`/`translation_jpn.yml`은 `schedule` 트리거만 비활성화(주석)했고 `workflow_dispatch`로 수동 실행은 여전히 가능하다. `tags`/`song_tags` 테이블 자체는 DB에 남아있지만 앱은 더 이상 참조하지 않는다.

## Self-Maintenance

이 파일(CLAUDE.md)은 프로젝트의 규칙과 구조가 변경될 때 함께 업데이트한다.
Expand Down
95 changes: 95 additions & 0 deletions apps/web/artist-vote-schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
-- 이달의 아티스트(포인트 투표) 스키마 + RLS 정책.
-- 여러 번 실행해도 안전하도록 작성했으니, 이미 테이블을 만든 환경에서도 이 파일을 그대로 다시 실행하면 된다.

create table if not exists public.artists (
id uuid primary key default gen_random_uuid(),
name text not null unique,
name_ko text,
image_url text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);

-- 이미 만들어진 환경에도 컬럼을 채워 넣는다. 랭킹 1위 액자에 쓰는 아티스트 사진 URL이다.
alter table public.artists add column if not exists image_url text;

alter table public.artists enable row level security;

create table if not exists public.artist_votes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.users(id) on delete cascade,
artist text not null references public.artists(name),
vote_month date not null, -- 매월 1일로 정규화 (예: 2026-08-01)
amount integer not null check (amount > 0),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
unique (user_id, artist, vote_month)
);

create index if not exists artist_votes_month_artist_idx on public.artist_votes (vote_month, artist);

alter table public.artist_votes enable row level security;

create table if not exists public.monthly_artist_rankings (
id uuid primary key default gen_random_uuid(),
vote_month date not null,
rank smallint not null check (rank between 1 and 30),
artist text not null references public.artists(name),
total_votes integer not null,
top_voter_user_id uuid references public.users(id) on delete set null,
top_voter_amount integer,
decided_at timestamptz not null default now(),
unique (vote_month, rank),
unique (vote_month, artist)
);

-- 최초 버전은 1~10위만 저장했다. 11~30위 구간을 화면에 쓰기 위해 제약을 넓힌다.
-- 제약 이름은 인라인 check에 postgres가 붙이는 기본 이름이다.
alter table public.monthly_artist_rankings
drop constraint if exists monthly_artist_rankings_rank_check;
alter table public.monthly_artist_rankings
add constraint monthly_artist_rankings_rank_check check (rank between 1 and 30);

alter table public.monthly_artist_rankings enable row level security;

-- RLS 정책 --------------------------------------------------------------
-- 앱의 API 라우트는 사용자 세션(anon 키)으로 Supabase에 접근하므로, 정책이 없으면
-- RLS가 모든 행을 가려 검색·랭킹이 조용히 빈 배열로 돌아온다.

-- artists: 아티스트 검색/자동완성에 필요해 누구나 읽는다.
-- 쓰기는 백필 스크립트(service_role)만 하므로 정책을 주지 않는다.
drop policy if exists "artists_select_public" on public.artists;
create policy "artists_select_public" on public.artists for select using (true);

-- monthly_artist_rankings: 확정된 월간 결과는 공개 데이터다.
-- 쓰기는 finalize 라우트(service_role) 전용이라 정책이 없다.
drop policy if exists "monthly_artist_rankings_select_public" on public.monthly_artist_rankings;
create policy "monthly_artist_rankings_select_public" on public.monthly_artist_rankings
for select using (true);

-- artist_votes: 본인 투표는 언제나 볼 수 있고, 남의 투표는 "이미 확정된 (월, 아티스트)"만 공개한다.
-- 이렇게 두지 않으면 아직 집계 전인 이번 달 실시간 득표 현황이 그대로 새어나간다.
drop policy if exists "artist_votes_select_own_or_finalized" on public.artist_votes;
create policy "artist_votes_select_own_or_finalized" on public.artist_votes
for select using (
auth.uid() = user_id
or exists (
select 1
from public.monthly_artist_rankings r
where r.vote_month = artist_votes.vote_month
and r.artist = artist_votes.artist
)
);

-- 투표 등록/수정/삭제는 본인 행만. upsert가 insert와 update 정책을 모두 타므로 둘 다 필요하다.
drop policy if exists "artist_votes_insert_own" on public.artist_votes;
create policy "artist_votes_insert_own" on public.artist_votes
for insert with check (auth.uid() = user_id);

drop policy if exists "artist_votes_update_own" on public.artist_votes;
create policy "artist_votes_update_own" on public.artist_votes
for update using (auth.uid() = user_id) with check (auth.uid() = user_id);

drop policy if exists "artist_votes_delete_own" on public.artist_votes;
create policy "artist_votes_delete_own" on public.artist_votes
for delete using (auth.uid() = user_id);
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"react-dom": "^19.0.0",
"react-intersection-observer": "^9.16.0",
"react-joyride": "3.2.0",
"recharts": "^3.10.1",
"serwist": "^9.5.7",
"sonner": "^2.0.3",
"tailwind-merge": "^3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const navigation: Navigation[] = [
{ name: '부를 곡', href: '/tosing', key: 'TOSING' },
{ name: '검색', href: '/', key: 'SEARCH' },

{ name: '인기곡', href: '/popular', key: 'POPULAR' },
{ name: '인기 차트', href: '/popular', key: 'POPULAR' },
{ name: '정보', href: '/info', key: 'INFO' },
];

Expand Down
Loading