Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 25 additions & 4 deletions packages/crawling/src/cron/translationJpn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { artistAlias } from '@repo/constants';

import { getJpopSongsForTranslationDB } from '@/supabase/getDB';
import { getArtistKoMapDB, getJpopSongsForTranslationDB } from '@/supabase/getDB';
import { updateSongKoTranslationDB } from '@/supabase/updateDB';
import { translateJpnToKo } from '@/utils/translateJpnToKo';

Expand All @@ -9,6 +9,7 @@ const resultsLog = {
failed: 0,
skipped: 0,
usedAlias: 0,
usedDbArtist: 0,
};

// 히라가나, 카타카나, CJK 한자 범위로 일본어 포함 여부 판단
Expand All @@ -27,6 +28,10 @@ const songs = await getJpopSongsForTranslationDB();

console.log('J-POP 곡 수:', songs.length);

// DB 에 이미 번역된 artist → artist_ko 맵 (동일 아티스트 번역 일관성 유지 목적)
// DB 는 아티스트당 단일 artist_ko 로 정규화되어 있으며, 번역 성공 시 런타임에도 동기화한다
const dbArtistKoMap = await getArtistKoMapDB();

let processedCount = 0;
for (const song of songs) {
if (processedCount >= 10000) break;
Expand All @@ -53,23 +58,38 @@ for (const song of songs) {
continue;
}

// artistAlias 에 등록된 아티스트면 artist_ko 를 고정 값(alias 배열 0번째)으로 덮어쓰기
// artist_ko 우선순위:
// 1) artistAlias (수동 큐레이션된 고정 값)
// 2) DB 에 이미 번역된 동일 아티스트의 artist_ko (번역 일관성 유지)
// 3) AI 번역 결과
// title_ko 는 AI 번역 결과를 그대로 사용
const aliasArtistKo = artistAliasMap.get(song.artist);
const finalArtistKo = aliasArtistKo ?? result.artist_ko;
const dbArtistKo = dbArtistKoMap.get(song.artist);
const finalArtistKo = aliasArtistKo ?? dbArtistKo ?? result.artist_ko;

const success = await updateSongKoTranslationDB(song.id, result.title_ko, finalArtistKo);
if (!success) {
resultsLog.failed++;
continue;
}

let logPrefix: string;
if (aliasArtistKo) {
resultsLog.usedAlias++;
logPrefix = '[ALIAS]';
} else if (dbArtistKo) {
resultsLog.usedDbArtist++;
logPrefix = '[DB]';
} else {
resultsLog.success++;
logPrefix = '[OK]';
}

// DB 업데이트 성공 시 런타임 맵도 동기화 (first-seen 원칙 — 기존 값 덮어쓰지 않음)
if (!dbArtistKoMap.has(song.artist)) {
dbArtistKoMap.set(song.artist, finalArtistKo);
}
const logPrefix = aliasArtistKo ? '[ALIAS]' : '[OK]';

console.log(
`${logPrefix} ${song.title} → ${result.title_ko} / ${song.artist} → ${finalArtistKo}`,
);
Expand All @@ -90,5 +110,6 @@ console.log(`
- 스킵 (이미 번역됨): ${resultsLog.skipped}곡
- 성공 (AI 번역): ${resultsLog.success}곡
- 성공 (artist_ko alias 적용): ${resultsLog.usedAlias}곡
- 성공 (artist_ko DB 재사용): ${resultsLog.usedDbArtist}곡
- 실패: ${resultsLog.failed}곡
`);
24 changes: 24 additions & 0 deletions packages/crawling/src/supabase/getDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ export async function getJpopSongsForTranslationDB() {
return data;
}

// J-POP 곡 중 이미 번역된 artist → artist_ko 맵
// DB 는 아티스트당 단일 artist_ko 로 정규화되어 있으므로 먼저 만난 값을 사용
export async function getArtistKoMapDB(): Promise<Map<string, string>> {
const supabase = getClient();

const { data, error } = await supabase
.from('songs')
.select('artist, artist_ko, song_tags!inner(tag_id)')
.eq('song_tags.tag_id', 101)
.not('artist_ko', 'is', null)
.limit(50000);

if (error) throw error;

const map = new Map<string, string>();
for (const row of data) {
if (!row.artist || !row.artist_ko) continue;
if (!map.has(row.artist)) {
map.set(row.artist, row.artist_ko);
}
Comment on lines +121 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Non-deterministic artist_ko pick 🐞 Bug ≡ Correctness

getArtistKoMapDB()는 Map에 first-seen으로 artist_ko를 고정하지만 쿼리에 ORDER BY가 없어 동일 artist에 여러 artist_ko가 존재할
때 어떤 값이 선택될지 실행마다 달라질 수 있습니다. 이 값은 translationJpn.ts에서 AI 결과를 덮어써 artist_ko 재사용 일관성을 깨뜨릴 수 있습니다.
Agent Prompt
### Issue description
`getArtistKoMapDB()`는 "first-seen" 규칙을 구현하지만 쿼리 결과의 순서가 고정되어 있지 않아(ORDER BY 없음) 동일 artist에 여러 `artist_ko`가 있는 경우 어떤 값이 선택될지 비결정적입니다. 이 Map은 번역 시 AI 결과보다 우선 적용되므로, 실행마다 `artist_ko` 재사용 결과가 달라질 수 있습니다.

### Issue Context
- Map 구성은 `if (!map.has(row.artist)) map.set(...)` 형태로 첫 번째 행을 채택합니다.
- 하지만 Supabase 쿼리에 `.order(...)`가 없어 "첫 번째"의 의미가 불명확합니다.

### Fix Focus Areas
- packages/crawling/src/supabase/getDB.ts[121-136]
- packages/crawling/src/cron/translationJpn.ts[61-69]

### Suggested fix
- `getArtistKoMapDB()` 쿼리에 결정적 정렬을 추가하세요(예: `order('artist', { ascending: true })` 후 `order('updated_at', { ascending: true })` 또는 일관성 있는 tie-breaker).
- 가능하면 DB에서 artist 단위로 단일 row를 보장(뷰/RPC/unique constraint)하거나, 어떤 기준(최신/최초)을 채택할지 명시적으로 결정해 쿼리로 강제하세요.

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

}
return map;
}

export async function getSongTagSongIdsDB(): Promise<Set<string>> {
const supabase = getClient();

Expand Down