fix(rework): Gemini 실패 사유를 사용자에게 명확히 + 음성 없음 메시지 정정

- GeminiSubtitleService: 403(프로젝트 차단)/429(한도)/400(키 무효)을 한국어 메시지로
  IllegalArgumentException throw → 'Internal Server Error' 대신 실제 사유 노출
- rework.html: 음성 패널 빈 경우 '말소리 없음(음악/화면자막만) 또는 전사 미실행'으로 정정

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-25 17:16:29 +09:00
parent 447645c19a
commit 6380a28a6f
2 changed files with 18 additions and 6 deletions

View File

@ -46,7 +46,7 @@ public class GeminiSubtitleService {
public List<ScriptResponseDto.Segment> fetchKoreanScreenSubtitles(String videoId) { public List<ScriptResponseDto.Segment> fetchKoreanScreenSubtitles(String videoId) {
String key = sanitizeApiKey(apiKey); String key = sanitizeApiKey(apiKey);
if (key.isEmpty()) { if (key.isEmpty()) {
throw new IllegalStateException("GEMINI_API_KEY 가 설정되지 않았습니다."); throw new IllegalArgumentException("GEMINI_API_KEY 가 설정되지 않았습니다.");
} }
String videoUrl = "https://www.youtube.com/watch?v=" + videoId; String videoUrl = "https://www.youtube.com/watch?v=" + videoId;
String apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/" String apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/"
@ -66,13 +66,25 @@ public class GeminiSubtitleService {
return segs; return segs;
} catch (HttpStatusCodeException e) { } catch (HttpStatusCodeException e) {
// 키는 URL 쿼리에 있으므로 메시지에 apiUrl 넣지 않는다(노출 방지). // 키는 URL 쿼리에 있으므로 메시지에 apiUrl 넣지 않는다(노출 방지).
int code = e.getStatusCode().value();
String body = e.getResponseBodyAsString(); String body = e.getResponseBodyAsString();
String tail = body.length() > 300 ? body.substring(0, 300) : body; String msg;
throw new RuntimeException("Gemini 호출 실패(" + e.getStatusCode() + "): " + tail, e); if (code == 403) {
} catch (RuntimeException e) { msg = "Gemini 접근 거부(403): 이 API 키의 프로젝트가 차단되었습니다. 다른 프로젝트의 새 API 키가 필요합니다.";
} else if (code == 429) {
msg = "Gemini 무료 한도 초과(429): 잠시 후 다시 시도하세요.";
} else if (code == 400 && body.contains("API key not valid")) {
msg = "Gemini API 키가 유효하지 않습니다(GEMINI_API_KEY 값을 확인하세요).";
} else {
String tail = body.length() > 200 ? body.substring(0, 200) : body;
msg = "Gemini 호출 실패(" + code + "): " + tail;
}
log.error("Gemini 호출 실패({}) for video {}", code, videoId);
throw new IllegalArgumentException(msg, e); // GlobalExceptionHandler 메시지를 그대로 노출
} catch (IllegalArgumentException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Gemini 자막 추출 실패: " + e.getMessage(), e); throw new IllegalArgumentException("Gemini 자막 추출 실패: " + e.getMessage(), e);
} }
} }

View File

@ -490,7 +490,7 @@
// 왼쪽: 저장된 Whisper 세그먼트 → 한국어 번역(LibreTranslate) // 왼쪽: 저장된 Whisper 세그먼트 → 한국어 번역(LibreTranslate)
const audioP = (async () => { const audioP = (async () => {
if(!SEGMENTS || !SEGMENTS.length) return { error: '음성 자막: 먼저 ‘전사’를 실행하세요' }; if(!SEGMENTS || !SEGMENTS.length) return { error: '음성 자막 없음 — 이 영상에 말소리가 없거나(음악·화면자막만) 아직 ‘전사’ 미실행' };
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' }); const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
const ko = (r && r.texts) || []; const ko = (r && r.texts) || [];
return { segments: SEGMENTS.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) })) }; return { segments: SEGMENTS.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) })) };