diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java index 0e3a752..e0c7450 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java @@ -143,6 +143,19 @@ public class ChannelVideoCurationController { return ApiResponse.ok(curationService.downloadAndTranscribe(id, language)); } + @GetMapping("/{id}/download") + @Operation(summary = "받은 원본 상태", description = "원본 다운로드 캐시 보유 여부와 용량을 반환한다. 응답: {cached, sizeBytes}.") + public ApiResponse> downloadStatus(@PathVariable Long id) { + return ApiResponse.ok(curationService.downloadStatus(id)); + } + + @DeleteMapping("/{id}/download") + @Operation(summary = "받은 원본 삭제", description = "원본 다운로드로 받아둔 캐시 mp4를 삭제한다(자막/세그먼트는 유지). 응답: {deleted}.") + public ApiResponse> deleteDownload(@PathVariable Long id) { + boolean deleted = curationService.deleteDownloadCache(id); + return ApiResponse.ok(Map.of("deleted", deleted)); + } + @PostMapping(value = "/{id}/transcribe", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation(summary = "업로드 영상 전사(Whisper)", description = "업로드한 영상 파일을 Python /transcribe(faster-whisper)로 보내 영상 싱크 세그먼트를 추출·저장한다. " 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 f959334..7e726ff 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java @@ -143,6 +143,22 @@ public class ChannelVideoCurationService { return result; } + /** 다운로드 캐시 상태(보유 여부 + 용량). 재가공 진입 시 '받은 원본 삭제' 버튼 표시/렌더 캐시 사용 판단에 쓴다. */ + public Map downloadStatus(Long videoId) { + ChannelVideo v = find(videoId); + java.util.Optional cached = videoDownloadService.cachedFile(v.getId()); + Map result = new LinkedHashMap<>(); + result.put("cached", cached.isPresent()); + result.put("sizeBytes", cached.map(java.io.File::length).orElse(0L)); + return result; + } + + /** 재가공 화면 '받은 원본 삭제' — 다운로드 캐시 mp4 삭제(자막/세그먼트는 유지). */ + public boolean deleteDownloadCache(Long videoId) { + ChannelVideo v = find(videoId); + return videoDownloadService.deleteCache(v.getId()); + } + /** 다운로드 캐시 파일로 말 없는 구간 제거(+배속) 렌더. 캐시 없으면 안내 예외. */ public byte[] renderTrimmedFromCached(Long videoId, double pad, double minGap, double speed) { ChannelVideo v = find(videoId); @@ -194,12 +210,13 @@ public class ChannelVideoCurationService { return v; } - /** 수집함에서 영상 제거(연결된 스크립트도 함께 삭제). */ + /** 수집함에서 영상 제거(연결된 스크립트 + 다운로드 캐시도 함께 삭제). */ @Transactional public void delete(Long videoId) { ChannelVideo video = find(videoId); channelVideoScriptRepository.deleteAll( channelVideoScriptRepository.findAllByVideoId(video.getVideoId())); + videoDownloadService.deleteCache(video.getId()); // 고아 mp4 방지 channelVideoRepository.delete(video); } diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/VideoDownloadService.java b/src/main/java/com/hlab/yanalyst/domain/channel/VideoDownloadService.java index af1866a..ddd07e1 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/VideoDownloadService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/VideoDownloadService.java @@ -122,6 +122,21 @@ public class VideoDownloadService { return cachedFile(channelVideoId).isPresent(); } + /** 캐시된 다운로드 파일 삭제. 존재해서 지웠으면 true, 원래 없었으면 false. */ + public boolean deleteCache(Long channelVideoId) { + File file = Path.of(downloadDir).resolve(channelVideoId + ".mp4").toFile(); + if (!file.exists()) { + return false; + } + boolean ok = file.delete(); + if (ok) { + log.info("다운로드 캐시 삭제: {}", file); + } else { + log.warn("다운로드 캐시 삭제 실패(파일 잠김?): {}", file); + } + return ok; + } + /** * yt-dlp 명령 인자 리스트를 조립한다(프로세스 실행 없음 — 단위테스트 대상). * videoId 는 {@code [A-Za-z0-9_-]{11}} 형식만 허용한다. diff --git a/src/main/resources/templates/rework.html b/src/main/resources/templates/rework.html index 7a605b1..028b9ef 100644 --- a/src/main/resources/templates/rework.html +++ b/src/main/resources/templates/rework.html @@ -152,6 +152,9 @@ + @@ -459,7 +462,7 @@ const lang = document.getElementById('langSel').value; const url = API + '/' + VIDEO_ID + '/download' + (lang ? ('?language=' + lang) : ''); const s = await api(url, { method:'POST' }); - HAS_SERVER_FILE = true; // 렌더는 서버 캐시본 사용 + updateCacheUI(true, s.sizeBytes || 0); // 서버 캐시 보유 → 렌더 캐시 사용 + 삭제버튼 노출 UPLOADED_FILE = null; renderSegments(s.segments); document.getElementById('transcript').value = s.transcript || ''; @@ -477,11 +480,40 @@ } } + // 서버 캐시 보유 상태 → '받은 원본 삭제' 버튼 표시 + 렌더 캐시 사용 여부(HAS_SERVER_FILE) 갱신 + function updateCacheUI(cached, sizeBytes){ + HAS_SERVER_FILE = !!cached; + const btn = document.getElementById('deleteDlBtn'); + if(!btn) return; + btn.style.display = cached ? '' : 'none'; + if(cached && sizeBytes){ + btn.title = '서버에 받아둔 원본(' + (sizeBytes/1048576).toFixed(1) + 'MB) 삭제(자막은 유지)'; + } + } + + // 받아둔 원본 mp4 삭제(자막/세그먼트는 유지) + async function deleteDownload(){ + if(!confirm('서버에 받아둔 원본 영상(mp4)을 삭제할까요?\n자막·세그먼트는 그대로 유지됩니다.')) return; + const btn = document.getElementById('deleteDlBtn'); + const status = document.getElementById('transcribeStatus'); + const orig = btn.innerHTML; btn.disabled = true; btn.innerHTML = '삭제 중…'; + try { + const r = await api(API + '/' + VIDEO_ID + '/download', { method:'DELETE' }); + updateCacheUI(false, 0); + status.style.display = 'block'; status.style.color = '#9ca3af'; + status.textContent = r && r.deleted ? '받은 원본을 삭제했습니다. (다시 받으려면 ‘원본 다운로드’)' : '삭제할 원본이 없습니다.'; + } catch(e){ + status.style.display = 'block'; status.style.color = '#f87171'; + status.textContent = '원본 삭제 실패: ' + e.message; + } finally { + btn.disabled = false; btn.innerHTML = orig; if(window.lucide) lucide.createIcons(); + } + } + async function onVideoSelected(ev){ const file = ev.target.files && ev.target.files[0]; if(!file) return; - UPLOADED_FILE = file; // 렌더(자르기) 재전송용으로 보관 - HAS_SERVER_FILE = false; // 업로드본 우선 + UPLOADED_FILE = file; // 렌더(자르기) 재전송용으로 보관(업로드본이 서버 캐시보다 우선) // 1) 업로드한 파일을 로컬에서 즉시 재생(