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

77 lines
9.1 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 - 소재 데스크</title></head>
<body>
<div layout:fragment="content">
<main class="md-desk">
<header class="md-head">
<div>
<span class="md-kicker">MATERIAL DESK · 최근 14일</span>
<h1>오늘 만들 소재</h1>
<p>업로드 시점보다 지금 오르는 속도를 먼저 봅니다.</p>
</div>
<div class="md-actions">
<span id="updatedAt">조회 중</span>
<button class="md-btn" id="refreshViewsBtn" onclick="refreshViews()">조회수 현행화</button>
<button class="md-btn ghost" onclick="loadDashboard()">화면 새로고침</button>
</div>
</header>
<section class="md-score" aria-label="소재 요약">
<a href="/feed" class="md-score-main">
<span>검토할 소재</span><strong id="sUnworked">—</strong><small>아직 손대지 않은 원본</small>
</a>
<div class="md-metric hot"><span>24시간 이내</span><strong id="sGolden">—</strong><small>골든타임</small></div>
<div class="md-metric"><span>예능</span><strong id="sEntertainment">—</strong><small>최근 14일</small></div>
<div class="md-metric"><span>정치</span><strong id="sPolitics">—</strong><small>최근 14일</small></div>
<div class="md-metric"><span>북마크</span><strong id="sBookmarked">—</strong><small>다시 볼 소재</small></div>
</section>
<section class="md-board">
<div class="md-board-head">
<div><span class="md-kicker">VELOCITY RANK</span><h2>지금 빠르게 오르는 소재</h2></div>
<div class="md-tabs"><a href="/feed">예능 피드</a><a href="/politics-feed">정치 피드</a></div>
</div>
<div class="md-columns" aria-hidden="true"><span>순위 / 영상</span><span>업로드</span><span>조회수</span><span>시간당</span></div>
<div id="materialList" class="md-list"><div class="md-empty">소재 데이터를 불러오는 중입니다.</div></div>
</section>
<footer class="md-foot">
<span>조회수 현행화는 YouTube videos.list를 50개씩 묶어 호출합니다.</span>
<a href="/board">선택한 소재 검토하기 →</a>
</footer>
</main>
<style>
.md-desk{--navy:#102a43;--blue:#176b87;--cyan:#34b3c0;--ice:#e8f4f5;--paper:#f7faf9;--line:#ccdadd;--ink:#16252d;--muted:#6c7d83;margin:-2.5rem;min-height:100vh;padding:34px;background:linear-gradient(125deg,#f8fbfa 0%,#edf5f5 62%,#e4f0f2 100%);color:var(--ink);font-family:'Hanken Grotesk',sans-serif}
:root[data-theme="dark"] .md-desk{--paper:#142228;--line:#30434a;--ink:#edf6f6;--muted:#92a6ac;--ice:#17343c;background:#0e1a20}
.md-head{display:flex;justify-content:space-between;align-items:flex-end;padding-bottom:24px;border-bottom:3px solid var(--navy)}
.md-kicker{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.16em;color:var(--blue)}
.md-head h1{font-family:'Bricolage Grotesque',sans-serif;font-size:42px;line-height:1;margin:8px 0 6px;letter-spacing:-.04em}.md-head p{color:var(--muted);margin:0}
.md-actions{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted)}
.md-btn{border:1px solid var(--navy);background:var(--navy);color:white;padding:9px 13px;font-weight:700;cursor:pointer}.md-btn.ghost{background:transparent;color:var(--ink)}.md-btn:disabled{opacity:.5;cursor:wait}
.md-score{display:grid;grid-template-columns:1.45fr repeat(4,1fr);margin:24px 0;border:1px solid var(--navy);background:var(--paper)}
.md-score>a{text-decoration:none;color:inherit}.md-score-main,.md-metric{padding:20px;border-right:1px solid var(--line);display:flex;flex-direction:column}.md-metric:last-child{border-right:0}.md-score-main{background:var(--navy);color:white}.md-score span{font-size:12px;font-weight:700}.md-score strong{font-family:'Bricolage Grotesque',sans-serif;font-size:38px;line-height:1;margin:12px 0 7px}.md-score small{color:var(--muted)}.md-score-main small{color:#bdd3dc}.md-metric.hot strong{color:#e85636}
.md-board{border:1px solid var(--navy);background:var(--paper)}.md-board-head{display:flex;justify-content:space-between;align-items:flex-end;padding:18px 20px;border-bottom:1px solid var(--navy)}.md-board-head h2{font-size:20px;margin:5px 0 0}.md-tabs{display:flex;gap:14px}.md-tabs a{font-size:12px;font-weight:700;color:var(--blue)}
.md-columns,.md-row{display:grid;grid-template-columns:minmax(320px,1fr) 110px 110px 110px;gap:14px;align-items:center}.md-columns{padding:8px 20px;background:var(--ice);font:10px 'JetBrains Mono',monospace;color:var(--muted);text-transform:uppercase}.md-row{padding:12px 20px;border-top:1px solid var(--line);text-decoration:none;color:inherit}.md-row:hover{background:var(--ice)}
.md-video{display:grid;grid-template-columns:28px 96px minmax(0,1fr);gap:12px;align-items:center}.md-rank{font:800 20px 'Bricolage Grotesque',sans-serif;color:var(--blue)}.md-thumb{width:96px;aspect-ratio:16/9;object-fit:cover}.md-title{font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.md-sub{font-size:11px;color:var(--muted);margin-top:4px}.md-cell{font:12px 'JetBrains Mono',monospace}.md-speed{font-weight:800;color:var(--blue)}.md-tag{display:inline-block;padding:2px 6px;margin-right:6px;background:var(--ice);color:var(--blue);font-size:10px;font-weight:800}.md-tag.politics{background:#f4e9e5;color:#a1442e}
.md-empty{padding:36px;text-align:center;color:var(--muted)}.md-foot{display:flex;justify-content:space-between;padding:14px 2px;color:var(--muted);font-size:11px}.md-foot a{color:var(--blue);font-weight:700}
@media(max-width:900px){.md-desk{margin:-1.25rem;padding:18px}.md-head{align-items:flex-start;gap:18px}.md-actions{flex-wrap:wrap;justify-content:flex-end}.md-score{grid-template-columns:repeat(2,1fr)}.md-score-main{grid-column:1/-1}.md-metric{border-top:1px solid var(--line)}.md-columns{display:none}.md-row{grid-template-columns:1fr 90px}.md-row .md-cell:nth-child(2),.md-row .md-cell:nth-child(3){display:none}}
@media(max-width:600px){.md-head{display:block}.md-head h1{font-size:34px}.md-actions{margin-top:16px;justify-content:flex-start}.md-score{grid-template-columns:1fr 1fr}.md-video{grid-template-columns:24px 72px minmax(0,1fr)}.md-thumb{width:72px}.md-row{padding:11px}.md-foot{display:block}.md-foot a{display:block;margin-top:8px}}
</style>
<script>
const fmt=n=>Number(n||0).toLocaleString();
const esc=s=>String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
const ago=iso=>{if(!iso)return'-';const h=Math.max(0,Math.floor((Date.now()-new Date(iso))/36e5));return h<24?h+'시간 전':Math.floor(h/24)+'일 전'};
async function api(url,opts){const r=await fetch(url,opts);const j=await r.json();if(!r.ok||j.success===false)throw new Error(j.message||('HTTP '+r.status));return j.data}
function renderRow(v,i){const topic=v.topic==='POLITICS'?'정치':'예능';const cls=v.topic==='POLITICS'?' politics':'';const href=v.topic==='POLITICS'?'/politics-feed':'/feed';return `<a class="md-row" href="${href}"><div class="md-video"><span class="md-rank">${String(i+1).padStart(2,'0')}</span><img class="md-thumb" src="${esc(v.thumbnailUrl)}" alt=""><div><div class="md-title">${esc(v.title)}</div><div class="md-sub"><span class="md-tag${cls}">${topic}</span>${esc(v.channelTitle||'')} ${v.goldenTime?'· 지금 선점':''}</div></div></div><span class="md-cell">${ago(v.publishedAt)}</span><span class="md-cell">${fmt(v.viewCount)}</span><span class="md-cell md-speed">${fmt(Math.round(Number(v.viewsPerHour||0)))}/h</span></a>`}
async function loadDashboard(){try{const d=await api('/api/dashboard/summary');const f=d.feedDesk||{};sUnworked.textContent=fmt(f.unworked);sGolden.textContent=fmt(f.golden);sEntertainment.textContent=fmt(f.entertainment);sPolitics.textContent=fmt(f.politics);sBookmarked.textContent=fmt(f.bookmarked);materialList.innerHTML=(f.top||[]).length?(f.top||[]).map(renderRow).join(''):'<div class="md-empty">최근 14일 소재가 없습니다. 소재 피드에서 수집을 실행하세요.</div>';updatedAt.textContent='화면 갱신 '+new Date().toLocaleTimeString('ko-KR',{hour:'2-digit',minute:'2-digit'});}catch(e){materialList.innerHTML='<div class="md-empty">불러오기 실패 · '+esc(e.message)+'</div>'}}
async function refreshViews(){const b=refreshViewsBtn;b.disabled=true;b.textContent='조회수 갱신 중';try{const [e,p]=await Promise.all([api('/api/feed/refresh-views?topic=ENTERTAINMENT',{method:'POST'}),api('/api/feed/refresh-views?topic=POLITICS',{method:'POST'})]);updatedAt.textContent=`${fmt(e.updated+p.updated)}개 갱신 완료`;await loadDashboard()}catch(err){alert('조회수 갱신 실패: '+err.message)}finally{b.disabled=false;b.textContent='조회수 현행화'}}
loadDashboard();
</script>
</div>
</body>
</html>