diff --git a/README.md b/README.md index 16dd2ab..87ac977 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ data/ - 영상 ID 인라인 편집 가능 (전체 영상에 걸쳐 고유, 안전한 ID 형태만 허용. 변경하면 공유 URL 의 마지막 세그먼트가 같이 바뀝니다). - 오른쪽 위 "영상 추가" → `/op/folder/.../video/editor` 로 이동. 1단계·2단계 폴더 어디서나 영상을 추가할 수 있습니다 (1단계 폴더에는 "하위 폴더 추가" 버튼도 함께 노출). - 2단계 폴더에서만 노출되는 "플레이리스트 추가" 버튼: YouTube 플레이리스트 URL 을 넣으면 항목을 미리 보여주고 시작할 수 있습니다. - - 항목별 **드래그 정렬** (정렬 UI 는 플레이리스트 임포트 화면 한정), 제목 / ID 인라인 편집. + - 항목별 **썸네일 미리보기**(YouTube `mqdefault`), **드래그 정렬** (정렬 UI 는 플레이리스트 임포트 화면 한정), 제목 / ID 인라인 편집. - ID 정책 토글: - **랜덤** — 항목별로 24자 hex (crypto.randomUUID 압축, 폴백은 16진수 24자) 자동 생성. - **시퀀셜** — 원래 플레이리스트 순서 기준 1, 2, 3, ... 으로 부여. zero-pad 옵션을 켜면 항목 총개수 자릿수에 맞춰 `001, 002, ...` 처럼 패딩. diff --git a/public/playlist.js b/public/playlist.js index e4aa854..c952bad 100644 --- a/public/playlist.js +++ b/public/playlist.js @@ -175,6 +175,7 @@ li.innerHTML = '' + '' + '' + (idx + 1) + '' + + '
' + '
' + '' + '
' @@ -185,11 +186,24 @@ + '' + '' + var thumbBox = li.querySelector('.entryThumb') var titleInput = li.querySelector('.entryTitleInput') var idInput = li.querySelector('.entryIdInput') var durSpan = li.querySelector('.entryDuration') var removeBtn = li.querySelector('.entryRemove') + if (entry.thumbnailUrl) { + var img = document.createElement('img') + img.src = entry.thumbnailUrl + img.alt = '' + img.loading = 'lazy' + // 이미지가 깨지면(예: 비공개/삭제 영상) 빈 박스로 둔다. + img.addEventListener('error', function () { thumbBox.classList.add('entryThumbEmpty') }) + thumbBox.appendChild(img) + } else { + thumbBox.classList.add('entryThumbEmpty') + } + titleInput.value = entry.title || '' idInput.value = entry.videoId || '' durSpan.textContent = formatDuration(entry.durationSec) @@ -364,6 +378,7 @@ url: e.url, title: e.title || '제목 없음', durationSec: e.durationSec, + thumbnailUrl: e.thumbnailUrl || null, videoId: '', idStatus: 'pending', idReason: '' diff --git a/public/styles.css b/public/styles.css index 872db75..70941fc 100644 --- a/public/styles.css +++ b/public/styles.css @@ -353,7 +353,7 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content } .playlistEntryRow { display: grid; - grid-template-columns: 26px 36px 1fr 70px 28px; + grid-template-columns: 26px 36px 96px 1fr 70px 28px; align-items: center; gap: 10px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; @@ -367,6 +367,13 @@ body.siteBody.centerLayout { display: flex; align-items: center; justify-content font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--text-muted); font-size: 13px; text-align: right; } +.entryThumb { + width: 96px; aspect-ratio: 16 / 9; + border-radius: 6px; overflow: hidden; + background: var(--bg); border: 1px solid var(--border); +} +.entryThumb img { width: 100%; height: 100%; object-fit: cover; display: block; } +.entryThumbEmpty { opacity: 0.6; } .entryFields { display: flex; flex-direction: column; gap: 4px; min-width: 0; } diff --git a/src/youtube.ts b/src/youtube.ts index 4c56d1e..81856d6 100644 --- a/src/youtube.ts +++ b/src/youtube.ts @@ -97,6 +97,8 @@ export interface PlaylistEntry { title: string /** 메타에 없으면 null. UI 표시용. */ durationSec: number | null + /** 미리보기용 썸네일 URL. 못 구하면 null. */ + thumbnailUrl: string | null /** 1 부터 시작하는 원래 플레이리스트 위치 — 시퀀셜 ID zero-pad 자릿수 계산용. */ originalIndex: number } @@ -157,7 +159,14 @@ export async function probeYoutubePlaylist(url: string): Promise