h-lab/src/main/resources/templates/dashboard.html

120 lines
7.0 KiB
HTML

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/base}">
<head>
<title>h-lab - Dashboard</title>
</head>
<body>
<div layout:fragment="content">
<header class="mb-4">
<h1 class="text-xl font-bold mb-4">Dashboard</h1>
<p class="text-muted">Overview of your YouTube analytics and tracking.</p>
</header>
<!-- Stats Grid -->
<div class="flex gap-4 mb-4"
style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
<div class="card flex flex-col justify-between">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-sm text-muted">수집 영상</p>
<h3 class="text-xl font-bold" id="statTotal">-</h3>
</div>
<div style="padding: 0.5rem; border-radius: 8px; background: var(--bg-hover);">
<i data-lucide="library" color="var(--primary)"></i>
</div>
</div>
<div class="text-sm text-muted" id="statSourceCaption">채널 + 검색 수집</div>
</div>
<div class="card flex flex-col justify-between">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-sm text-muted">작업대상 (TARGET)</p>
<h3 class="text-xl font-bold" id="statTarget">-</h3>
</div>
<div style="padding: 0.5rem; border-radius: 8px; background: var(--bg-hover);">
<i data-lucide="clapperboard" color="var(--primary)"></i>
</div>
</div>
<div class="text-sm text-muted" id="statReviewCaption">검토중 -</div>
</div>
<div class="card flex flex-col justify-between">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-sm text-muted">미검토 (NEW)</p>
<h3 class="text-xl font-bold" id="statNew">-</h3>
</div>
<div style="padding: 0.5rem; border-radius: 8px; background: var(--bg-hover);">
<i data-lucide="inbox" color="var(--primary)"></i>
</div>
</div>
<div class="text-sm text-muted" id="statExcludedCaption">제외 -</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
<div class="card">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold">🚀 떡상 후보 TOP</h3>
<a th:href="@{/collection}" class="text-sm text-muted hover:text-white">수집함 →</a>
</div>
<div class="flex flex-col gap-2" id="opList">
<p class="text-muted text-sm">로딩 중...</p>
</div>
</div>
<div class="card">
<h3 class="text-lg font-bold mb-4">수집 출처</h3>
<div class="flex flex-col gap-2" id="sourceList">
<p class="text-muted text-sm">로딩 중...</p>
</div>
</div>
</div>
<script th:inline="javascript">
/*<![CDATA[*/
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
async function getData(url){ const r = await fetch(url); const j = await r.json().catch(()=>({})); return j.data; }
(async ()=>{
try {
const s = await getData('/api/v1/channel-videos/stats');
if(s){
document.getElementById('statTotal').textContent = Number(s.total||0).toLocaleString();
const bs = s.byStatus||{}, src = s.bySource||{};
document.getElementById('statTarget').textContent = Number(bs.TARGET||0).toLocaleString();
document.getElementById('statNew').textContent = Number(bs.NEW||0).toLocaleString();
document.getElementById('statReviewCaption').textContent = '검토중 ' + (bs.REVIEWING||0);
document.getElementById('statExcludedCaption').textContent = '제외 ' + (bs.EXCLUDED||0);
document.getElementById('statSourceCaption').textContent = '채널 ' + (src.CHANNEL||0) + ' · 검색 ' + (src.SEARCH||0);
document.getElementById('sourceList').innerHTML =
'<div class="flex items-center justify-between p-3" style="border-radius:var(--radius-md); background:rgba(255,255,255,0.03);"><span class="text-sm">채널 수집</span><span class="text-sm font-bold">'+(src.CHANNEL||0)+'</span></div>' +
'<div class="flex items-center justify-between p-3" style="border-radius:var(--radius-md); background:rgba(255,255,255,0.03);"><span class="text-sm">검색 수집</span><span class="text-sm font-bold">'+(src.SEARCH||0)+'</span></div>';
}
} catch(e){ console.error(e); }
try {
const op = await getData('/api/v1/channel-videos/outperformers?limit=5&minRatio=1') || [];
const list = document.getElementById('opList');
if(op.length===0){ list.innerHTML = '<p class="text-muted text-sm">아직 떡상 후보가 없습니다. 채널을 수집해 보세요.</p>'; return; }
list.innerHTML = op.map(v=>{
const ratio = v.viewsPerSubRatio!=null ? Number(v.viewsPerSubRatio).toFixed(1)+'x' : '-';
return '<a href="https://www.youtube.com/watch?v='+v.videoId+'" target="_blank" class="flex items-center justify-between p-3" style="border-radius:var(--radius-md); background:rgba(255,255,255,0.03); text-decoration:none;">' +
'<div class="flex items-center gap-2" style="min-width:0;">' +
'<img src="'+esc(v.thumbnailUrl)+'" style="width:48px;height:27px;object-fit:cover;border-radius:4px;flex-shrink:0;">' +
'<div style="min-width:0;"><div class="text-sm font-bold truncate" style="max-width:280px;">'+esc(v.title)+'</div>' +
'<div class="text-muted" style="font-size:0.7rem;">'+esc(v.channelTitle||'')+' · 조회 '+Number(v.viewCount||0).toLocaleString()+'</div></div>' +
'</div>' +
'<span style="color:#ef4444; font-weight:bold; font-size:0.8rem; flex-shrink:0;">'+ratio+'</span></a>';
}).join('');
} catch(e){ console.error(e); }
})();
/*]]>*/
</script>
</div>
</body>
</html>