feat(playlist): per-entry/bulk trim, preview, and trim editor on import

플레이리스트 가져오기 화면에 다음을 추가:
- 항목별 제목 입력을 절반으로 줄이고 오른쪽에 시작/종료 시간 입력 추가
  (기본 시작 0, 종료는 영상 길이)
- 정렬 옵션 옆에 일괄 시작/종료 입력과 적용/초기화 버튼 추가
  (초기화 = 시작 0, 종료 = 영상 길이)
- 항목 썸네일 클릭 시 YouTube 임베드로 미리보기 재생
- 항목 우클릭 → 자르기 메뉴 → /video/editor 식 타임라인 모달(YouTube IFrame)
  에서 구간을 정하면 항목의 시작/종료 입력에 반영
- 백엔드: playlist/start 가 항목별 startSec/endSec 를 받아 다운로드 잡에
  실어 보내고, 60fps 후처리 후 ffmpeg 로 해당 구간을 잘라 편집본 생성

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Claude (owner)
2026-06-02 00:50:04 +09:00
parent 066ae6b112
commit 34c040c15d
5 changed files with 596 additions and 12 deletions

View File

@@ -385,7 +385,13 @@ opRouter.post(
throw new Error('등록할 항목이 없습니다.')
}
type Cleaned = { url: string; title: string; videoId: string }
type Cleaned = {
url: string
title: string
videoId: string
startSec: number
endSec: number | null
}
const cleaned: Cleaned[] = []
const seen = new Set<string>()
for (let i = 0; i < rawEntries.length; i += 1) {
@@ -405,8 +411,14 @@ opRouter.post(
if (videoIdExists(videoId)) {
throw new Error(`항목 ${i + 1}: 영상 ID "${videoId}" 가 이미 사용 중입니다.`)
}
// 자르기 구간(선택). 잘못된 값은 무시하고 전체로 처리.
const startRaw = Number(e?.startSec)
const startSec = Number.isFinite(startRaw) && startRaw > 0 ? startRaw : 0
const endRaw = Number(e?.endSec)
const endSec =
Number.isFinite(endRaw) && endRaw > startSec ? endRaw : null
seen.add(videoId)
cleaned.push({ url, title, videoId })
cleaned.push({ url, title, videoId, startSec, endSec })
}
// 검증 통과 → 순차적으로 잡 시작. createVideo 의 unique 제약이 마지막 안전망.
@@ -416,7 +428,9 @@ opRouter.post(
folderId: folder.id,
url: entry.url,
title: entry.title,
videoId: entry.videoId
videoId: entry.videoId,
startSec: entry.startSec,
endSec: entry.endSec
})
jobs.push({ jobId: job.id, videoId: job.videoId })
}