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("자막 생성 완료"));
}
}