From fa6342f97eb43c8a999d2f694b3d51430c72cd7d Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 12 Jun 2026 20:06:00 +0900 Subject: [PATCH] fix(rework): long-op timeout, dedup query, clearer error, logging Review follow-ups (safe fixes): - pythonRestTemplate bean (10min read timeout) for /transcribe and /render so ffmpeg encoding / Whisper don't hit the shared 120s timeout; resolved by bean name so existing restTemplate injections are unaffected - getScriptData: fetch the script row once instead of 3 separate queries - renderTrimmed "no segments" now IllegalArgumentException (400 + visible message) instead of IllegalStateException swallowed as generic 500 - ProductionService: System.out.println -> log.info (3x) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../yanalyst/domain/channel/ChannelService.java | 8 +++++--- .../domain/channel/ChannelVideoCurationService.java | 13 ++++++------- .../domain/production/ProductionService.java | 6 +++--- .../yanalyst/global/config/RestTemplateConfig.java | 13 +++++++++++++ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java index 2af9c35..a5661a8 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java @@ -34,6 +34,7 @@ public class ChannelService { private final ChannelRepository channelRepository; private final RestTemplate restTemplate; + private final RestTemplate pythonRestTemplate; // 전사/렌더 등 장시간 호출용(긴 read timeout). 빈 이름으로 구분 주입. private final ObjectMapper objectMapper; private final ChannelVideoRepository channelVideoRepository; @@ -410,7 +411,7 @@ public class ChannelService { } HttpEntity> request = new HttpEntity<>(body, headers); - ResponseEntity response = restTemplate.postForEntity(apiUrl, request, String.class); + ResponseEntity response = pythonRestTemplate.postForEntity(apiUrl, request, String.class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Transcribe API failed with status: " + response.getStatusCode()); @@ -479,7 +480,8 @@ public class ChannelService { public byte[] renderTrimmed(Long channelVideoId, MultipartFile file, double pad, double minGap, double speed) { List segments = getSegments(channelVideoId); if (segments.isEmpty()) { - throw new IllegalStateException("세그먼트가 없습니다. 먼저 전사를 실행하세요."); + // 사용자에게 이유가 보이도록 400(IllegalArgumentException → GlobalExceptionHandler 가 메시지 노출). + throw new IllegalArgumentException("세그먼트가 없습니다. 먼저 영상 업로드·전사를 실행하세요."); } KeepIntervalPlanner.Plan plan = KeepIntervalPlanner.plan(segments, pad, minGap); @@ -494,7 +496,7 @@ public class ChannelService { body.add("speed", String.valueOf(speed)); HttpEntity> request = new HttpEntity<>(body, headers); - ResponseEntity response = restTemplate.postForEntity(apiUrl, request, byte[].class); + ResponseEntity response = pythonRestTemplate.postForEntity(apiUrl, request, byte[].class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Render API failed with status: " + response.getStatusCode()); } diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java index 3e92fb1..e146d39 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java @@ -91,17 +91,16 @@ public class ChannelVideoCurationService { /** 스크립트 전체(평문 + 영상 싱크 세그먼트) 조회. 에디터의 세그먼트 리스트용. */ public Map getScriptData(Long videoId) { ChannelVideo v = find(videoId); - String transcript = channelVideoScriptRepository.findFirstByVideoIdOrderByIdDesc(v.getVideoId()) - .map(ChannelVideoScript::getTranscript) + ChannelVideoScript script = channelVideoScriptRepository + .findFirstByVideoIdOrderByIdDesc(v.getVideoId()) .orElse(null); - String language = channelVideoScriptRepository.findFirstByVideoIdOrderByIdDesc(v.getVideoId()) - .map(ChannelVideoScript::getLanguage) - .orElse(null); - List segments = channelService.getSegments(v.getId()); + String transcript = (script != null) ? script.getTranscript() : null; + List segments = (script != null) + ? channelService.parseSegments(script.getSegmentsJson()) : List.of(); Map result = new LinkedHashMap<>(); result.put("hasScript", transcript != null); - result.put("language", language); + result.put("language", (script != null) ? script.getLanguage() : null); result.put("transcript", transcript == null ? "" : transcript); result.put("segments", segments); return result; diff --git a/src/main/java/com/hlab/yanalyst/domain/production/ProductionService.java b/src/main/java/com/hlab/yanalyst/domain/production/ProductionService.java index 54449e9..f4b4151 100644 --- a/src/main/java/com/hlab/yanalyst/domain/production/ProductionService.java +++ b/src/main/java/com/hlab/yanalyst/domain/production/ProductionService.java @@ -226,7 +226,7 @@ public class ProductionService { String oldSummary = text.substring(oldStart + oldSummaryMarker.length(), newStart).trim(); String newSummary = text.substring(newStart + newSummaryMarker.length()).trim(); - System.out.println("Saving summaries for video " + videoId); + log.info("Saving summaries for video {}", videoId); script.setOldScriptSummary(oldSummary); script.setNewScriptSummary(newSummary); @@ -270,7 +270,7 @@ public class ProductionService { // 1. Fetch content using API String text = readGoogleDoc(docId); - System.out.println("Saving final script for video " + videoId); + log.info("Saving final script for video {}", videoId); // 2. Save to DB script.setFinalScript(text); @@ -307,7 +307,7 @@ public class ProductionService { // 1. Fetch content using API String text = readGoogleDoc(docId); - System.out.println("Saving opening script for video " + videoId); + log.info("Saving opening script for video {}", videoId); // 2. Save to DB script.setOpeningScript(text); diff --git a/src/main/java/com/hlab/yanalyst/global/config/RestTemplateConfig.java b/src/main/java/com/hlab/yanalyst/global/config/RestTemplateConfig.java index fba975b..5e238ca 100644 --- a/src/main/java/com/hlab/yanalyst/global/config/RestTemplateConfig.java +++ b/src/main/java/com/hlab/yanalyst/global/config/RestTemplateConfig.java @@ -21,4 +21,17 @@ public class RestTemplateConfig { }) .build(); } + + /** + * Python 마이크로서비스(Whisper 전사 /transcribe, ffmpeg 렌더 /render)용 RestTemplate. + * 전사·인코딩은 수십 초~수 분이 걸릴 수 있어 read timeout 을 길게 둔다. + * (주입 시 빈 이름 {@code pythonRestTemplate} 로 구분 — 다른 곳의 restTemplate 주입에는 영향 없음) + */ + @Bean + public RestTemplate pythonRestTemplate(RestTemplateBuilder builder) { + return builder + .connectTimeout(Duration.ofSeconds(20)) + .readTimeout(Duration.ofMinutes(10)) + .build(); + } }