From 32740fa1a63c95daba831a5712d3f4e271e6e20f Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 15 Mar 2026 20:34:08 +0900 Subject: [PATCH 01/12] =?UTF-8?q?fix=20:=20alias=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/constants/krToJpnArtist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/constants/krToJpnArtist.ts b/apps/web/src/constants/krToJpnArtist.ts index d71bb2e..1573101 100644 --- a/apps/web/src/constants/krToJpnArtist.ts +++ b/apps/web/src/constants/krToJpnArtist.ts @@ -2,7 +2,6 @@ export const krToJpnArtistSort = { // A-Z Ado: 'Ado', Aimer: 'Aimer', - 'ASIAN KUNG-FU GENERATION': '아시안 쿵푸 제너레이션', DECO27: 'DECO*27', Eve: 'Eve', @@ -39,6 +38,7 @@ export const krToJpnArtistSort = { // 아 아라시: '嵐', 아마자라시: 'amazarashi', + '아시안 쿵푸 제너레이션': 'ASIAN KUNG-FU GENERATION', 아이묭: 'あいみょん', 엘레가든: 'ELLEGARDEN', '오이시 마사요시': 'オーイシマサヨシ', From 8fc5445d0889589875cc22a206b94ac3b83ae3b2 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Tue, 17 Mar 2026 01:15:41 +0900 Subject: [PATCH 02/12] =?UTF-8?q?fix=20:=20crawlYoutubeVerify=202000?= =?UTF-8?q?=EA=B1=B4=20=EC=A0=9C=ED=95=9C=20=EB=B0=8F=20validateSongMatch?= =?UTF-8?q?=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - crawlYoutubeVerify: index >= 2000 시 반복문 break - validateSongMatch: JSON 파싱 try-catch 추가, isValid === true 명시적 비교, max_tokens 50으로 증가 --- .github/workflows/update_ky_youtube.yml | 1 - .github/workflows/verify_ky_youtube.yml | 2 ++ .../src/crawling/crawlYoutubeVerify.ts | 2 ++ .../crawling/src/utils/validateSongMatch.ts | 25 ++++++++++++++----- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/update_ky_youtube.yml b/.github/workflows/update_ky_youtube.yml index fa15183..83ddbe7 100644 --- a/.github/workflows/update_ky_youtube.yml +++ b/.github/workflows/update_ky_youtube.yml @@ -1,6 +1,5 @@ name: Update ky by Youtube -# 실행 일시 중지 on: schedule: - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00) diff --git a/.github/workflows/verify_ky_youtube.yml b/.github/workflows/verify_ky_youtube.yml index 5ffaa91..2d9f4fe 100644 --- a/.github/workflows/verify_ky_youtube.yml +++ b/.github/workflows/verify_ky_youtube.yml @@ -1,6 +1,8 @@ name: Verify ky by Youtube on: + schedule: + - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00) workflow_dispatch: permissions: diff --git a/packages/crawling/src/crawling/crawlYoutubeVerify.ts b/packages/crawling/src/crawling/crawlYoutubeVerify.ts index 1091ae0..d4c6ea9 100644 --- a/packages/crawling/src/crawling/crawlYoutubeVerify.ts +++ b/packages/crawling/src/crawling/crawlYoutubeVerify.ts @@ -45,6 +45,8 @@ for (const song of data) { index++; console.log('crawlYoutubeVerify : ', index); + + if (index >= 2000) break; } browser.close(); diff --git a/packages/crawling/src/utils/validateSongMatch.ts b/packages/crawling/src/utils/validateSongMatch.ts index a355ee2..b9bf10c 100644 --- a/packages/crawling/src/utils/validateSongMatch.ts +++ b/packages/crawling/src/utils/validateSongMatch.ts @@ -25,22 +25,35 @@ export const validateSongMatch = async ( messages: [ { role: 'system', - content: - 'Decide if two (title, artist) pairs refer to the same song. Allow spelling variants (spaces, en/kr, case). Return JSON: {"isValid":boolean}', + content: ` + You are a music database expert. + Decide if two (title, artist) pairs refer to the same song recording. + + Rules: + 1. Ignore additional info in parentheses like "(Original Artist Name)", "(Movie OST)", or "Remake". + 2. Allow spelling variants, spaces, case, and Language mix (KR/EN/JP). + 3. If the song title and the PERFORMING artist are the same, it is a MATCH, even if the original composer/artist is mentioned in the found title. + + Return JSON: {"isValid": boolean} + `, }, { role: 'user', - content: `"${inputTitle}"(${inputArtist}) vs "${foundTitle}"(${foundArtist})`, + content: `Pair A: "${inputTitle}" by "${inputArtist}"\nPair B: "${foundTitle}" by "${foundArtist}"`, }, ], response_format: { type: 'json_object' }, temperature: 0, - max_tokens: 20, + max_tokens: 50, }); const content = response.choices[0].message.content; if (!content) return false; - const result: { isValid: boolean } = JSON.parse(content); - return result.isValid; + try { + const result = JSON.parse(content); + return result.isValid === true; + } catch { + return false; + } }; From e71b477071ac8d8b7e103e43f1abe8f3a7b2405c Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 22 Mar 2026 00:40:49 +0900 Subject: [PATCH 03/12] =?UTF-8?q?chore=20:=20Claude=20Code=20=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=BB=A4=EB=A7=A8?= =?UTF-8?q?=EB=93=9C=20=EB=B0=8F=20=EC=84=A4=EC=A0=95=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JIRA 연동을 GitHub Issues 기반으로 전환 (spsc, commit) - /start 커맨드 추가 (이슈 생성 + 브랜치 체크아웃) - 모든 커맨드에 다음 단계 네비게이션 및 사이클 규칙 추가 - CLAUDE.md에 Workflow Commands, Self-Maintenance 섹션 추가 - Git Conventions에 이슈 번호 포함 형식 반영 - SessionStart hook으로 stale 브랜치 자동 정리 설정 Co-Authored-By: Claude Opus 4.6 --- .claude/commands/commit.md | 48 +++++++++++++++++++++++ .claude/commands/green.md | 40 +++++++++++++++++++ .claude/commands/red.md | 41 +++++++++++++++++++ .claude/commands/refactor.md | 58 +++++++++++++++++++++++++++ .claude/commands/spsc.md | 53 +++++++++++++++++++++++++ .claude/commands/start.md | 76 ++++++++++++++++++++++++++++++++++++ .claude/commands/verify.md | 59 ++++++++++++++++++++++++++++ .claude/settings.json | 16 ++++++++ .gitignore | 1 - .gitmessage.txt | 23 ----------- CLAUDE.md | 61 ++++++++++++++++++++--------- 11 files changed, 433 insertions(+), 43 deletions(-) create mode 100644 .claude/commands/commit.md create mode 100644 .claude/commands/green.md create mode 100644 .claude/commands/red.md create mode 100644 .claude/commands/refactor.md create mode 100644 .claude/commands/spsc.md create mode 100644 .claude/commands/start.md create mode 100644 .claude/commands/verify.md create mode 100644 .claude/settings.json delete mode 100644 .gitmessage.txt diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 0000000..842d954 --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,48 @@ +# /commit — Commit + +Git 커밋 규칙에 따라 커밋 메시지를 생성하고 커밋한다. + +## Steps + +1. 현재 브랜치 이름을 확인한다. + + ``` + git branch --show-current + ``` + +2. 브랜치 이름에서 이슈 번호를 추출한다. + - 규칙: `/` 이후 첫 번째 숫자 + - 예: `feat/42-addSearchFilter` → `42` + +3. `gh issue view <번호> --json title,body,labels` 로 이슈를 조회해 맥락을 파악한다. + +4. `git diff --staged` 또는 `git diff HEAD` 로 변경 내용을 확인한다. + - 변경 내용을 기반으로 커밋 메시지를 한국어로 작성한다. + - 사용자에게 확인을 구하지 않는다. + +5. 커밋 메시지 포맷: + + ``` + : 변경 내용 요약 (한국어) (#이슈번호) + ``` + + 예: `feat : 검색 필터 API 연동 (#42)` + +6. 스테이징 및 커밋 실행: + + ``` + git add -A + git commit -m " : 변경 내용 요약 (#이슈번호)" + ``` + +7. 완료 후 출력: + ``` + ✅ 커밋 완료: : 변경 내용 요약 (#이슈번호) + ``` + +## Notes + +- `git push` 는 명시적으로 요청받았을 때만 실행한다. +- `commit all` 명령 시 이 플로우를 즉시 실행한다. +- /verify 를 통과하지 않은 상태에서 커밋 요청 시 + "/verify 를 먼저 실행하세요." 를 출력하고 중단한다. diff --git a/.claude/commands/green.md b/.claude/commands/green.md new file mode 100644 index 0000000..2e9e7eb --- /dev/null +++ b/.claude/commands/green.md @@ -0,0 +1,40 @@ +# /green — Green Phase + +실패 중인 테스트를 통과시키는 최소한의 구현 코드를 작성한다. + +## Rules + +- **과도한 추상화 금지**: 지금 당장 필요한 것만 구현한다. 미래를 위한 설계는 /refactor 단계에서 한다. +- **컴포넌트**: 함수형 컴포넌트 + hooks only. 클래스 컴포넌트 금지. +- **서버 상태**: `useQuery` / `useMutation` 만 사용. 컴포넌트 내 axios 직접 호출 금지. +- **타입**: `any` 사용 금지. 명시적 interface 또는 generic 사용. +- **에러/로딩 처리**: skeleton 및 error boundary 상태를 항상 처리한다. + +## Steps + +1. 실패 중인 테스트 목록을 확인한다 (`pnpm vitest run {파일경로}`). +2. 테스트를 통과시키는 구현 코드를 작성한다. +3. `pnpm vitest run {파일경로}` 를 재실행해 모든 테스트가 통과하는지 확인한다. +4. 통과 후 아래 형식으로 출력한다. + +--- + +## ✅ Green Phase 완료 + +**통과한 테스트**: N개 +**작성/수정한 파일**: + +- `경로/파일명` — (한 줄 설명) + +## 🎓 **Mentor note**: (이번 구현에서 적용한 패턴 또는 주니어가 놓치기 쉬운 포인트) + +5. 아래 다음 단계 안내를 출력한다. + +### 👉 다음 단계 + +| 명령어 | 설명 | +| ----------- | ----------------------------- | +| `/refactor` | 코드 품질 개선 (동작 변경 X, 생략 가능) | +| `/verify` | 품질에 문제 없으면 바로 검증 (필수) | + +> `/refactor` 는 생략 가능. `/verify` → `/commit` 은 항상 실행. diff --git a/.claude/commands/red.md b/.claude/commands/red.md new file mode 100644 index 0000000..cd015e3 --- /dev/null +++ b/.claude/commands/red.md @@ -0,0 +1,41 @@ +# /red — Red Phase + +현재 작업 범위에 대해 실패하는 테스트를 먼저 작성한다. 구현 코드는 작성하지 않는다. + +## Rules + +- 테스트 파일 위치: 구현 파일과 동일 경로에 `*.test.ts(x)` 생성 +- 테스트 러너: Vitest (`import { describe, it, expect } from 'vitest'`) +- 아직 존재하지 않는 함수/컴포넌트를 import해도 된다. + 테스트가 컴파일 에러 또는 실패 상태여야 정상이다. +- 테스트 설명은 한국어로 작성한다. (`it('빈 배열이면 빈 문자열을 반환한다')`) +- `any` 타입 사용 금지. 테스트에도 명시적 타입을 사용한다. + +## Test Coverage + +아래 케이스를 커버하는 테스트를 작성한다. + +1. **Happy path**: 정상 입력 → 예상 출력 +2. **Edge case**: 빈 값, null, undefined, 경계값 +3. **Error case**: 에러 발생 시 동작 (throw, error state 등) + +## Steps + +1. /spsc 에서 정의한 완료 기준을 기반으로 테스트 케이스 목록을 먼저 나열한다. +2. 테스트 코드를 작성한다. +3. `pnpm vitest run {파일경로}` 를 실행해 테스트가 실패하는지 확인한다. +4. 실패 확인 후 아래를 출력한다. + +--- + +## 🔴 Red Phase 완료 + +**작성한 테스트 케이스**: + +- [ ] (케이스 목록) + +### 👉 다음 단계 + +| 명령어 | 설명 | +| --------- | ------------------------ | +| `/green` | 테스트를 통과시키는 구현 | diff --git a/.claude/commands/refactor.md b/.claude/commands/refactor.md new file mode 100644 index 0000000..4f52203 --- /dev/null +++ b/.claude/commands/refactor.md @@ -0,0 +1,58 @@ +# /refactor — Refactor Phase + +테스트를 통과한 코드의 품질을 개선한다. 동작은 변경하지 않는다. + +## Checklist + +아래 기준으로 코드를 검토하고, 문제가 있으면 수정한다. + +### 구조 + +- [ ] SRP: 하나의 함수/컴포넌트가 너무 많은 일을 하지 않는가? + → 로직이 많으면 custom hook으로 분리 +- [ ] DRY: 중복 코드가 있는가? + → 공통 유틸 또는 shared 컴포넌트로 추출 +- [ ] 컴포넌트가 100줄을 넘는가? + → 하위 컴포넌트로 분리 검토 + +### 타입 + +- [ ] `any` 타입이 있는가? → 제거 +- [ ] 리터럴 유니온 타입으로 좁힐 수 있는 `string`/`number`가 있는가? +- [ ] Props interface가 명시적으로 정의되어 있는가? + +### 성능 + +- [ ] `useMemo`/`useCallback`이 증명 없이 남용되고 있지 않은가? +- [ ] 불필요한 리렌더링을 유발하는 구조가 있는가? + +### 네이밍 + +- [ ] 함수/변수명이 역할을 명확히 설명하는가? +- [ ] 이벤트 핸들러는 `handle` 접두어를 사용하는가? (`handleSubmit`, `handleChange`) +- [ ] boolean은 `is`/`has`/`can` 접두어를 사용하는가? + +## Steps + +1. 위 체크리스트를 순서대로 검토한다. +2. 문제가 있는 항목만 수정한다 (diff 중심으로 출력). +3. 수정 후 `pnpm vitest run {파일경로}` 를 실행해 테스트가 여전히 통과하는지 확인한다. +4. 아래 형식으로 출력한다. + +--- + +## 🔧 Refactor 완료 + +**개선한 항목**: + +- (체크리스트 항목 + 간단한 이유) + +**변경 없는 항목**: (개선이 필요 없었던 항목) + +## 🎓 **Mentor note**: (이번 리팩토링의 핵심 포인트) + +### 👉 다음 단계 + +| 명령어 | 설명 | +| --------- | ------------------ | +| `/verify` | 머지 전 전체 검증 | diff --git a/.claude/commands/spsc.md b/.claude/commands/spsc.md new file mode 100644 index 0000000..936b63b --- /dev/null +++ b/.claude/commands/spsc.md @@ -0,0 +1,53 @@ +# /spsc — Spec & Scope + +주어진 GitHub Issue 번호를 기반으로 작업 범위를 정의한다. + +## Steps + +1. `gh issue view <번호> --json title,body,labels,assignees,milestone` 로 이슈 상세를 조회한다. + - 체크리스트(task list)가 있으면 하위 작업 목록도 함께 확인한다. + +2. 아래 형식으로 작업 범위를 출력한다. + +--- + +## 📋 Spec & Scope — #{ISSUE-NUMBER} + +**이슈 제목**: (원문) +**작업 유형**: Feature | Bugfix | Refactor | Chore + +### 구현 범위 + +- (구체적인 작업 항목 bullet) + +### 변경 예상 파일 + +- `src/features/.../` — (이유) +- `src/shared/.../` — (이유) + +### 범위 외 (이번 작업에서 하지 않는 것) + +- (명시적으로 제외할 항목) + +### 완료 기준 + +- [ ] (체크리스트 형태) + +--- + +3. 출력 후 "이 범위로 진행할까요?" 라고 확인을 구한다. + - 승인 시 작업 브랜치를 생성한다. + - 브랜치 형식: `/-` + - type 매핑: Feature → `feat`, Bugfix → `fix`, Refactor → `refactor`, Chore → `chore` + - 예: `feat/42-addSearchFilter` + - develop 브랜치에서 분기한다. + - 이후 아래 다음 단계 안내를 출력한다. + +### 👉 다음 단계 + +| 명령어 | 설명 | +| --------- | ---------------------------- | +| `/red` | 실패 테스트 먼저 작성 (TDD) | +| `/green` | 바로 구현 시작 | + +> `/red` ~ `/refactor` 사이클은 생략 가능. `/verify` → `/commit` 은 필수. diff --git a/.claude/commands/start.md b/.claude/commands/start.md new file mode 100644 index 0000000..14826fa --- /dev/null +++ b/.claude/commands/start.md @@ -0,0 +1,76 @@ +# /start — Start Task + +작업 설명을 받아 GitHub Issue를 생성하고, 해당 브랜치로 체크아웃한다. + +## Input + +`$ARGUMENTS` — 작업 설명 (자연어) + +예: `/start 검색 결과에 페이지네이션 추가` + +## Steps + +1. `$ARGUMENTS` 를 분석해 아래 항목을 결정한다. + - **작업 유형**: `feat` | `fix` | `hotfix` | `chore` | `refactor` | `doc` + - **이슈 제목**: 한국어, 간결하게 + - **이슈 본문**: 구현할 내용을 bullet으로 정리 + - **라벨**: 작업 유형에 맞는 라벨 (없으면 생략) + +2. GitHub Issue를 생성한다. + + ``` + gh issue create --title "<이슈 제목>" --body "<이슈 본문>" + ``` + + 생성된 이슈 번호를 추출한다. + +3. develop 브랜치를 최신으로 갱신한다. + + ``` + git checkout develop + git pull origin develop + ``` + +4. 작업 브랜치를 생성하고 체크아웃한다. + - 브랜치 형식: `/-` + - camelCaseName은 이슈 제목에서 핵심 키워드를 추출해 작성한다. + + ``` + git checkout -b /- + ``` + +5. 완료 후 아래 형식으로 출력한다. + +--- + +## 🚀 작업 시작 + +**이슈**: #<번호> — <제목> +**브랜치**: `/-` +**유형**: <작업 유형> + +### 👉 다음 단계 + +| 명령어 | 설명 | +| --------- | ------------------------ | +| `/spsc` | 작업 범위 정의 (권장) | +| `/red` | 테스트 먼저 작성 | +| `/green` | 바로 구현 시작 | + +### 🔄 워크플로우 사이클 + +``` +일반: /start → /spsc → /red → /green → /refactor → /verify → /commit +단축: /start → /spsc → /green → /verify → /commit +``` + +- `/red` ~ `/refactor` 사이클은 상황에 따라 생략 가능하다. +- `/verify` 와 `/commit` 은 항상 실행한다. +- 긴급 핫픽스: `/spsc` → `/green` → `/verify` → `/commit` 단축 사이클 허용. + +--- + +## Notes + +- `$ARGUMENTS` 가 비어 있으면 "작업 내용을 입력해주세요." 를 출력하고 중단한다. +- 이슈 생성 실패 시 에러 메시지를 출력하고 중단한다. diff --git a/.claude/commands/verify.md b/.claude/commands/verify.md new file mode 100644 index 0000000..5aab262 --- /dev/null +++ b/.claude/commands/verify.md @@ -0,0 +1,59 @@ +# /verify — Verify Phase + +머지 전 전체 품질을 검증한다. 실패 시 자동으로 수정하고 재실행한다. + +## Steps + +아래 커맨드를 순서대로 실행한다. +각 단계가 실패하면 즉시 수정하고 재실행한다. +사용자에게 확인을 구하지 않고 자동으로 처리한다. + +1. **Type check** + + ``` + pnpm build + ``` + + 실패 시: TypeScript 에러를 수정한다. + +2. **Lint** + + ``` + pnpm lint + ``` + + 실패 시: ESLint 에러를 수정한다. `eslint-disable` 주석으로 우회하지 않는다. + +3. **Format** + + ``` + pnpm format + ``` + +4. **Unit test** + + ``` + pnpm test + ``` + + 실패 시: 실패한 테스트를 분석하고 구현 코드를 수정한다. + 테스트를 삭제하거나 skip 처리하지 않는다. + +5. 모든 단계 통과 후 아래 형식으로 출력한다. + +--- + +## ✅ Verify 완료 + +| 단계 | 결과 | +| ---------- | ----------- | +| Type check | ✅ | +| Lint | ✅ | +| Format | ✅ | +| Test | ✅ N개 통과 | + +### 👉 다음 단계 + +| 명령어 | 설명 | +| --------- | ------------- | +| `/commit` | 커밋 실행 | diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0ef714b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "git fetch --prune 2>&1 && git branch -vv | awk '/: gone]/{print $1}' | xargs -r git branch -D 2>&1 || true", + "timeout": 30, + "statusMessage": "원격 삭제된 브랜치 정리 중..." + } + ] + } + ] + } +} diff --git a/.gitignore b/.gitignore index 02f5a07..a4dd6ba 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ yarn-error.log* .gitmessage.txt # Claude Code local settings (contains secrets) -.claude/* temp/ .vscode \ No newline at end of file diff --git a/.gitmessage.txt b/.gitmessage.txt deleted file mode 100644 index 1d65fac..0000000 --- a/.gitmessage.txt +++ /dev/null @@ -1,23 +0,0 @@ -################ -# <타입>: <제목> 의 형식으로 제목을 아래 공백줄에 작성 -# 제목은 50자 이내 / 변경사항이 "무엇"인지 명확히 작성 / 끝에 마침표 금지 -# 예) feat : 로그인 기능 추가 - -################ -# 본문(구체적인 내용)을 아랫줄에 작성 -# 여러 줄의 메시지를 작성할 땐 "-"로 구분 (한 줄은 72자 이내) - -################ -# 꼬릿말(footer)을 아랫줄에 작성 (현재 커밋과 관련된 이슈 번호 추가 등) -# 예) Close #7 - -################ -# feat : 새로운 기능 추가 -# fix : 버그 수정 -# docs : 문서 수정 -# style : 코드 의미에 영향을 주지 않는 변경사항 -# refactor : 코드 리팩토링 -# test : 테스트 코드 추가 -# chore : 빌드 부분 혹은 패키지 매니저 수정사항 -# init : 초기 세팅 작업 -################ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 4cc0f88..ad72a5a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,37 +58,60 @@ See [apps/web/CLAUDE.md](apps/web/CLAUDE.md) for full detail. Key points: - **Tailwind CSS v4** + **shadcn/ui** in `src/components/ui/` (do not modify directly) - Path alias `@/` → `src/` -## Git Workflows +## Workflow Commands -### "branch 정리해줘" +`.claude/commands/` 에 정의된 슬래시 커맨드로 작업을 진행한다. -원격에서 merge 후 삭제된 브랜치를 로컬에서도 동기화한다. +### 전체 플로우 -```bash -# 1. 원격 삭제된 브랜치 참조 정리 -git fetch --prune +``` +/start → /spsc → /red → /green → /refactor → /verify → /commit +``` -# 2. 원격에 없는 로컬 브랜치 목록 확인 -git branch -vv | grep ': gone]' +| 커맨드 | 설명 | 필수 여부 | +| ----------- | --------------------------------------- | --------- | +| `/start` | GitHub Issue 생성 + 작업 브랜치 체크아웃 | 권장 | +| `/spsc` | 이슈 기반 작업 범위 정의 | 권장 | +| `/red` | 실패 테스트 먼저 작성 (TDD) | 생략 가능 | +| `/green` | 구현 코드 작성 | 필수 | +| `/refactor` | 코드 품질 개선 (동작 변경 X) | 생략 가능 | +| `/verify` | build, lint, format, test 전체 검증 | **필수** | +| `/commit` | 커밋 메시지 생성 및 커밋 | **필수** | -# 3. 삭제 대상 브랜치 제거 (main, develop, 현재 브랜치 제외) -git branch -vv | grep ': gone]' | awk '{print $1}' | xargs -r git branch -d -``` +### 단축 사이클 -- `-d` 플래그 사용 (merge되지 않은 브랜치는 삭제 안 됨, 안전) -- 삭제 전 목록을 사용자에게 보여주고 확인 후 진행 +- 긴급 핫픽스: `/start` → `/spsc` → `/green` → `/verify` → `/commit` +- `/red` ~ `/refactor` 는 상황에 따라 생략 가능하나, `/verify` → `/commit` 은 항상 실행한다. ## Git Conventions -Branch format: `/` — flow: `feat/*` → `develop` → `main` - -Commit format: ` : ` (space before and after colon) +Branch format: `/-` — flow: `feat/*` → `develop` → `main` Types: `feat`, `fix`, `hotfix`, `chore`, `refactor`, `doc` +Branch examples: +``` +feat/42-addSearchFilter +fix/57-songCardCss +chore/61-versionBump +``` + +Commit format: ` : (#issue-number)` (space before and after colon) + Examples: ``` -feat : MarqueeText 자동 스크롤 텍스트 적용 -fix : SongCard css 수정 -chore : 버전 2.3.0 +feat : MarqueeText 자동 스크롤 텍스트 적용 (#42) +fix : SongCard css 수정 (#57) +chore : 버전 2.3.0 (#61) ``` + +## Self-Maintenance + +이 파일(CLAUDE.md)은 프로젝트의 규칙과 구조가 변경될 때 함께 업데이트한다. +별도 요청 없이도 아래 항목에 해당하는 변경이 발생하면 자동으로 반영한다. + +- 커맨드(`.claude/commands/`) 추가·수정·삭제 시 → **Workflow Commands** 섹션 반영 +- 브랜치·커밋 규칙 변경 시 → **Git Conventions** 섹션 반영 +- 패키지 추가·삭제·구조 변경 시 → **Monorepo Structure** 섹션 반영 +- 기술 스택·아키텍처 변경 시 → **Web App Architecture** 섹션 반영 +- 빌드·린트·포맷 명령어 변경 시 → **Commands** 섹션 반영 From 76a78fb4092ed6bb7ad56973291b3be5e4178935 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 22 Mar 2026 01:01:47 +0900 Subject: [PATCH 04/12] =?UTF-8?q?chore=20:=20Claude=20Code=20skills=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .claude/skills/find-skills/SKILL.md | 143 ++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 .claude/skills/find-skills/SKILL.md diff --git a/.claude/skills/find-skills/SKILL.md b/.claude/skills/find-skills/SKILL.md new file mode 100644 index 0000000..f92bce2 --- /dev/null +++ b/.claude/skills/find-skills/SKILL.md @@ -0,0 +1,143 @@ +--- +name: find-skills +description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. +--- + +# Find Skills + +This skill helps you discover and install skills from the open agent skills ecosystem. + +## When to Use This Skill + +Use this skill when the user: + +- Asks "how do I do X" where X might be a common task with an existing skill +- Says "find a skill for X" or "is there a skill for X" +- Asks "can you do X" where X is a specialized capability +- Expresses interest in extending agent capabilities +- Wants to search for tools, templates, or workflows +- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.) + +## What is the Skills CLI? + +The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools. + +**Key commands:** + +- `npx skills find [query]` - Search for skills interactively or by keyword +- `npx skills add ` - Install a skill from GitHub or other sources +- `npx skills check` - Check for skill updates +- `npx skills update` - Update all installed skills + +**Browse skills at:** https://skills.sh/ + +## How to Help Users Find Skills + +### Step 1: Understand What They Need + +When a user asks for help with something, identify: + +1. The domain (e.g., React, testing, design, deployment) +2. The specific task (e.g., writing tests, creating animations, reviewing PRs) +3. Whether this is a common enough task that a skill likely exists + +### Step 2: Check the Leaderboard First + +Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options. + +For example, top skills for web development include: + +- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each) +- `anthropics/skills` — Frontend design, document processing (100K+ installs) + +### Step 3: Search for Skills + +If the leaderboard doesn't cover the user's need, run the find command: + +```bash +npx skills find [query] +``` + +For example: + +- User asks "how do I make my React app faster?" → `npx skills find react performance` +- User asks "can you help me with PR reviews?" → `npx skills find pr review` +- User asks "I need to create a changelog" → `npx skills find changelog` + +### Step 4: Verify Quality Before Recommending + +**Do not recommend a skill based solely on search results.** Always verify: + +1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100. +2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors. +3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism. + +### Step 5: Present Options to the User + +When you find relevant skills, present them to the user with: + +1. The skill name and what it does +2. The install count and source +3. The install command they can run +4. A link to learn more at skills.sh + +Example response: + +``` +I found a skill that might help! The "react-best-practices" skill provides +React and Next.js performance optimization guidelines from Vercel Engineering. +(185K installs) + +To install it: +npx skills add vercel-labs/agent-skills@react-best-practices + +Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices +``` + +### Step 6: Offer to Install + +If the user wants to proceed, you can install the skill for them: + +```bash +npx skills add -g -y +``` + +The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts. + +## Common Skill Categories + +When searching, consider these common categories: + +| Category | Example Queries | +| --------------- | ---------------------------------------- | +| Web Development | react, nextjs, typescript, css, tailwind | +| Testing | testing, jest, playwright, e2e | +| DevOps | deploy, docker, kubernetes, ci-cd | +| Documentation | docs, readme, changelog, api-docs | +| Code Quality | review, lint, refactor, best-practices | +| Design | ui, ux, design-system, accessibility | +| Productivity | workflow, automation, git | + +## Tips for Effective Searches + +1. **Use specific keywords**: "react testing" is better than just "testing" +2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd" +3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills` + +## When No Skills Are Found + +If no relevant skills exist: + +1. Acknowledge that no existing skill was found +2. Offer to help with the task directly using your general capabilities +3. Suggest the user could create their own skill with `npx skills init` + +Example: + +``` +I searched for skills related to "xyz" but didn't find any matches. +I can still help you with this task directly! Would you like me to proceed? + +If this is something you do often, you could create your own skill: +npx skills init my-xyz-skill +``` From 793c48185472155edbb4da046ffcb16be153281a Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 22 Mar 2026 01:07:50 +0900 Subject: [PATCH 05/12] =?UTF-8?q?chore=20:=20PR=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20Qodo=20AI=20=EB=A6=AC=EB=B7=B0=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .claude/commands/pr.md | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 .claude/commands/pr.md diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md new file mode 100644 index 0000000..2dd05ec --- /dev/null +++ b/.claude/commands/pr.md @@ -0,0 +1,83 @@ +# /pr — Pull Request + +현재 브랜치의 PR을 생성하고 Qodo AI 코드 리뷰를 요청한다. + +## Steps + +1. 현재 브랜치 이름을 확인한다. + + ``` + git branch --show-current + ``` + +2. 브랜치 이름에서 작업 유형과 이슈 번호를 추출한다. + - 규칙: `/-` + - 예: `feat/42-addSearchFilter` → type=`feat`, issue=`42` + +3. `gh issue view <번호> --json title,body,labels` 로 이슈 정보를 조회한다. + +4. `git log develop..HEAD --oneline` 과 `git diff develop...HEAD` 로 변경 내용을 파악한다. + +5. `.github/pull_request_template.md` 양식에 맞춰 PR 본문을 작성한다. + + ```markdown + ## 📌 PR 제목 + + ### [Type] : 작업 내용 요약 + + ## 📌 변경 사항 + + - 변경 1 + - 변경 2 + + ## 💬 추가 참고 사항 + + - 관련 이슈: #<번호> + ``` + + - PR 제목: `[Type] : 작업 내용 요약 (#이슈번호)` (한국어) + - Type은 브랜치의 작업 유형을 대문자로 (feat → Feat, fix → Fix 등) + - 변경 사항은 커밋 내역과 diff를 기반으로 bullet 정리 + +6. PR을 생성한다. + + ``` + gh pr create --base develop --title "" --body "" + ``` + + 생성된 PR 번호를 추출한다. + +7. Qodo AI 코드 리뷰를 위해 댓글을 순서대로 작성한다. + + ``` + gh pr comment --body "/describe" + gh pr comment --body "/review" + gh pr comment --body "/improve" + ``` + +8. 완료 후 아래 형식으로 출력한다. + +--- + +## 📋 PR 생성 완료 + +**PR**: # +**Base**: `develop` ← `<현재 브랜치>` +**이슈**: #<이슈번호> + +### 🤖 Qodo AI 리뷰 요청 + +- [x] `/describe` — PR 설명 자동 생성 +- [x] `/review` — 코드 리뷰 +- [x] `/improve` — 개선 제안 + +**PR 링크**: + +--- + +## Notes + +- PR의 base 브랜치는 `develop` 이다. +- 이슈 번호를 추출할 수 없는 경우 이슈 연결 없이 PR을 생성한다. +- PR 생성 실패 시 에러 메시지를 출력하고 중단한다. +- Qodo AI 댓글 실패 시 경고를 출력하되 PR 생성 자체는 성공으로 처리한다. From 5255535480c39c1739a185c2c13ff846e888f72d Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Sun, 22 Mar 2026 01:12:33 +0900 Subject: [PATCH 06/12] =?UTF-8?q?chore=20:=20PR=20=EC=BB=A4=EB=A7=A8?= =?UTF-8?q?=EB=93=9C=20close=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EB=B0=8F=20M?= =?UTF-8?q?SYS=20=EA=B2=BD=EB=A1=9C=20=EB=B3=80=ED=99=98=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80=20(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .claude/commands/pr.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 2dd05ec..29e18aa 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -32,7 +32,7 @@ ## 💬 추가 참고 사항 - - 관련 이슈: #<번호> + - close #<번호> ``` - PR 제목: `[Type] : 작업 내용 요약 (#이슈번호)` (한국어) @@ -49,10 +49,13 @@ 7. Qodo AI 코드 리뷰를 위해 댓글을 순서대로 작성한다. + Windows Git Bash에서 `/`로 시작하는 문자열이 경로로 변환되는 것을 방지하기 위해 + `MSYS_NO_PATHCONV=1` 환경변수를 설정한다. + ``` - gh pr comment --body "/describe" - gh pr comment --body "/review" - gh pr comment --body "/improve" + MSYS_NO_PATHCONV=1 gh pr comment --body "/describe" + MSYS_NO_PATHCONV=1 gh pr comment --body "/review" + MSYS_NO_PATHCONV=1 gh pr comment --body "/improve" ``` 8. 완료 후 아래 형식으로 출력한다. From b5871064175296dd8036c5adc52441e252ba8fc5 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:33 +0900 Subject: [PATCH 07/12] =?UTF-8?q?chore=20:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=EC=84=B1=20=EB=B0=8F=20=ED=8E=B8=EC=9D=98=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /pr 커맨드: develop/main 브랜치에서 실행 시 차단 로직 추가 - /start 커맨드: 인자 없을 때 git diff 기반 자동 판단 - 워크플로우 사이클에 /pr 단계 추가 Co-Authored-By: Claude Opus 4.6 --- .claude/commands/pr.md | 8 ++++++++ .claude/commands/start.md | 20 ++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 29e18aa..dd732e3 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -10,6 +10,14 @@ git branch --show-current ``` + **브랜치가 `develop` 또는 `main` 인 경우 즉시 중단한다.** + 아래 메시지를 출력하고 더 이상 진행하지 않는다. + + ``` + ⛔ `develop` / `main` 브랜치에서는 PR을 생성할 수 없습니다. + `/start <작업 설명>` 으로 이슈를 생성하고 작업 브랜치를 만들어 주세요. + ``` + 2. 브랜치 이름에서 작업 유형과 이슈 번호를 추출한다. - 규칙: `/-` - 예: `feat/42-addSearchFilter` → type=`feat`, issue=`42` diff --git a/.claude/commands/start.md b/.claude/commands/start.md index 14826fa..038c9f9 100644 --- a/.claude/commands/start.md +++ b/.claude/commands/start.md @@ -11,6 +11,10 @@ ## Steps 1. `$ARGUMENTS` 를 분석해 아래 항목을 결정한다. + + **`$ARGUMENTS` 가 비어 있는 경우:** + `git diff` 와 `git status` 로 현재 변경 사항을 파악하고, + 변경 내용을 기반으로 작업 유형, 이슈 제목, 이슈 본문을 자동으로 결정한다. - **작업 유형**: `feat` | `fix` | `hotfix` | `chore` | `refactor` | `doc` - **이슈 제목**: 한국어, 간결하게 - **이슈 본문**: 구현할 내용을 bullet으로 정리 @@ -51,17 +55,17 @@ ### 👉 다음 단계 -| 명령어 | 설명 | -| --------- | ------------------------ | -| `/spsc` | 작업 범위 정의 (권장) | -| `/red` | 테스트 먼저 작성 | -| `/green` | 바로 구현 시작 | +| 명령어 | 설명 | +| -------- | --------------------- | +| `/spsc` | 작업 범위 정의 (권장) | +| `/red` | 테스트 먼저 작성 | +| `/green` | 바로 구현 시작 | ### 🔄 워크플로우 사이클 ``` -일반: /start → /spsc → /red → /green → /refactor → /verify → /commit -단축: /start → /spsc → /green → /verify → /commit +일반: /start → /spsc → /red → /green → /refactor → /verify → /commit → /pr +단축: /start → /spsc → /green → /verify → /commit → /pr ``` - `/red` ~ `/refactor` 사이클은 상황에 따라 생략 가능하다. @@ -72,5 +76,5 @@ ## Notes -- `$ARGUMENTS` 가 비어 있으면 "작업 내용을 입력해주세요." 를 출력하고 중단한다. +- `$ARGUMENTS` 가 비어 있으면 현재 변경 사항(`git diff`, `git status`)을 분석해 자동으로 판단한다. - 이슈 생성 실패 시 에러 메시지를 출력하고 중단한다. From 4927aec089ada03251470b5a982dcde2dc2b3284 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:39 +0900 Subject: [PATCH 08/12] =?UTF-8?q?chore=20:=20crawling=20=EC=9D=BC=EB=B3=B8?= =?UTF-8?q?=EC=96=B4=20=EB=B2=88=EC=97=AD=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20ESLint=20v9=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - postTransDictionary.ts 및 transList.txt 삭제 - TransDictionary 타입, 관련 DB 함수, 로그 함수 제거 - trans 스크립트 제거, CLAUDE.md 반영 - ESLint v9 flat config 추가 및 lint 명령어 수정 Co-Authored-By: Claude Opus 4.6 --- packages/crawling/CLAUDE.md | 28 +++----- packages/crawling/eslint.config.mjs | 4 ++ packages/crawling/package.json | 3 +- packages/crawling/src/assets/transList.txt | 29 -------- .../crawling/src/crawling/crawlRecentTJ.ts | 2 - .../src/crawling/replaceSupabaseFailed.ts | 1 + packages/crawling/src/findKYByOpen.ts | 1 - packages/crawling/src/postTransDictionary.ts | 71 ------------------- packages/crawling/src/supabase/getDB.ts | 29 +------- packages/crawling/src/supabase/postDB.ts | 38 +--------- packages/crawling/src/types.ts | 7 -- packages/crawling/src/updateJpnSongs.ts | 1 - packages/crawling/src/utils/logData.ts | 20 ------ 13 files changed, 16 insertions(+), 218 deletions(-) create mode 100644 packages/crawling/eslint.config.mjs delete mode 100644 packages/crawling/src/assets/transList.txt delete mode 100644 packages/crawling/src/postTransDictionary.ts diff --git a/packages/crawling/CLAUDE.md b/packages/crawling/CLAUDE.md index 735f568..7136ffc 100644 --- a/packages/crawling/CLAUDE.md +++ b/packages/crawling/CLAUDE.md @@ -13,7 +13,6 @@ pnpm ky-open # Open API(금영)로 KY 번호 수집 pnpm ky-youtube # YouTube 크롤링으로 KY 번호 수집 + AI 검증 pnpm ky-verify # 기존 KY 번호의 실제 존재 여부 재검증 (체크포인트 지원) pnpm ky-update # ky-youtube + ky-verify 병렬 실행 -pnpm trans # 일본어 아티스트명 → 한국어 번역 후 DB 저장 pnpm test # vitest 실행 pnpm lint # ESLint ``` @@ -74,24 +73,14 @@ findKYByOpen.ts └─ 제목 + 아티스트 문자열 비교로 KY 번호 매칭 ``` -**일본어 번역** - -``` -postTransDictionary.ts - └─ getSongsJpnDB() # 일본어 포함된 곡 필터링 - └─ transChatGPT() # GPT-4-turbo로 아티스트명 번역 - └─ postTransDictionariesDB() # trans_dictionaries 테이블에 저장 -``` - ### 핵심 패턴: 진행 상태 저장 (체크포인트) 장시간 실행되는 스크립트가 중단됐을 때 재시작하면 처음부터 다시 하지 않도록, `src/assets/`에 텍스트 파일로 진행 상태를 기록한다. -| 파일 | 용도 | -| ----------------------------------------- | ---------------------------------- | -| `src/assets/transList.txt` | 이미 번역 시도한 일본어 아티스트명 | -| `src/assets/crawlKYValidList.txt` | 검증 완료된 (제목-아티스트) 쌍 | -| `src/assets/crawlKYYoutubeFailedList.txt` | YouTube 크롤링 실패 목록 | +| 파일 | 용도 | +| ----------------------------------------- | ------------------------------ | +| `src/assets/crawlKYValidList.txt` | 검증 완료된 (제목-아티스트) 쌍 | +| `src/assets/crawlKYYoutubeFailedList.txt` | YouTube 크롤링 실패 목록 | `logData.ts`의 `save*` / `load*` 함수로 관리. 스크립트 시작 시 로드해 `Set`으로 변환 후 O(1) 검색으로 스킵 처리. @@ -101,11 +90,10 @@ postTransDictionary.ts ### Supabase 테이블 -| 테이블 | 용도 | -| -------------------- | -------------------------------- | -| `songs` | 메인 곡 데이터 (TJ/KY 번호 포함) | -| `invalid_ky_songs` | KY 번호 수집 실패 목록 | -| `trans_dictionaries` | 일본어 → 한국어 번역 사전 | +| 테이블 | 용도 | +| ------------------ | -------------------------------- | +| `songs` | 메인 곡 데이터 (TJ/KY 번호 포함) | +| `invalid_ky_songs` | KY 번호 수집 실패 목록 | ### AI 유틸 diff --git a/packages/crawling/eslint.config.mjs b/packages/crawling/eslint.config.mjs new file mode 100644 index 0000000..dc943dc --- /dev/null +++ b/packages/crawling/eslint.config.mjs @@ -0,0 +1,4 @@ +import { config } from '@repo/eslint-config/base'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/crawling/package.json b/packages/crawling/package.json index cfa6209..e81f2ff 100644 --- a/packages/crawling/package.json +++ b/packages/crawling/package.json @@ -11,9 +11,8 @@ "ky-youtube": "tsx src/crawling/crawlYoutube.ts", "ky-verify": "tsx src/crawling/crawlYoutubeVerify.ts", "ky-update": "pnpm run ky-youtube & pnpm run ky-verify", - "trans": "tsx src/postTransDictionary.ts", "recent-tj": "tsx src/crawling/crawlRecentTJ.ts", - "lint": "eslint . --ext .ts,.js", + "lint": "eslint .", "test": "vitest run", "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, diff --git a/packages/crawling/src/assets/transList.txt b/packages/crawling/src/assets/transList.txt deleted file mode 100644 index 486a91c..0000000 --- a/packages/crawling/src/assets/transList.txt +++ /dev/null @@ -1,29 +0,0 @@ -鳥羽一郎 -欅坂46 -张芸京 -中島みゆき -双笙 -藤咲かりん(miko) -吉田羊 -紫今 -友成空 -文夫 -米津玄師 -林俊杰,林俊峰 -Ayase,R-指定 -张朕,婉枫 -ヨルシカ -矢口真里とストローハット -福田こうへい -彩菜 -桑田佳祐 -嵐 -福原遥 -ラックライフ -いきものがかり -ユンナ -梁咏琪 -ポルノグラフィティ -氷川きよし -薛明媛,朱贺 -陈泫孝(大泫) diff --git a/packages/crawling/src/crawling/crawlRecentTJ.ts b/packages/crawling/src/crawling/crawlRecentTJ.ts index 9cfbf65..8238714 100644 --- a/packages/crawling/src/crawling/crawlRecentTJ.ts +++ b/packages/crawling/src/crawling/crawlRecentTJ.ts @@ -60,6 +60,4 @@ console.log('실패 개수 : ', result.failed.length); console.log('성공 데이터 : ', result.success); console.log('실패 데이터 : ', result.failed); - - await browser.close(); diff --git a/packages/crawling/src/crawling/replaceSupabaseFailed.ts b/packages/crawling/src/crawling/replaceSupabaseFailed.ts index 3d9a163..cdf2224 100644 --- a/packages/crawling/src/crawling/replaceSupabaseFailed.ts +++ b/packages/crawling/src/crawling/replaceSupabaseFailed.ts @@ -1,6 +1,7 @@ import { getSongsKyNullDB } from '@/supabase/getDB'; import { postInvalidKYSongsDB } from '@/supabase/postDB'; import { Song } from '@/types'; + // import { loadCrawlYoutubeFailedKYSongs } from '@/utils/logData'; const data: Song[] = await getSongsKyNullDB(); diff --git a/packages/crawling/src/findKYByOpen.ts b/packages/crawling/src/findKYByOpen.ts index 5177f39..aaadb8c 100644 --- a/packages/crawling/src/findKYByOpen.ts +++ b/packages/crawling/src/findKYByOpen.ts @@ -76,4 +76,3 @@ console.log(` - 성공: ${resultsLog.success.length}곡 - 실패: ${resultsLog.failed.length}곡 `); - diff --git a/packages/crawling/src/postTransDictionary.ts b/packages/crawling/src/postTransDictionary.ts deleted file mode 100644 index 7bcb59e..0000000 --- a/packages/crawling/src/postTransDictionary.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { sleep } from 'openai/core'; - -import { getSongsJpnDB, getTransDictionariesDBByOriginal } from '@/supabase/getDB'; -import { postTransDictionariesDB } from '@/supabase/postDB'; -import { TransDictionary, TransSong } from '@/types'; -// import { loadDictionariesLog, saveDictionariesLog } from '@/utils/logData'; -import { transChatGPT } from '@/utils/transChatGPT'; - -const data: TransSong[] = await getSongsJpnDB(); -console.log('data to translate : ', data.length); - -// 만약 null로 반환된다면 해당 id와 함께 배열에 담가두다가 끝났을 때 error.txt에 저장 - -const unknownData: { item: TransSong; error: any }[] = []; - -const transData: TransDictionary[] = []; - -const refreshData = async () => { - console.log('refreshData'); - - await postTransDictionariesDB(transData); - // for (const song of transData) { - // saveDictionariesLog(song.original_japanese); - // } - - transData.length = 0; - unknownData.length = 0; -}; - -let count = 0; - -// const tryLogs = loadDictionariesLog(); - -for (const song of data) { - if (count >= 10) { - await refreshData(); - count = 0; - } - console.log('count : ', count++); - await sleep(150); // 0.15초(150ms) 대기 - - // if (tryLogs.has(song.artist)) { - // continue; - // } - - const dupArtistTrans = await getTransDictionariesDBByOriginal(song.artist); - - if (dupArtistTrans) { - // saveDictionariesLog(song.artist); - continue; - } - - if (song.isArtistJp) { - const artistTrans = await transChatGPT(song.artist); - if (!artistTrans || artistTrans.length === 0) { - unknownData.push({ item: song, error: 'transChatGPT failed' }); - transData.push({ - original_japanese: song.artist, - translated_korean: null, - }); - } else { - console.log(song.artist, artistTrans); - transData.push({ - original_japanese: song.artist, - translated_korean: artistTrans, - }); - } - } -} - -refreshData(); diff --git a/packages/crawling/src/supabase/getDB.ts b/packages/crawling/src/supabase/getDB.ts index abeb58e..81c7a3c 100644 --- a/packages/crawling/src/supabase/getDB.ts +++ b/packages/crawling/src/supabase/getDB.ts @@ -1,4 +1,4 @@ -import { TransDictionary, TransSong } from '@/types'; +import { TransSong } from '@/types'; import { containsJapanese } from '@/utils/parseString'; import { getClient } from './getClient'; @@ -63,33 +63,6 @@ export async function getSongsKyNotNullDB(max: number = 50000) { return data; } -export async function getTransDictionariesDB(): Promise { - const supabase = getClient(); - - // artist 정렬 - const { data, error } = await supabase.from('trans_dictionaries').select('*'); - - if (error) throw error; - - return data; -} -export async function getTransDictionariesDBByOriginal( - original: string, -): Promise { - const supabase = getClient(); - - // artist 정렬 - const { data, error } = await supabase - .from('trans_dictionaries') - .select('*') - .eq('original_japanese', original) - .limit(1); - - if (error) throw error; - - return data[0] ?? null; -} - export async function getInvalidKYSongsDB(): Promise< { id: string; title: string; artist: string }[] > { diff --git a/packages/crawling/src/supabase/postDB.ts b/packages/crawling/src/supabase/postDB.ts index 72e5cc2..d53f82d 100644 --- a/packages/crawling/src/supabase/postDB.ts +++ b/packages/crawling/src/supabase/postDB.ts @@ -1,4 +1,4 @@ -import { LogData, Song, TransDictionary } from '@/types'; +import { LogData, Song } from '@/types'; import { getClient } from './getClient'; @@ -36,42 +36,6 @@ export async function postSongsDB(songs: Song[] | Song) { return results; } -export async function postTransDictionariesDB(dictionaries: TransDictionary[]) { - const supabase = getClient(); - - const results: LogData = { - success: [] as TransDictionary[], - failed: [] as { item: TransDictionary; error: any }[], - }; - - // 각 곡을 개별적으로 처리 - for (const item of dictionaries) { - try { - const { original_japanese, translated_korean } = item; - const { data, error } = await supabase - .from('trans_dictionaries') - .insert([{ original_japanese, translated_korean }]) - .select(); - - if (error) { - results.failed.push({ item, error }); - } else { - results.success.push(item); - } - } catch (error) { - results.failed.push({ item, error }); - } - } - - console.log(` - 총 ${dictionaries.length} 데이터 중: - - 성공: ${results.success.length}개 - - 실패: ${results.failed.length}개 - `); - - return results; -} - export async function postVerifyKySongsDB(song: Song) { const supabase = getClient(); diff --git a/packages/crawling/src/types.ts b/packages/crawling/src/types.ts index d403d15..1534694 100644 --- a/packages/crawling/src/types.ts +++ b/packages/crawling/src/types.ts @@ -21,13 +21,6 @@ export interface TransSong extends Song { type?: 'title' | 'artist'; } -export interface TransDictionary { - id?: string; - original_japanese: string; - translated_korean: string | null; - created_at?: string; -} - export interface LogData { success: T[]; failed: { item: T; error: any }[]; diff --git a/packages/crawling/src/updateJpnSongs.ts b/packages/crawling/src/updateJpnSongs.ts index a43a5de..8d9c222 100644 --- a/packages/crawling/src/updateJpnSongs.ts +++ b/packages/crawling/src/updateJpnSongs.ts @@ -53,4 +53,3 @@ for (const song of transData) { updateSongsJpnDB(song); } } - diff --git a/packages/crawling/src/utils/logData.ts b/packages/crawling/src/utils/logData.ts index e326647..b26a40e 100644 --- a/packages/crawling/src/utils/logData.ts +++ b/packages/crawling/src/utils/logData.ts @@ -1,26 +1,6 @@ import fs from 'fs'; import path from 'path'; -export function saveDictionariesLog(japanese: string) { - const logPath = path.join('src', 'assets', 'transList.txt'); - const logDir = path.dirname(logPath); - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } - fs.appendFileSync(logPath, `${japanese}\n`, 'utf-8'); -} - -export function loadDictionariesLog(): Set { - const logPath = path.join('src', 'assets', 'transList.txt'); - if (!fs.existsSync(logPath)) return new Set(); - const lines = fs - .readFileSync(logPath, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(Boolean); - return new Set(lines); -} - export function loadCrawlYoutubeFailedKYSongs(): Set { const logPath = path.join('src', 'assets', 'crawlKYYoutubeFailedList.txt'); if (!fs.existsSync(logPath)) return new Set(); From 1cf103c2572383a953b530d069144f757bf3fb38 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:55 +0900 Subject: [PATCH 09/12] =?UTF-8?q?chore=20:=20apps/web=20CLAUDE.md=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- apps/web/CLAUDE.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md index e39ab9d..f784765 100644 --- a/apps/web/CLAUDE.md +++ b/apps/web/CLAUDE.md @@ -25,6 +25,7 @@ No test suite is configured. ### Monorepo Structure This is a pnpm workspace monorepo. The web app lives at `apps/web/`. Key workspace packages: + - `@repo/open-api` — wrapper around the external karaoke open API (used in API routes) - `@repo/query` — shared TanStack Query setup - `@repo/eslint-config`, `@repo/format-config` — shared tooling configs @@ -49,6 +50,7 @@ This is a pnpm workspace monorepo. The web app lives at `apps/web/`. Key workspa ### Supabase Client Variants Three different Supabase clients for different contexts: + - `src/lib/supabase/client.ts` — browser client (`createBrowserClient`), uses `NEXT_PUBLIC_` env vars - `src/lib/supabase/server.ts` — server/route handler client (`createServerClient`) - `src/lib/supabase/api.ts` — legacy API routes client (Next.js Pages-style `req/res`) @@ -87,6 +89,7 @@ Song searches go through `GET /api/open_songs/[type]/[param]` which proxies to ` ## Environment Variables Required in `.env` / `.env.development.local`: + - `NEXT_PUBLIC_SUPABASE_URL` - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - `SUPABASE_URL` (server-only, used in legacy API client) @@ -99,12 +102,12 @@ Required in `.env` / `.env.development.local`: Format: `/` -| type | usage | -|------|-------| -| `feat` | new feature | -| `fix` | bug fix | -| `hotfix` | urgent fix | -| `chore` | maintenance, docs, config | +| type | usage | +| --------- | ------------------------------ | +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent fix | +| `chore` | maintenance, docs, config | | `release` | release (e.g. `release/2.1.0`) | The part after the slash uses camelCase (e.g. `feat/scrollText`, `feat/FooterNavbar`, `fix/loginAuth`). @@ -115,16 +118,17 @@ Branch flow: `feat/*` → `develop` → `main` Format: ` : ` — one space before and after the colon. -| type | usage | -|------|-------| -| `feat` | new feature | -| `fix` | bug fix | -| `hotfix` | urgent bug fix | -| `chore` | version bump, config, format, cleanup | -| `refactor` | refactoring | -| `doc` | documentation | +| type | usage | +| ---------- | ------------------------------------- | +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent bug fix | +| `chore` | version bump, config, format, cleanup | +| `refactor` | refactoring | +| `doc` | documentation | Examples: + ``` feat : MarqueeText 자동 스크롤 텍스트 적용 fix : SongCard css 수정 From 1cf69042d3b128278c719d3ea420d9b536b1cf7c Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:41:54 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore=20:=20gitignore=EC=97=90=20settings?= =?UTF-8?q?.local.json=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20sitemap=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 +++- apps/web/public/sitemap-0.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4dd6ba..2d74ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ yarn-error.log* # Claude Code local settings (contains secrets) temp/ -.vscode \ No newline at end of file +.vscode + +settings.local.json \ No newline at end of file diff --git a/apps/web/public/sitemap-0.xml b/apps/web/public/sitemap-0.xml index 99ebb14..73fd5f7 100644 --- a/apps/web/public/sitemap-0.xml +++ b/apps/web/public/sitemap-0.xml @@ -1,4 +1,4 @@ -https://www.singcode.kr2026-03-02T07:59:31.054Zweekly0.7 +https://www.singcode.kr2026-03-25T14:32:28.966Zweekly0.7 \ No newline at end of file From 940a85431cb2dada60f803870a91d89d4dea3f68 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Thu, 26 Mar 2026 01:32:07 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor=20:=20thumb-up=20API=EB=A5=BC=20?= =?UTF-8?q?thumb=5Flogs=20=EA=B0=9C=EB=B3=84=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#1?= =?UTF-8?q?69)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- apps/web/src/app/api/search/route.ts | 14 +-- apps/web/src/app/api/songs/thumb-up/route.ts | 101 +++++++++++------- .../src/app/popular/PopularRankingList.tsx | 2 +- apps/web/src/lib/api/thumbSong.ts | 4 +- apps/web/src/queries/songThumbQuery.ts | 6 +- apps/web/src/types/song.ts | 2 +- 6 files changed, 74 insertions(+), 55 deletions(-) diff --git a/apps/web/src/app/api/search/route.ts b/apps/web/src/app/api/search/route.ts index ee9a2e9..7b86bd3 100644 --- a/apps/web/src/app/api/search/route.ts +++ b/apps/web/src/app/api/search/route.ts @@ -6,9 +6,9 @@ import { SearchSong, Song } from '@/types/song'; import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; interface DBSong extends Song { - total_stats: { - total_thumb: number; - }; + thumb_logs: { + thumb_count: number; + }[] | null; tosings: { user_id: string; }[]; @@ -49,7 +49,7 @@ export async function GET(request: Request): Promise sum + log.thumb_count, 0) ?? 0, })); return NextResponse.json({ @@ -99,7 +99,7 @@ export async function GET(request: Request): Promise tosing.user_id === userId) ?? false, isLike: song.like_activities?.some(like => like.user_id === userId) ?? false, isSave: song.save_activities?.some(save => save.user_id === userId) ?? false, - thumb: song.total_stats?.total_thumb ?? 0, + thumb: song.thumb_logs?.reduce((sum, log) => sum + log.thumb_count, 0) ?? 0, })); return NextResponse.json({ diff --git a/apps/web/src/app/api/songs/thumb-up/route.ts b/apps/web/src/app/api/songs/thumb-up/route.ts index b4f54fb..d8a2061 100644 --- a/apps/web/src/app/api/songs/thumb-up/route.ts +++ b/apps/web/src/app/api/songs/thumb-up/route.ts @@ -3,29 +3,55 @@ import { NextResponse } from 'next/server'; import createClient from '@/lib/supabase/server'; import { ApiResponse } from '@/types/apiRoute'; import { Song } from '@/types/song'; +import { getAuthenticatedUser } from '@/utils/getAuthenticatedUser'; interface ThumbUpSong extends Song { - total_thumb: number; + thumb_count: number; } export async function GET(): Promise>> { try { const supabase = await createClient(); - // like_activities에서 song_id 목록을 가져옴 - const { data, error: thumbUpError } = await supabase - .from('total_stats') - .select( - `total_thumb, - ...songs ( - * - ) - `, - ) - .order('total_thumb', { ascending: false }) - .limit(50); - - if (thumbUpError) throw thumbUpError; + // 1) thumb_logs 전체 조회 + const { data: thumbData, error: thumbError } = await supabase + .from('thumb_logs') + .select('song_id, thumb_count'); + + if (thumbError) throw thumbError; + if (!thumbData || thumbData.length === 0) { + return NextResponse.json({ success: true, data: [] }); + } + + // 2) 앱에서 song_id별 합계 집계 + const thumbMap = new Map(); + for (const row of thumbData) { + thumbMap.set(row.song_id, (thumbMap.get(row.song_id) ?? 0) + row.thumb_count); + } + + // 3) 상위 50개 song_id 추출 + const sorted = [...thumbMap.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 50); + + const songIds = sorted.map(([songId]) => songId); + + // 4) 해당 song 상세 정보 조회 + const { data: songs, error: songError } = await supabase + .from('songs') + .select('*') + .in('id', songIds); + + if (songError) throw songError; + + // 5) 병합 후 thumb_count 내림차순 정렬 + const songMap = new Map(songs?.map(song => [song.id, song])); + const data = sorted + .filter(([songId]) => songMap.has(songId)) + .map(([songId, thumbCount]) => ({ + ...songMap.get(songId)!, + thumb_count: thumbCount, + })); return NextResponse.json({ success: true, data }); } catch (error) { @@ -38,48 +64,41 @@ export async function GET(): Promise>> { { status: 401 }, ); } - console.error('Error in like API:', error); + console.error('Error in thumb-up API:', error); return NextResponse.json( - { success: false, error: 'Failed to get like songs' }, + { success: false, error: 'Failed to get thumb-up songs' }, { status: 500 }, ); } } -export async function PATCH(request: Request): Promise>> { +export async function POST(request: Request): Promise>> { try { const supabase = await createClient(); + const userId = await getAuthenticatedUser(supabase); const { point, songId } = await request.json(); - const { data } = await supabase - .from('total_stats') - .select('total_thumb') - .eq('song_id', songId) - .single(); - - if (data) { - const totalThumb = data.total_thumb + point; - - const { error: updateError } = await supabase - .from('total_stats') - .update({ total_thumb: totalThumb }) - .eq('song_id', songId); + const { error: insertError } = await supabase + .from('thumb_logs') + .insert({ song_id: songId, user_id: userId, thumb_count: point }); - if (updateError) throw updateError; - } else { - const { error: insertError } = await supabase - .from('total_stats') - .insert({ song_id: songId, total_thumb: point }); - - if (insertError) throw insertError; - } + if (insertError) throw insertError; return NextResponse.json({ success: true }); } catch (error) { - console.error('Error in like API:', error); + if (error instanceof Error && error.cause === 'auth') { + return NextResponse.json( + { + success: false, + error: 'User not authenticated', + }, + { status: 401 }, + ); + } + console.error('Error in thumb-up API:', error); return NextResponse.json( - { success: false, error: 'Failed to post like song' }, + { success: false, error: 'Failed to post thumb-up song' }, { status: 500 }, ); } diff --git a/apps/web/src/app/popular/PopularRankingList.tsx b/apps/web/src/app/popular/PopularRankingList.tsx index 0ca527b..03b4d0f 100644 --- a/apps/web/src/app/popular/PopularRankingList.tsx +++ b/apps/web/src/app/popular/PopularRankingList.tsx @@ -32,7 +32,7 @@ export default function PopularRankingList() {
{data && data.length > 0 ? ( data.map((item, index) => ( - + )) ) : (
diff --git a/apps/web/src/lib/api/thumbSong.ts b/apps/web/src/lib/api/thumbSong.ts index e921e92..6e441f4 100644 --- a/apps/web/src/lib/api/thumbSong.ts +++ b/apps/web/src/lib/api/thumbSong.ts @@ -8,7 +8,7 @@ export async function getSongThumbList() { return response.data; } -export async function patchSongThumb(body: { songId: string; point: number }) { - const response = await instance.patch>('/songs/thumb-up', body); +export async function postSongThumb(body: { songId: string; point: number }) { + const response = await instance.post>('/songs/thumb-up', body); return response.data; } diff --git a/apps/web/src/queries/songThumbQuery.ts b/apps/web/src/queries/songThumbQuery.ts index 2978ab3..3bbfd74 100644 --- a/apps/web/src/queries/songThumbQuery.ts +++ b/apps/web/src/queries/songThumbQuery.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getSongThumbList, patchSongThumb } from '@/lib/api/thumbSong'; +import { getSongThumbList, postSongThumb } from '@/lib/api/thumbSong'; export const useSongThumbQuery = () => { return useQuery({ @@ -21,14 +21,14 @@ export const useSongThumbQuery = () => { export const useSongThumbMutation = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (body: { songId: string; point: number }) => patchSongThumb(body), + mutationFn: (body: { songId: string; point: number }) => postSongThumb(body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['songThumb'] }); queryClient.invalidateQueries({ queryKey: ['searchSong'] }); }, onError: error => { console.error('error', error); - alert(error.message ?? 'PATCH 실패'); + alert(error.message ?? 'POST 실패'); }, }); }; diff --git a/apps/web/src/types/song.ts b/apps/web/src/types/song.ts index dbb66ab..ea781fb 100644 --- a/apps/web/src/types/song.ts +++ b/apps/web/src/types/song.ts @@ -73,5 +73,5 @@ export interface AddListModalSong extends Song { } export interface ThumbUpSong extends Song { - total_thumb: number; + thumb_count: number; } From 3d30b35fd4fab96b8d929555c64b66c324b7a316 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Thu, 26 Mar 2026 01:35:28 +0900 Subject: [PATCH 12/12] =?UTF-8?q?chore=20:=20gitignore=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a4dd6ba..5aae6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,5 @@ yarn-error.log* # Claude Code local settings (contains secrets) temp/ -.vscode \ No newline at end of file +.vscode +settings.local.json \ No newline at end of file