@ -1,308 +1,76 @@
<!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 >
< head > < title > h-lab - 소재 데스크< / title > < / head >
< body >
< div layout:fragment = "content" >
< div class = "fd-dash" >
< div class = "fd-masthead" >
< div layout:fragment = "content" >
< main class = "md-desk" >
< header class = "md-head" >
< div >
< div class = "fd-tick" style = "margin-bottom:8px;" > 지금 할 일 중심 · 수집 → 큐레이션 → 재가공< / div >
< h1 > 대시보드< / h1 >
< / div >
< div class = "fd-meta" >
< div class = "fd-tick" > PIPELINE STATUS< / div >
< div class = "fd-big" id = "fdDate" > —< / div >
< button class = "fd-btn fd-sm" onclick = "loadDashboard()" style = "margin-top:8px;" >
< i data-lucide = "refresh-cw" style = "width:13px;" > < / i > 새로고침
< / button >
< 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 >
< div class = "fd-body fd-stagger" >
<!-- ACTION BAR -->
< section class = "fd-actions" style = "animation-delay:.02s" >
< a class = "fd-act fd-hot" th:href = "@{/board}" >
< div class = "fd-act-top" >
< div class = "fd-num" id = "aNew" > – < / div >
< div class = "fd-act-lab" > < div class = "t" > 미검토 영상< / div > < div class = "s" id = "aNewSub" > NEW · 검토 대기< / div > < / div >
< / div >
< div class = "fd-cta-row" > < span class = "fd-btn fd-sig" > 검토 시작 →< / span > < / div >
< / a >
< a class = "fd-act" th:href = "@{/discover}" >
< div class = "fd-act-top" >
< div class = "fd-num" id = "aOut" style = "color:var(--fd-signal)" > – < / div >
< div class = "fd-act-lab" > < div class = "t" > 떡상 후보< / div > < div class = "s" id = "aOutSub" > 배율 상위< / div > < / div >
< / div >
< div class = "fd-cta-row" > < span class = "fd-btn" > 발굴 열기 →< / span > < / div >
< / a >
< a class = "fd-act" th:href = "@{/collection(status='TARGET')}" >
< div class = "fd-act-top" >
< div class = "fd-num" id = "aTarget" style = "color:var(--fd-ink-3)" > – < / div >
< div class = "fd-act-lab" > < div class = "t" > 재가공 대기< / div > < div class = "s" > TARGET< / div > < / div >
< / div >
< div class = "fd-muted-cta" id = "aTargetCta" > 발굴에서 후보를 TARGET으로 보내세요 →< / div >
< 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 >
<!-- WORK SURFACE -->
< section class = "fd-cols" style = "animation-delay:.1s" >
< div class = "fd-panel" >
< div class = "fd-ph" > < h3 > 떡상 후보 — 바로 처리< / h3 > < a class = "fd-tick" style = "color:var(--fd-signal)" th:href = "@{/discover}" > 발굴 전체 →< / a > < / div >
< div class = "fd-pb" id = "leadList" > < div class = "fd-loading" > 불러오는 중…< / div > < / div >
< / div >
< div class = "fd-panel" >
< div class = "fd-ph" > < h3 > 파이프라인< / h3 > < span class = "fd-tick" id = "pipeTotal" > 총 —< / span > < / div >
< div class = "fd-pb" id = "funnel" > < div class = "fd-loading" > 불러오는 중…< / div > < / div >
< div class = "fd-ph" style = "border-top:1.5px solid var(--fd-rule-strong)" > < h3 style = "font-size:13px" > 출처 · 포맷< / h3 > < a class = "fd-tick" th:href = "@{/collection}" > 수집함 →< / a > < / div >
< div class = "fd-pb" id = "srcFmt" > < / div >
< 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 >
< / div >
<!-- 영상 미리보기 모달 -->
< div id = "videoModal" class = "fd-vmodal" onclick = "if(event.target===this) closeVideoModal()" >
< div class = "fd-vmodal-card" >
< div class = "fd-vmodal-head" >
< span class = "fd-vmodal-title" id = "vmTitle" > 미리보기< / span >
< button class = "fd-vmodal-x" onclick = "closeVideoModal()" aria-label = "닫기" > × < / button >
< / div >
< div id = "vmFrame" class = "fd-vmodal-frame" > < / div >
< / div >
< / div >
< / div >
< footer class = "md-foot" >
< span > 조회수 현행화는 YouTube videos.list를 50개씩 묶어 호출합니다.< / span >
< a href = "/board" > 선택한 소재 검토하기 →< / a >
< / footer >
< / main >
< style >
/* ===== Editorial identity — scoped to .fd-dash ===== */
.fd-dash{
--fd-paper:#f1ede3; --fd-panel:#fbfaf6; --fd-ink:#17160f; --fd-ink-2:#6f6a5c; --fd-ink-3:#a8a293;
--fd-rule:#ddd7c8; --fd-rule-strong:#17160f; --fd-signal:#ff4d23; --fd-signal-soft:#ffe6df; --fd-lime:#1f7a3a;
--fd-disp:'Bricolage Grotesque', sans-serif; --fd-body:'Hanken Grotesk', sans-serif; --fd-mono:'JetBrains Mono', monospace;
font-family:var(--fd-body); color:var(--fd-ink);
margin:-2.5rem; padding:0; min-height:100vh; background:var(--fd-paper); position:relative;
}
:root[data-theme="dark"] .fd-dash{
--fd-paper:#100f0c; --fd-panel:#191712; --fd-ink:#eceae2; --fd-ink-2:#a39d8d; --fd-ink-3:#6b6657;
--fd-rule:#2b281f; --fd-rule-strong:#3a362a;
}
.fd-dash::before{ content:""; position:absolute; inset:0; pointer-events:none; opacity:.05;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
.fd-dash a{ text-decoration:none; color:inherit; }
.fd-tick{ font-family:var(--fd-mono); font-size:10.5px; font-weight:500; letter-spacing:.18em; text-transform:uppercase; color:var(--fd-ink-3); }
.fd-masthead{ display:flex; justify-content:space-between; align-items:flex-end; padding:24px 34px 16px; border-bottom:1.5px solid var(--fd-rule-strong); position:relative; }
.fd-masthead h1{ font-family:var(--fd-disp); font-weight:800; font-size:32px; letter-spacing:-.025em; line-height:.95; }
.fd-meta{ text-align:right; }
.fd-meta .fd-big{ font-family:var(--fd-mono); font-weight:700; font-size:13px; }
.fd-body{ padding:24px 34px 40px; position:relative; }
.fd-stagger > *{ opacity:0; transform:translateY(10px); animation:fdRise .7s cubic-bezier(.2,.7,.2,1) forwards; }
@keyframes fdRise{ to{ opacity:1; transform:none; } }
@media (prefers-reduced-motion: reduce){ .fd-stagger > *{ animation:none; opacity:1; transform:none; } }
.fd-btn{ display:inline-flex; align-items:center; gap:7px; font-family:var(--fd-mono); font-weight:700; font-size:11.5px; letter-spacing:.04em; padding:8px 13px; border:1.5px solid var(--fd-rule-strong); background:var(--fd-panel); color:var(--fd-ink); cursor:pointer; }
.fd-btn:hover{ background:var(--fd-ink); color:var(--fd-paper); }
.fd-btn.fd-sig{ background:var(--fd-signal); border-color:var(--fd-signal); color:#fff; }
.fd-btn.fd-sig:hover{ background:#e23c14; color:#fff; }
.fd-btn.fd-sm{ padding:5px 9px; font-size:10.5px; }
.fd-actions{ display:grid; grid-template-columns:1.3fr 1fr 1fr; border:1.5px solid var(--fd-rule-strong); background:var(--fd-panel); margin-bottom:22px; }
.fd-act{ padding:20px 22px; border-right:1.5px solid var(--fd-rule-strong); display:flex; flex-direction:column; gap:12px; position:relative; overflow:hidden; }
.fd-act:last-child{ border-right:none; }
.fd-act:hover{ background:#f3eee2; }
:root[data-theme="dark"] .fd-act:hover{ background:#211e17; }
.fd-act-top{ display:flex; align-items:baseline; gap:12px; }
.fd-num{ font-family:var(--fd-disp); font-weight:800; font-size:60px; line-height:.8; letter-spacing:-.04em; font-variant-numeric:tabular-nums; }
.fd-act-lab .t{ font-weight:700; font-size:14px; }
.fd-act-lab .s{ font-family:var(--fd-mono); font-size:10.5px; color:var(--fd-ink-3); margin-top:3px; }
.fd-hot::after{ content:""; position:absolute; right:-60px; top:-60px; width:200px; height:200px; border-radius:50%; background:radial-gradient(circle, rgba(255,77,35,.14), transparent 62%); }
.fd-cta-row{ display:flex; gap:8px; z-index:1; }
.fd-muted-cta{ font-family:var(--fd-mono); font-size:10.5px; color:var(--fd-ink-3); line-height:1.5; }
.fd-cols{ display:grid; grid-template-columns:1.55fr 1fr; gap:22px; align-items:start; }
.fd-panel{ border:1.5px solid var(--fd-rule-strong); background:var(--fd-panel); }
.fd-ph{ display:flex; justify-content:space-between; align-items:center; padding:13px 20px; border-bottom:1.5px solid var(--fd-rule-strong); }
.fd-ph h3{ font-family:var(--fd-disp); font-weight:700; font-size:15px; letter-spacing:-.01em; }
.fd-pb{ padding:4px 20px 12px; }
.fd-loading{ font-family:var(--fd-mono); font-size:11px; color:var(--fd-ink-3); padding:20px 0; text-align:center; }
.fd-lead{ display:grid; grid-template-columns:30px 52px 1fr auto auto; gap:13px; align-items:center; padding:11px 0; border-bottom:1px solid var(--fd-rule); }
.fd-lead:last-child{ border-bottom:none; }
.fd-lead:hover{ background:#f3eee2; }
:root[data-theme="dark"] .fd-lead:hover{ background:#211e17; }
.fd-rk{ font-family:var(--fd-disp); font-weight:800; font-size:23px; line-height:1; font-variant-numeric:tabular-nums; }
.fd-lead:first-child .fd-rk{ color:var(--fd-signal); }
.fd-lead .fd-th{ width:52px; height:30px; object-fit:cover; background:#e7e1d2; border:1px solid var(--fd-rule); }
.fd-lead .fd-t{ font-weight:600; font-size:12.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:230px; }
.fd-lead .fd-s{ font-family:var(--fd-mono); font-size:10px; color:var(--fd-ink-3); margin-top:2px; }
.fd-ratio{ font-family:var(--fd-disp); font-weight:800; font-size:17px; color:var(--fd-signal); font-variant-numeric:tabular-nums; }
.fd-actcell{ display:flex; gap:6px; opacity:.4; transition:opacity .15s; }
.fd-lead:hover .fd-actcell{ opacity:1; }
.fd-fbar{ margin-bottom:11px; }
.fd-fbar .top{ display:flex; justify-content:space-between; align-items:baseline; margin-bottom:5px; }
.fd-fbar .top .k{ font-size:12px; font-weight:600; }
.fd-fbar .top .v{ font-family:var(--fd-mono); font-size:11px; color:var(--fd-ink-2); }
.fd-fbar .track{ height:8px; background:var(--fd-rule); border:1px solid var(--fd-rule); position:relative; }
.fd-fbar .track > i{ position:absolute; left:0; top:0; bottom:0; background:var(--fd-ink); }
.fd-fbar.fd-sigbar .track > i{ background:var(--fd-signal); }
.fd-bottleneck{ display:flex; justify-content:space-between; align-items:center; margin-top:12px; padding-top:12px; border-top:1px dashed var(--fd-rule-strong); }
.fd-bottleneck .msg{ font-family:var(--fd-mono); font-size:10.5px; color:var(--fd-ink-2); }
.fd-bottleneck .msg b{ color:var(--fd-signal); }
.fd-row{ display:flex; justify-content:space-between; align-items:center; padding:9px 0; border-bottom:1px solid var(--fd-rule); font-size:12.5px; }
.fd-row:last-child{ border-bottom:none; }
.fd-row .v{ font-family:var(--fd-mono); color:var(--fd-ink-2); }
.fd-row .v b{ color:var(--fd-ink); }
.fd-empty{ font-family:var(--fd-mono); font-size:11px; color:var(--fd-ink-3); padding:14px 0; }
/* 영상 미리보기 모달 */
.fd-vmodal{ display:none; position:fixed; inset:0; z-index:1000; background:rgba(10,10,12,.86); align-items:center; justify-content:center; padding:20px; }
.fd-vmodal.open{ display:flex; }
.fd-vmodal-card{ background:var(--fd-panel); border:1.5px solid var(--fd-rule-strong); box-shadow:0 20px 60px rgba(0,0,0,.4); }
.fd-vmodal-head{ display:flex; justify-content:space-between; align-items:center; gap:14px; padding:11px 14px; border-bottom:1.5px solid var(--fd-rule-strong); }
.fd-vmodal-title{ font-weight:600; font-size:13px; color:var(--fd-ink); max-width:300px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.fd-vmodal-x{ background:none; border:none; font-size:24px; line-height:1; cursor:pointer; color:var(--fd-ink-2); padding:0 2px; }
.fd-vmodal-x:hover{ color:var(--fd-ink); }
.fd-vmodal-frame{ width:min(360px,90vw); aspect-ratio:9/16; max-height:78vh; background:#000; }
.fd-vmodal-frame iframe{ width:100%; height:100%; border:0; display:block; }
@media (max-width:1100px){ .fd-cols{ grid-template-columns:1fr; } .fd-actions{ grid-template-columns:1fr; } .fd-act{ border-right:none; border-bottom:1.5px solid var(--fd-rule-strong); } }
@media (max-width:680px){
.fd-dash{ margin:-1.25rem; }
.fd-masthead{ flex-direction:column; align-items:stretch; gap:8px; padding:16px 16px 12px; }
.fd-masthead .fd-tick{ display:none; } /* 긴 브레드크럼/라벨 숨김 */
.fd-masthead h1{ font-size:26px; }
.fd-meta{ display:flex; align-items:center; gap:12px; }
.fd-meta .fd-big{ margin:0; }
.fd-body{ padding:16px 16px 28px; }
.fd-num{ font-size:44px; }
.fd-act{ padding:16px 16px; }
/* 리더보드 행: 배율까지 한 줄, 액션 버튼은 아랫줄 우측 정렬 */
.fd-lead{ grid-template-columns:24px 46px 1fr auto; column-gap:10px; }
.fd-lead .fd-actcell{ grid-column:1 / -1; justify-content:flex-end; opacity:1; padding-top:2px; }
.fd-lead .fd-t{ white-space:normal; max-width:none; }
}
.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 th:inline = "javascript" >
/*< ![CDATA[*/
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'& ').replace(/< /g,'< ').replace(/>/g,'> ').replace(/"/g,'" '); }
function fmt(n){ return (n==null)?'0':Number(n).toLocaleString(); }
function pct(n,t){ return t>0?Math.round((Number(n||0)/t)*100):0; }
async function getData(url, opts){
const r = await fetch(url, opts);
const j = await r.json().catch(()=>({}));
if(!r.ok || (j & & j.success===false)) throw new Error((j& & j.message)||('HTTP '+r.status));
return j.data;
}
(function(){ const d=new Date(); const p=n=>String(n).padStart(2,'0');
document.getElementById('fdDate').textContent = d.getFullYear()+' · '+p(d.getMonth()+1)+' · '+p(d.getDate()); })();
function fbar(label, count, total, signal){
const p = pct(count,total);
return `< div class = "fd-fbar ${signal?'fd-sigbar':''}" > < div class = "top" > < span class = "k" > ${esc(label)}< / span > < span class = "v" > ${fmt(count)} · ${p}%< / span > < / div > < div class = "track" > < i style = "width:${p}%" > < / i > < / div > < / div > `;
}
async function loadDashboard(){
let d;
try { d = await getData('/api/dashboard/summary'); }
catch(e){ document.getElementById('leadList').innerHTML = '< div class = "fd-empty" style = "color:var(--fd-signal)" > 불러오기 실패: '+esc(e.message)+'< / div > '; return; }
const pipe = d.pipeline||{}, bs = pipe.byStatus||{}, src = pipe.bySource||{};
const total = Number(pipe.total||0);
const op = d.outperformers||[];
// ----- 액션 바 -----
document.getElementById('aNew').textContent = fmt(bs.NEW);
document.getElementById('aNewSub').textContent = 'NEW · 검토 대기' + (bs.REVIEWING?(' · 검토중 '+fmt(bs.REVIEWING)):'');
document.getElementById('aOut').textContent = op.length ? fmt(op.length) : '0';
document.getElementById('aOutSub').textContent = op.length ? ('배율 상위 · 최고 '+(op[0].viewsPerSubRatio!=null?Number(op[0].viewsPerSubRatio).toFixed(0)+'× ':'-')) : '아직 없음';
document.getElementById('aTarget').textContent = fmt(bs.TARGET);
document.getElementById('aTargetCta').textContent = Number(bs.TARGET||0)>0 ? '재가공 작업공간에서 이어서 →' : '발굴에서 후보를 TARGET으로 보내세요 →';
// ----- 떡상 리더보드 (실데이터 + 액션) -----
const lead = document.getElementById('leadList');
if(op.length===0){ lead.innerHTML = '< div class = "fd-empty" > 아직 떡상 후보가 없습니다. 채널을 수집하면 배율 상위 영상이 여기 모입니다.< / div > '; }
else {
lead.innerHTML = op.slice(0,6).map((v,i)=>{
const ratio = v.viewsPerSubRatio!=null ? Number(v.viewsPerSubRatio).toFixed(0)+'× ' : '– ';
return `< div class = "fd-lead" data-id = "${v.id}" >
< div class = "fd-rk" > ${i+1}< / div >
< img class = "fd-th" src = "${esc(v.thumbnailUrl)}" alt = "" title = "미리보기" style = "cursor:pointer;"
data-vid="${v.videoId}" data-title="${esc(v.title)}" onclick="openVideoModal(this.dataset.vid, this.dataset.title)">
< div style = "min-width:0;" >
< div class = "fd-t" > < a href = "https://www.youtube.com/watch?v=${v.videoId}" target = "_blank" > ${esc(v.title)}< / a > < / div >
< div class = "fd-s" > SHORTS · ${esc(v.channelTitle||'')} · ${fmt(v.viewCount)}< / div >
< / div >
< div class = "fd-ratio" > ${ratio}< / div >
< div class = "fd-actcell" >
< a class = "fd-btn fd-sm fd-sig" href = "/rework/${v.id}" > 재가공 →< / a >
< button class = "fd-btn fd-sm" onclick = "excludeVideo(${v.id})" > 제외< / button >
< / div >
< / div > `;
}).join('');
}
// ----- 파이프라인 + 병목 -----
document.getElementById('pipeTotal').textContent = '총 ' + fmt(total);
const stages = [
['미검토', bs.NEW], ['검토중', bs.REVIEWING], ['작업대상', bs.TARGET],
['완료', bs.DONE]
];
let bottleneck = stages[0];
for(const s of stages){ if(Number(s[1]||0) > Number(bottleneck[1]||0)) bottleneck = s; }
document.getElementById('funnel').innerHTML =
fbar('미검토', bs.NEW, total, true) +
fbar('검토중', bs.REVIEWING, total) +
fbar('작업대상', bs.TARGET, total) +
fbar('완료', bs.DONE, total) +
`< div class = "fd-bottleneck" > < div class = "msg" > 병목: < b > ${esc(bottleneck[0])} ${fmt(bottleneck[1])}< / b > — 다음 단계로 옮겨보세요< / div > < a class = "fd-btn fd-sm fd-sig" href = "/board" > 칸반 보드< / a > < / div > `;
// ----- 출처/포맷 -----
document.getElementById('srcFmt').innerHTML =
`< div class = "fd-row" > < span > 채널 수집< / span > < span class = "v" > < b > ${fmt(src.CHANNEL)}< / b > · ${pct(src.CHANNEL,total)}%< / span > < / div > ` +
`< div class = "fd-row" > < span > 검색 수집< / span > < span class = "v" > < b > ${fmt(src.SEARCH)}< / b > · ${pct(src.SEARCH,total)}%< / span > < / div > ` +
`< div class = "fd-row" > < span > Shorts< / span > < span class = "v" > < b > ${fmt(pipe.shorts)}< / b > · ${pct(pipe.shorts,total)}%< / span > < / div > `;
if(window.lucide) lucide.createIcons();
}
async function excludeVideo(id){
if(!confirm('이 영상을 제외(EXCLUDED) 처리할까요? 떡상 후보에서 사라집니다.')) return;
try {
await getData('/api/v1/channel-videos/'+id+'/status', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({status:'EXCLUDED'}) });
< script >
const fmt=n=>Number(n||0).toLocaleString();
const esc=s=>String(s||'').replace(/&/g,'& ').replace(/< /g,'< ').replace(/>/g,'> ').replace(/"/g,'" ');
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();
} catch(e){ alert('제외 실패: '+e.message); }
}
// ----- 영상 미리보기 (수집함과 동일) -----
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').classList.add('open');
}
function closeVideoModal(){
document.getElementById('vmFrame').innerHTML = '';
document.getElementById('videoModal').classList.remove('open');
}
document.addEventListener('keydown', e => { if(e.key === 'Escape') closeVideoModal(); });
loadDashboard();
/*]]>*/
< / script >
< / div >
< / div >
< / body >
< / html >