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

@@ -363,7 +363,7 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content
}
.playlistEntryRow {
display: grid;
grid-template-columns: 26px 36px 96px 1fr 70px 28px;
grid-template-columns: 24px 28px 90px minmax(0, 1fr) 210px 60px 26px;
align-items: center; gap: 10px;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 8px 10px;
@@ -378,10 +378,18 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content
color: var(--text-muted); font-size: 13px; text-align: right;
}
.entryThumb {
width: 96px; aspect-ratio: 16 / 9;
border-radius: 6px; overflow: hidden;
width: 90px; aspect-ratio: 16 / 9;
border-radius: 6px; overflow: hidden; position: relative;
background: var(--bg); border: 1px solid var(--border);
cursor: pointer;
}
.entryThumb::after {
content: '▶'; position: absolute; inset: 0;
display: flex; align-items: center; justify-content: center;
color: #fff; font-size: 18px; text-shadow: 0 1px 4px rgba(0,0,0,0.7);
opacity: 0; transition: opacity 120ms ease; pointer-events: none;
}
.entryThumb:hover::after { opacity: 1; }
.entryThumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.entryThumbEmpty { opacity: 0.6; }
.entryFields {
@@ -412,6 +420,49 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content
}
.entryRemove:hover { color: #e57373; border-color: #e57373; }
/* 항목별 시작/종료 시간 입력 (제목 오른쪽 절반 공간) */
.entryTrim {
display: flex; flex-direction: column; gap: 4px;
}
.entryTrim label {
display: flex; align-items: center; gap: 6px;
font-size: 12px; color: var(--text-muted);
}
.entryTrim input {
flex: 1; min-width: 0;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 3px 6px; font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* 일괄 시작/종료 + 적용/초기화 */
.bulkTrimBox {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: 10px; padding: 10px 14px;
}
.bulkTrimBox label {
display: flex; align-items: center; gap: 6px;
color: var(--text); font-size: 13px;
}
.bulkTrimBox input {
width: 70px;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 4px 8px; font-size: 13px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* YouTube 임베드 (미리보기 / 자르기 모달) */
.ytEmbedWrap {
position: relative; width: 100%; aspect-ratio: 16 / 9;
background: #000; border-radius: 8px; overflow: hidden;
}
.ytEmbedWrap iframe, .ytEmbedWrap > div {
position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
}
.trimModal { width: min(900px, 94vw); gap: 12px; }
.trimModal .trimTimeline { background: var(--bg-alt); }
.playlistFooter {
display: flex; align-items: center; justify-content: space-between;
gap: 12px; flex-wrap: wrap; margin-top: 8px;