fix: 자막 상태 갱신과 재시작 복구 개선
This commit is contained in:
parent
9d27ad3d79
commit
504d0f09d5
@ -11,6 +11,7 @@ public interface ShortformJobRepository extends JpaRepository<ShortformJob, Long
|
||||
Optional<ShortformJob> findByVideoId(String videoId);
|
||||
List<ShortformJob> findAllByOrderByCreatedAtDesc();
|
||||
List<ShortformJob> findByStatusOrderByCreatedAtAsc(ShortformJobStatus status);
|
||||
List<ShortformJob> findBySubtitleStatus(SubtitleStatus subtitleStatus);
|
||||
@Query("select j from ShortformJob j where coalesce(j.category, "
|
||||
+ "com.hlab.yanalyst.domain.shortform.ShortformCategory.ENTERTAINMENT) = :category "
|
||||
+ "order by j.createdAt desc")
|
||||
|
||||
@ -3,6 +3,9 @@ package com.hlab.yanalyst.domain.shortform;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobDetail;
|
||||
import com.hlab.yanalyst.domain.shortform.dto.ShortformDtos.JobSummary;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
@ -12,12 +15,23 @@ import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ShortformService {
|
||||
|
||||
private final ShortformJobRepository jobRepository;
|
||||
private final ShortformOutputParser parser;
|
||||
private final ShortformSubtitleService subtitleService;
|
||||
|
||||
/** 배포·재시작으로 중단된 비동기 자막 수집을 애플리케이션 기동 후 다시 시작한다. */
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void recoverInterruptedSubtitleCollections() {
|
||||
List<ShortformJob> interrupted = jobRepository.findBySubtitleStatus(SubtitleStatus.COLLECTING);
|
||||
if (!interrupted.isEmpty()) {
|
||||
log.info("중단된 숏폼 자막 수집 {}건 재개", interrupted.size());
|
||||
}
|
||||
interrupted.forEach(job -> subtitleService.collectAsync(job.getId()));
|
||||
}
|
||||
|
||||
/** URL 등록. 같은 videoId가 이미 있으면 그 작업을 그대로 반환한다(중복 방지). */
|
||||
@Transactional
|
||||
public JobDetail register(String youtubeUrl) {
|
||||
|
||||
@ -235,12 +235,13 @@
|
||||
jobs.forEach(j => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'sf-job';
|
||||
el.dataset.jobId = j.id;
|
||||
el.innerHTML = `<div class="sf-job-head">
|
||||
<img src="https://img.youtube.com/vi/${j.videoId}/mqdefault.jpg" alt="">
|
||||
<div class="sf-job-title">
|
||||
<div class="t">${esc(j.title || j.videoId)}</div>
|
||||
<div class="u">${esc(j.youtubeUrl)} · 클립 ${j.clipCount}개</div>
|
||||
<div class="sf-subtitle ${j.subtitleStatus}"
|
||||
<div class="sf-subtitle ${j.subtitleStatus}" data-subtitle-status="${j.subtitleStatus}"
|
||||
title="${esc(j.subtitleError || '')}">자막 ${subtitleLabel(j)}</div>
|
||||
</div>
|
||||
<span class="sf-category ${j.category}">${j.category === 'POLITICS' ? '정치' : '예능'}</span>
|
||||
@ -261,12 +262,51 @@
|
||||
}
|
||||
});
|
||||
if (window.lucide) lucide.createIcons();
|
||||
scheduleSubtitlePoll(jobs);
|
||||
}
|
||||
|
||||
function scheduleSubtitlePoll(jobs) {
|
||||
clearTimeout(subtitlePollTimer);
|
||||
if (jobs.some(j => j.subtitleStatus === 'COLLECTING')) {
|
||||
subtitlePollTimer = setTimeout(loadJobs, 2000);
|
||||
subtitlePollTimer = setTimeout(pollSubtitleStatuses, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollSubtitleStatuses() {
|
||||
try {
|
||||
const jobs = await api('/api/shortform/jobs?category=' + currentCategory);
|
||||
jobs.forEach(j => {
|
||||
const card = document.querySelector(`.sf-job[data-job-id="${j.id}"]`);
|
||||
const status = card && card.querySelector('[data-subtitle-status]');
|
||||
if (!status) return;
|
||||
const previous = status.dataset.subtitleStatus;
|
||||
status.dataset.subtitleStatus = j.subtitleStatus;
|
||||
status.className = 'sf-subtitle ' + j.subtitleStatus;
|
||||
status.title = j.subtitleError || '';
|
||||
status.textContent = '자막 ' + subtitleLabel(j);
|
||||
if (previous === 'COLLECTING' && j.subtitleStatus === 'READY' && card.classList.contains('open')) {
|
||||
refreshSubtitleCopy(j.id, card);
|
||||
}
|
||||
});
|
||||
scheduleSubtitlePoll(jobs);
|
||||
} catch (err) {
|
||||
subtitlePollTimer = setTimeout(pollSubtitleStatuses, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSubtitleCopy(id, card) {
|
||||
const detail = await api('/api/shortform/jobs/' + id);
|
||||
const button = card.querySelector('[data-subtitle-copy]');
|
||||
if (!button || !detail.subtitleVtt) return;
|
||||
button.disabled = false;
|
||||
button.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(detail.subtitleVtt);
|
||||
button.textContent = '복사됨!';
|
||||
setTimeout(() => button.textContent = '타임스탬프 자막 복사', 1200);
|
||||
};
|
||||
}
|
||||
|
||||
function subtitleLabel(job) {
|
||||
if (job.subtitleStatus === 'COLLECTING' && job.subtitleError) return job.subtitleError;
|
||||
return ({NOT_REQUESTED:'대기', COLLECTING:'수집 중…', READY:'준비 완료',
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package com.hlab.yanalyst.domain.shortform;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ShortformServiceTest {
|
||||
|
||||
@Test
|
||||
void resumesCollectingSubtitleJobsAfterRestart() {
|
||||
ShortformJobRepository repository = mock(ShortformJobRepository.class);
|
||||
ShortformSubtitleService subtitleService = mock(ShortformSubtitleService.class);
|
||||
ShortformJob job = mock(ShortformJob.class);
|
||||
when(job.getId()).thenReturn(34L);
|
||||
when(repository.findBySubtitleStatus(SubtitleStatus.COLLECTING)).thenReturn(List.of(job));
|
||||
ShortformService service = new ShortformService(
|
||||
repository, mock(ShortformOutputParser.class), subtitleService);
|
||||
|
||||
service.recoverInterruptedSubtitleCollections();
|
||||
|
||||
verify(subtitleService).collectAsync(34L);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user