diff --git a/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleService.java b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleService.java index 98b45a8..df4e250 100644 --- a/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleService.java +++ b/src/main/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleService.java @@ -1,5 +1,6 @@ package com.hlab.yanalyst.domain.shortform; +import com.hlab.yanalyst.global.notify.TelegramNotifier; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -29,6 +30,7 @@ public class ShortformSubtitleService { private final ShortformJobRepository jobRepository; private final ShortformWhisperService whisperService; + private final TelegramNotifier telegramNotifier; @Value("${ytdlp.bin:yt-dlp}") private String ytdlpBin; @@ -52,18 +54,47 @@ public class ShortformSubtitleService { String vtt = collectOrTranscribe(jobId, job.getVideoId()); ShortformJob current = jobRepository.findById(jobId).orElse(null); if (current == null) return; - if (vtt == null) current.markSubtitleUnavailable("한국어 공식/자동 자막이 없습니다"); + if (vtt == null) current.markSubtitleUnavailable("YouTube/Whisper 자막을 생성하지 못했습니다"); else current.storeSubtitle(vtt); jobRepository.save(current); + if (vtt == null) notifyFailure(current, current.getSubtitleError()); + else notifyReady(current); } catch (Exception e) { log.warn("숏폼 큐 자막 수집 실패: jobId={}, videoId={}", jobId, job.getVideoId(), e); jobRepository.findById(jobId).ifPresent(current -> { current.markSubtitleFailed(e.getMessage()); jobRepository.save(current); + notifyFailure(current, current.getSubtitleError()); }); } } + private void notifyReady(ShortformJob job) { + telegramNotifier.sendMessage("✅ 숏폼 자막 생성 완료\n" + + "분류: " + categoryLabel(job) + "\n" + + "영상: " + job.getVideoId() + "\n" + + "숏폼 큐에서 확인"); + } + + private void notifyFailure(ShortformJob job, String reason) { + telegramNotifier.sendMessage("❌ 숏폼 자막 생성 실패\n" + + "분류: " + categoryLabel(job) + "\n" + + "영상: " + job.getVideoId() + "\n" + + "사유: " + escapeHtml(reason) + "\n" + + "숏폼 큐에서 확인"); + } + + private static String categoryLabel(ShortformJob job) { + return job.categoryOrDefault() == ShortformCategory.POLITICS ? "정치" : "예능"; + } + + private static String escapeHtml(String value) { + if (value == null || value.isBlank()) return "알 수 없는 오류"; + return value.replace("&", "&").replace("<", "<").replace(">", ">"); + } + String collectOrTranscribe(Long jobId, String videoId) throws IOException, InterruptedException { try { String vtt = collectWithRetry(videoId); diff --git a/src/test/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleServiceTest.java b/src/test/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleServiceTest.java index da0f856..d6641f3 100644 --- a/src/test/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleServiceTest.java +++ b/src/test/java/com/hlab/yanalyst/domain/shortform/ShortformSubtitleServiceTest.java @@ -1,5 +1,6 @@ package com.hlab.yanalyst.domain.shortform; +import com.hlab.yanalyst.global.notify.TelegramNotifier; import org.junit.jupiter.api.Test; import java.nio.file.Path; @@ -37,7 +38,8 @@ class ShortformSubtitleServiceTest { @Test void retriesOnceWhenFirstCollectionHasNoSubtitle() throws Exception { ShortformSubtitleService service = spy(new ShortformSubtitleService( - mock(ShortformJobRepository.class), mock(ShortformWhisperService.class))); + mock(ShortformJobRepository.class), mock(ShortformWhisperService.class), + mock(TelegramNotifier.class))); doReturn(null, "WEBVTT\n").when(service).collect("gDfGqRDubCg"); assertThat(service.collectWithRetry("gDfGqRDubCg")).isEqualTo("WEBVTT\n"); @@ -48,7 +50,8 @@ class ShortformSubtitleServiceTest { void fallsBackToWhisperWhenYoutubeHasNoSubtitle() throws Exception { ShortformJobRepository repository = mock(ShortformJobRepository.class); ShortformWhisperService whisperService = mock(ShortformWhisperService.class); - ShortformSubtitleService service = spy(new ShortformSubtitleService(repository, whisperService)); + ShortformSubtitleService service = spy(new ShortformSubtitleService( + repository, whisperService, mock(TelegramNotifier.class))); doReturn(null).when(service).collectWithRetry("gDfGqRDubCg"); when(whisperService.transcribe("gDfGqRDubCg")).thenReturn("WEBVTT\n"); @@ -64,13 +67,15 @@ class ShortformSubtitleServiceTest { "gDfGqRDubCg", ShortformCategory.ENTERTAINMENT); when(repository.findById(1L)).thenReturn(Optional.of(job)); + TelegramNotifier telegramNotifier = mock(TelegramNotifier.class); ShortformSubtitleService service = spy(new ShortformSubtitleService( - repository, mock(ShortformWhisperService.class))); + repository, mock(ShortformWhisperService.class), telegramNotifier)); doReturn("WEBVTT\n").when(service).collectOrTranscribe(1L, "gDfGqRDubCg"); service.collectAsync(1L); assertThat(job.getSubtitleStatus()).isEqualTo(SubtitleStatus.READY); assertThat(job.getSubtitleVtt()).isEqualTo("WEBVTT\n"); + verify(telegramNotifier).sendMessage(org.mockito.ArgumentMatchers.contains("자막 생성 완료")); } }