feat(ui): 썸네일 클릭 미리보기를 칸반·채널상세·프로덕션상세에 추가

수집함·발굴·대시보드처럼, 썸네일 있는 나머지 화면에서도 썸네일을 클릭하면
YouTube 9:16 미리보기 팝업이 뜨도록 통일.
- 칸반(board): 카드 썸네일 클릭(드래그와 분리 위해 stopPropagation)
- 채널상세(channel_detail): 영상 목록 썸네일(제목 링크는 YouTube 유지)
- 프로덕션상세(production_detail): videoUrl에서 11자리 id 추출해 임베드,
  id 없으면 새 탭 폴백
각 페이지에 동일 모달(배경/ESC 닫힘, 닫을 때 iframe 비워 재생 중지) 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-16 15:18:09 +09:00
parent c7ce8de90e
commit c0ade287c2
3 changed files with 90 additions and 9 deletions

View File

@ -44,7 +44,19 @@
<!-- columns injected -->
</div>
<!-- 영상 미리보기 모달 -->
<div id="videoModal" style="display:none; position:fixed; z-index:1000; inset:0; background:rgba(0,0,0,0.85); align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this) closeVideoModal()">
<div style="background:var(--surface); border:1px solid var(--border); border-radius:12px; overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:14px; padding:11px 14px; border-bottom:1px solid var(--border);">
<span id="vmTitle" class="text-sm font-bold truncate" style="max-width:300px;">미리보기</span>
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size:24px; line-height:1; background:none; border:none; cursor:pointer;">&times;</button>
</div>
<div id="vmFrame" style="width:min(360px,90vw); aspect-ratio:9/16; max-height:78vh; background:#000;"></div>
</div>
</div>
<style>
#vmFrame iframe { width:100%; height:100%; border:0; display:block; }
.kb-col { background: var(--surface-2); border:1px solid var(--border); border-radius: var(--r); display:flex; flex-direction:column; min-height:200px; }
.kb-col-head { padding:12px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--surface-2); border-radius:var(--r) var(--r) 0 0; }
.kb-body { padding:10px; display:flex; flex-direction:column; gap:8px; min-height:120px; flex:1; transition: background 0.15s; }
@ -94,7 +106,9 @@
].filter(Boolean).join('&nbsp;&nbsp;·&nbsp;&nbsp;');
return `<div class="kb-card" draggable="true" data-id="${v.id}">
<div style="display:flex; gap:8px;">
<img src="${esc(v.thumbnailUrl)}" style="width:64px; height:36px; object-fit:cover; border-radius:4px; flex-shrink:0;">
<img src="${esc(v.thumbnailUrl)}" title="미리보기" data-vid="${v.videoId}" data-title="${esc(v.title)}"
onclick="event.stopPropagation(); openVideoModal(this.dataset.vid, this.dataset.title)"
style="width:64px; height:36px; object-fit:cover; border-radius:4px; flex-shrink:0; cursor:pointer;">
<div style="min-width:0; flex:1;">
<div class="text-sm font-bold" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.3;">${esc(v.title)}</div>
</div>
@ -184,7 +198,17 @@
function openHelp(){ document.getElementById('helpModal').classList.add('open'); }
function closeHelp(){ document.getElementById('helpModal').classList.remove('open'); }
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeHelp(); });
function openVideoModal(vid, title){
document.getElementById('vmTitle').textContent = title || '미리보기';
document.getElementById('vmFrame').innerHTML = '<iframe src="https://www.youtube.com/embed/'+vid+'?autoplay=1" allow="autoplay; encrypted-media" allowfullscreen></iframe>';
document.getElementById('videoModal').style.display = 'flex';
}
function closeVideoModal(){
document.getElementById('vmFrame').innerHTML = '';
document.getElementById('videoModal').style.display = 'none';
}
document.addEventListener('keydown', e => { if(e.key === 'Escape'){ closeHelp(); closeVideoModal(); } });
loadBoard();
/*]]>*/

View File

@ -97,11 +97,12 @@
style="border-bottom: 1px solid var(--glass-border); transition: background 0.2s;">
<td class="p-4">
<div class="flex items-center gap-4">
<a th:href="'https://www.youtube.com/watch?v=' + ${video.videoId}" target="_blank"
class="block relative group">
<div class="block relative group" style="cursor:pointer;" title="미리보기"
th:attr="data-vid=${video.videoId},data-title=${video.title}"
onclick="openVideoModal(this.dataset.vid, this.dataset.title)">
<img th:src="${video.thumbnailUrl}" alt="Thumb"
style="width: 120px; height: 68px; object-fit: cover; border-radius: 6px;">
</a>
</div>
<a th:href="'https://www.youtube.com/watch?v=' + ${video.videoId}" target="_blank"
class="font-bold text-sm hover:text-[var(--primary)] transition-colors"
style="max-width: 400px; line-height: 1.4;" th:text="${video.title}">Video Title</a>
@ -337,8 +338,32 @@
finally { btn.disabled = false; btn.innerHTML = orig; if (window.lucide) lucide.createIcons(); }
}
// ----- 영상 미리보기 -----
function openVideoModal(vid, title){
document.getElementById('vmTitle').textContent = title || '미리보기';
document.getElementById('vmFrame').innerHTML = '<iframe src="https://www.youtube.com/embed/'+vid+'?autoplay=1" allow="autoplay; encrypted-media" allowfullscreen></iframe>';
document.getElementById('videoModal').style.display = 'flex';
}
function closeVideoModal(){
document.getElementById('vmFrame').innerHTML = '';
document.getElementById('videoModal').style.display = 'none';
}
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeVideoModal(); });
loadGrowth();
</script>
<!-- 영상 미리보기 모달 -->
<div id="videoModal" style="display:none; position:fixed; z-index:1000; inset:0; background:rgba(0,0,0,0.85); align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this) closeVideoModal()">
<div style="background:var(--surface); border:1px solid var(--border); border-radius:12px; overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:14px; padding:11px 14px; border-bottom:1px solid var(--border);">
<span id="vmTitle" class="text-sm font-bold truncate" style="max-width:300px;">미리보기</span>
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size:24px; line-height:1; background:none; border:none; cursor:pointer;">&times;</button>
</div>
<div id="vmFrame" style="width:min(360px,90vw); aspect-ratio:9/16; max-height:78vh; background:#000;"></div>
</div>
</div>
<style>#vmFrame iframe { width:100%; height:100%; border:0; display:block; }</style>
</div>
</body>

View File

@ -77,15 +77,16 @@
<td class="p-4" data-label="Video">
<div class="flex items-center gap-4">
<!-- Thumbnail (toggleable) -->
<a th:href="${video.videoUrl}" target="_blank"
class="block relative group thumbnail-item" style="display: none;">
<div class="block relative group thumbnail-item" style="display: none; cursor:pointer;" title="미리보기"
th:attr="data-url=${video.videoUrl},data-title=${video.title}"
onclick="openVideoModalUrl(this.dataset.url, this.dataset.title)">
<img th:src="${video.thumbnailUrl}" alt="Thumb"
style="width: 120px; height: 68px; object-fit: cover; border-radius: 6px;">
<div class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
style="border-radius: 6px;">
<i data-lucide="external-link" class="text-white w-6 h-6"></i>
<i data-lucide="play" class="text-white w-6 h-6"></i>
</div>
</div>
</a>
<div class="flex flex-col gap-1">
<a th:href="${video.videoUrl}" target="_blank"
class="font-bold text-sm hover:text-[var(--primary)] transition-colors"
@ -1006,7 +1007,38 @@
alert('Error saving script.');
}
}
// ----- 영상 미리보기 (videoUrl → id 추출) -----
function ytId(url){
if(!url) return null;
const m = String(url).match(/(?:v=|\/embed\/|youtu\.be\/|\/shorts\/)([0-9A-Za-z_-]{11})/);
return m ? m[1] : null;
}
function openVideoModalUrl(url, title){
const id = ytId(url);
if(!id){ window.open(url, '_blank'); return; } // id 못찾으면 새 탭으로 폴백
document.getElementById('vmTitle').textContent = title || '미리보기';
document.getElementById('vmFrame').innerHTML = '<iframe src="https://www.youtube.com/embed/'+id+'?autoplay=1" allow="autoplay; encrypted-media" allowfullscreen></iframe>';
document.getElementById('videoModal').style.display = 'flex';
}
function closeVideoModal(){
document.getElementById('vmFrame').innerHTML = '';
document.getElementById('videoModal').style.display = 'none';
}
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeVideoModal(); });
</script>
<!-- 영상 미리보기 모달 -->
<div id="videoModal" style="display:none; position:fixed; z-index:1000; inset:0; background:rgba(0,0,0,0.85); align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this) closeVideoModal()">
<div style="background:var(--surface); border:1px solid var(--border); border-radius:12px; overflow:hidden;">
<div style="display:flex; justify-content:space-between; align-items:center; gap:14px; padding:11px 14px; border-bottom:1px solid var(--border);">
<span id="vmTitle" class="text-sm font-bold truncate" style="max-width:300px;">미리보기</span>
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size:24px; line-height:1; background:none; border:none; cursor:pointer;">&times;</button>
</div>
<div id="vmFrame" style="width:min(360px,90vw); aspect-ratio:9/16; max-height:78vh; background:#000;"></div>
</div>
</div>
<style>#vmFrame iframe { width:100%; height:100%; border:0; display:block; }</style>
</div>
</body>