feat(dashboard): count-up, 3-tone bars, rank/ratio badges, click-through deep links
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5ec04eaa60
commit
2d6c962567
@ -22,41 +22,41 @@
|
|||||||
|
|
||||||
<!-- KPI 카드 -->
|
<!-- KPI 카드 -->
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.25rem; margin-bottom: 1.5rem;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1.25rem; margin-bottom: 1.5rem;">
|
||||||
<div class="card flex flex-col justify-between">
|
<a th:href="@{/collection}" class="card flex flex-col justify-between cursor-pointer" style="text-decoration:none;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div><p class="text-sm text-muted">수집 영상</p><h3 class="text-xl font-bold" id="kTotal">-</h3></div>
|
<div><p class="text-sm text-muted">수집 영상</p><h3 class="text-2xl font-bold" id="kTotal">-</h3></div>
|
||||||
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover);"><i data-lucide="library" color="var(--primary)"></i></div>
|
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover, rgba(255,255,255,0.06));"><i data-lucide="library" color="var(--primary)"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted" id="kTotalCap">채널 - · 검색 -</div>
|
<div class="text-sm text-muted" id="kTotalCap">채널 - · 검색 -</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="card flex flex-col justify-between">
|
<a th:href="@{/collection(status='NEW')}" class="card flex flex-col justify-between cursor-pointer" style="text-decoration:none;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div><p class="text-sm text-muted">미검토 (NEW)</p><h3 class="text-xl font-bold" id="kNew">-</h3></div>
|
<div><p class="text-sm text-muted">미검토 (NEW)</p><h3 class="text-2xl font-bold" id="kNew">-</h3></div>
|
||||||
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover);"><i data-lucide="inbox" color="var(--primary)"></i></div>
|
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover, rgba(255,255,255,0.06));"><i data-lucide="inbox" color="var(--primary)"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted" id="kReviewCap">검토중 -</div>
|
<div class="text-sm text-muted" id="kReviewCap">검토중 -</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="card flex flex-col justify-between">
|
<a th:href="@{/collection(status='TARGET')}" class="card flex flex-col justify-between cursor-pointer" style="text-decoration:none;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div><p class="text-sm text-muted">작업대상 (TARGET)</p><h3 class="text-xl font-bold" id="kTarget">-</h3></div>
|
<div><p class="text-sm text-muted">작업대상 (TARGET)</p><h3 class="text-2xl font-bold" id="kTarget">-</h3></div>
|
||||||
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover);"><i data-lucide="clapperboard" color="var(--primary)"></i></div>
|
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover, rgba(255,255,255,0.06));"><i data-lucide="clapperboard" color="var(--primary)"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted">재가공 대상</div>
|
<div class="text-sm text-muted">재가공 대상</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="card flex flex-col justify-between">
|
<a th:href="@{/collection(status='DONE')}" class="card flex flex-col justify-between cursor-pointer" style="text-decoration:none;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div><p class="text-sm text-muted">완료 (DONE)</p><h3 class="text-xl font-bold" id="kDone">-</h3></div>
|
<div><p class="text-sm text-muted">완료 (DONE)</p><h3 class="text-2xl font-bold" id="kDone">-</h3></div>
|
||||||
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover);"><i data-lucide="check-circle" color="var(--primary)"></i></div>
|
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover, rgba(255,255,255,0.06));"><i data-lucide="check-circle" color="var(--primary)"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted" id="kExcludedCap">제외 -</div>
|
<div class="text-sm text-muted" id="kExcludedCap">제외 -</div>
|
||||||
</div>
|
</a>
|
||||||
<div class="card flex flex-col justify-between">
|
<a th:href="@{/publish}" class="card flex flex-col justify-between cursor-pointer" style="text-decoration:none;">
|
||||||
<div class="flex justify-between items-start mb-3">
|
<div class="flex justify-between items-start mb-3">
|
||||||
<div><p class="text-sm text-muted">발행완료</p><h3 class="text-xl font-bold" id="kPublished">-</h3></div>
|
<div><p class="text-sm text-muted">발행완료</p><h3 class="text-2xl font-bold" id="kPublished">-</h3></div>
|
||||||
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover);"><i data-lucide="send" color="var(--primary)"></i></div>
|
<div style="padding:0.5rem; border-radius:8px; background:var(--bg-hover, rgba(255,255,255,0.06));"><i data-lucide="send" color="var(--primary)"></i></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted" id="kPublishCap">대기 - · 작성중 -</div>
|
<div class="text-sm text-muted" id="kPublishCap">대기 - · 작성중 -</div>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 파이프라인 깔때기 -->
|
<!-- 파이프라인 깔때기 -->
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<span class="text-sm text-muted" id="funnelExcluded"></span>
|
<span class="text-sm text-muted" id="funnelExcluded"></span>
|
||||||
</div>
|
</div>
|
||||||
<div id="funnel" class="flex flex-col gap-3">
|
<div id="funnel" class="flex flex-col gap-3">
|
||||||
<p class="text-muted text-sm">로딩 중...</p>
|
<div class="skeleton" style="height:48px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<h3 class="text-lg font-bold">🚀 떡상 후보 TOP</h3>
|
<h3 class="text-lg font-bold">🚀 떡상 후보 TOP</h3>
|
||||||
<a th:href="@{/discover}" class="text-sm text-muted hover:text-white">발굴 →</a>
|
<a th:href="@{/discover}" class="text-sm text-muted hover:text-white">발굴 →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2" id="opList"><p class="text-muted text-sm">로딩 중...</p></div>
|
<div class="flex flex-col gap-2" id="opList"><div class="skeleton" style="height:48px;"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
@ -96,26 +96,33 @@
|
|||||||
<h3 class="text-lg font-bold">카테고리 분포</h3>
|
<h3 class="text-lg font-bold">카테고리 분포</h3>
|
||||||
<a th:href="@{/collection}" class="text-sm text-muted hover:text-white">수집함 →</a>
|
<a th:href="@{/collection}" class="text-sm text-muted hover:text-white">수집함 →</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="catList" class="flex flex-col gap-2"><p class="text-muted text-sm">로딩 중...</p></div>
|
<div id="catList" class="flex flex-col gap-2"><div class="skeleton" style="height:48px;"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="text-lg font-bold mb-4">수집 출처 & 포맷</h3>
|
<h3 class="text-lg font-bold mb-4">수집 출처 & 포맷</h3>
|
||||||
<div id="sourceFormat" class="flex flex-col gap-2"><p class="text-muted text-sm">로딩 중...</p></div>
|
<div id="sourceFormat" class="flex flex-col gap-2"><div class="skeleton" style="height:48px;"></div></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.bar-track { flex:1; height:10px; background:rgba(255,255,255,0.06); border-radius:999px; overflow:hidden; }
|
|
||||||
.bar-fill { height:100%; border-radius:999px; }
|
|
||||||
.st-badge { font-size:0.65rem; padding:1px 7px; border-radius:999px; font-weight:bold; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
/*<![CDATA[*/
|
/*<![CDATA[*/
|
||||||
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
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 fmt(n){ return (n==null) ? '0' : Number(n).toLocaleString(); }
|
||||||
function pct(n, total){ return total > 0 ? Math.round((Number(n||0)/total)*100) : 0; }
|
function pct(n, total){ return total > 0 ? Math.round((Number(n||0)/total)*100) : 0; }
|
||||||
|
|
||||||
|
function countUp(el, target){
|
||||||
|
target = Number(target||0);
|
||||||
|
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){ el.textContent = fmt(target); return; }
|
||||||
|
const dur = 600, t0 = performance.now();
|
||||||
|
function tick(now){
|
||||||
|
const p = Math.min(1, (now - t0) / dur);
|
||||||
|
const eased = 1 - Math.pow(1 - p, 3);
|
||||||
|
el.textContent = fmt(Math.round(target * eased));
|
||||||
|
if (p < 1) requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
async function getData(url){
|
async function getData(url){
|
||||||
const r = await fetch(url);
|
const r = await fetch(url);
|
||||||
const j = await r.json().catch(()=>({}));
|
const j = await r.json().catch(()=>({}));
|
||||||
@ -132,91 +139,104 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ratioBadgeClass(r){ const v=Number(r); return v>=10?'badge-danger':(v>=2?'badge-warning':'badge-success'); }
|
||||||
|
|
||||||
async function loadDashboard(){
|
async function loadDashboard(){
|
||||||
|
const refreshIcon = document.querySelector('button[onclick="loadDashboard()"] svg, button[onclick="loadDashboard()"] i');
|
||||||
|
if (refreshIcon) refreshIcon.classList.add('animate-spin');
|
||||||
let d;
|
let d;
|
||||||
try { d = await getData('/api/dashboard/summary'); }
|
try {
|
||||||
catch(e){
|
d = await getData('/api/dashboard/summary');
|
||||||
|
} catch(e) {
|
||||||
document.getElementById('funnel').innerHTML = '<p class="text-danger text-sm">불러오기 실패: '+esc(e.message)+'</p>';
|
document.getElementById('funnel').innerHTML = '<p class="text-danger text-sm">불러오기 실패: '+esc(e.message)+'</p>';
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
if (refreshIcon) refreshIcon.classList.remove('animate-spin');
|
||||||
}
|
}
|
||||||
const pipe = d.pipeline || {};
|
const pipe = d.pipeline || {};
|
||||||
const bs = pipe.byStatus || {}, src = pipe.bySource || {};
|
const bs = pipe.byStatus || {}, src = pipe.bySource || {};
|
||||||
const total = Number(pipe.total || 0);
|
const total = Number(pipe.total || 0);
|
||||||
const pub = d.publish || {}, pbs = pub.byStatus || {};
|
const pub = d.publish || {}, pbs = pub.byStatus || {};
|
||||||
|
|
||||||
// KPI
|
// KPI (count-up for numbers, textContent for captions)
|
||||||
document.getElementById('kTotal').textContent = fmt(total);
|
countUp(document.getElementById('kTotal'), total);
|
||||||
document.getElementById('kTotalCap').textContent = '채널 ' + fmt(src.CHANNEL) + ' · 검색 ' + fmt(src.SEARCH);
|
document.getElementById('kTotalCap').textContent = '채널 ' + fmt(src.CHANNEL) + ' · 검색 ' + fmt(src.SEARCH);
|
||||||
document.getElementById('kNew').textContent = fmt(bs.NEW);
|
countUp(document.getElementById('kNew'), bs.NEW);
|
||||||
document.getElementById('kReviewCap').textContent = '검토중 ' + fmt(bs.REVIEWING);
|
document.getElementById('kReviewCap').textContent = '검토중 ' + fmt(bs.REVIEWING);
|
||||||
document.getElementById('kTarget').textContent = fmt(bs.TARGET);
|
countUp(document.getElementById('kTarget'), bs.TARGET);
|
||||||
document.getElementById('kDone').textContent = fmt(bs.DONE);
|
countUp(document.getElementById('kDone'), bs.DONE);
|
||||||
document.getElementById('kExcludedCap').textContent = '제외 ' + fmt(bs.EXCLUDED);
|
document.getElementById('kExcludedCap').textContent = '제외 ' + fmt(bs.EXCLUDED);
|
||||||
document.getElementById('kPublished').textContent = fmt(pbs.PUBLISHED);
|
countUp(document.getElementById('kPublished'), pbs.PUBLISHED);
|
||||||
document.getElementById('kPublishCap').textContent = '대기 ' + fmt(pbs.READY) + ' · 작성중 ' + fmt(pbs.DRAFT);
|
document.getElementById('kPublishCap').textContent = '대기 ' + fmt(pbs.READY) + ' · 작성중 ' + fmt(pbs.DRAFT);
|
||||||
|
|
||||||
// 깔때기 (총 수집 대비)
|
// 깔때기 (총 수집 대비) — 3-tone color scheme
|
||||||
document.getElementById('funnelExcluded').textContent = '총 ' + fmt(total) + '건 · 제외 ' + fmt(bs.EXCLUDED);
|
document.getElementById('funnelExcluded').textContent = '총 ' + fmt(total) + '건 · 제외 ' + fmt(bs.EXCLUDED);
|
||||||
document.getElementById('funnel').innerHTML =
|
document.getElementById('funnel').innerHTML =
|
||||||
bar('미검토', bs.NEW, total, '#64748b') +
|
bar('미검토', bs.NEW, total, '#64748b') +
|
||||||
bar('검토중', bs.REVIEWING, total, '#38bdf8') +
|
bar('검토중', bs.REVIEWING, total, 'var(--primary)') +
|
||||||
bar('작업대상', bs.TARGET, total, '#f59e0b') +
|
bar('작업대상', bs.TARGET, total, 'var(--primary)') +
|
||||||
bar('완료', bs.DONE, total, '#10b981') +
|
bar('완료', bs.DONE, total, 'var(--success)') +
|
||||||
bar('발행완료', pbs.PUBLISHED, total, '#7C3AED');
|
bar('발행완료', pbs.PUBLISHED, total, 'var(--success)');
|
||||||
|
|
||||||
// 떡상 TOP
|
// 떡상 TOP — rank badge + ratio badge
|
||||||
const op = d.outperformers || [];
|
const op = d.outperformers || [];
|
||||||
const opList = document.getElementById('opList');
|
const opList = document.getElementById('opList');
|
||||||
if(op.length===0){ opList.innerHTML = '<p class="text-muted text-sm">아직 떡상 후보가 없습니다. 채널을 수집해 보세요.</p>'; }
|
if(op.length===0){ opList.innerHTML = '<p class="text-muted text-sm">아직 떡상 후보가 없습니다. 채널을 수집해 보세요.</p>'; }
|
||||||
else {
|
else {
|
||||||
opList.innerHTML = op.map(v=>{
|
opList.innerHTML = op.map((v,i)=>{
|
||||||
const ratio = v.viewsPerSubRatio!=null ? Number(v.viewsPerSubRatio).toFixed(1)+'x' : '-';
|
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;">' +
|
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;">' +
|
'<div class="flex items-center gap-2" style="min-width:0;">' +
|
||||||
|
'<span class="badge badge-muted" style="flex-shrink:0;">'+(i+1)+'</span>' +
|
||||||
'<img src="'+esc(v.thumbnailUrl)+'" style="width:48px;height:27px;object-fit:cover;border-radius:4px;flex-shrink: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:300px;">'+esc(v.title)+'</div>' +
|
'<div style="min-width:0;"><div class="text-sm font-bold truncate" style="max-width:300px;">'+esc(v.title)+'</div>' +
|
||||||
'<div class="text-muted" style="font-size:0.7rem;">'+esc(v.channelTitle||'')+' · 조회 '+fmt(v.viewCount)+'</div></div>' +
|
'<div class="text-muted" style="font-size:0.7rem;">'+esc(v.channelTitle||'')+' · 조회 '+fmt(v.viewCount)+'</div></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<span style="color:#ef4444; font-weight:bold; font-size:0.8rem; flex-shrink:0;">'+ratio+'</span></a>';
|
'<span class="badge '+ratioBadgeClass(v.viewsPerSubRatio)+'" style="flex-shrink:0;">'+ratio+'</span></a>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 발행 현황
|
// 발행 현황 — badge for status
|
||||||
const ptotal = Number(pub.total || 0);
|
const ptotal = Number(pub.total || 0);
|
||||||
document.getElementById('publishBars').innerHTML =
|
document.getElementById('publishBars').innerHTML =
|
||||||
bar('작성중', pbs.DRAFT, ptotal, '#64748b') +
|
bar('작성중', pbs.DRAFT, ptotal, '#64748b') +
|
||||||
bar('발행대기', pbs.READY, ptotal, '#f59e0b') +
|
bar('발행대기', pbs.READY, ptotal, 'var(--primary)') +
|
||||||
bar('발행완료', pbs.PUBLISHED, ptotal, '#10b981');
|
bar('발행완료', pbs.PUBLISHED, ptotal, 'var(--success)');
|
||||||
const recent = pub.recent || [];
|
const recent = pub.recent || [];
|
||||||
const PUB_ST = { DRAFT:{t:'작성중',c:'#64748b'}, READY:{t:'대기',c:'#f59e0b'}, PUBLISHED:{t:'완료',c:'#10b981'} };
|
const PUB_ST = {
|
||||||
|
DRAFT: { t:'작성중', cls:'badge-muted' },
|
||||||
|
READY: { t:'대기', cls:'badge-warning' },
|
||||||
|
PUBLISHED: { t:'완료', cls:'badge-success' }
|
||||||
|
};
|
||||||
document.getElementById('publishRecent').innerHTML = recent.length===0
|
document.getElementById('publishRecent').innerHTML = recent.length===0
|
||||||
? '<p class="text-muted text-sm" style="margin-top:4px;">발행 패키지가 없습니다.</p>'
|
? '<p class="text-muted text-sm" style="margin-top:4px;">발행 패키지가 없습니다.</p>'
|
||||||
: '<div class="text-sm text-muted" style="margin:6px 0 2px;">최근</div>' + recent.map(p=>{
|
: '<div class="text-sm text-muted" style="margin:6px 0 2px;">최근</div>' + recent.map(p=>{
|
||||||
const st = PUB_ST[p.status] || {t:p.status,c:'#64748b'};
|
const st = PUB_ST[p.status] || { t:p.status, cls:'badge-muted' };
|
||||||
return '<a href="/rework/'+p.channelVideoId+'" class="flex items-center justify-between p-2" style="border-radius:6px; background:rgba(255,255,255,0.03); text-decoration:none;">' +
|
return '<a href="/rework/'+p.channelVideoId+'" class="flex items-center justify-between p-2" style="border-radius:6px; background:rgba(255,255,255,0.03); text-decoration:none;">' +
|
||||||
'<span class="text-sm truncate" style="max-width:170px; color:#e2e8f0;">'+esc(p.title||'(제목 없음)')+'</span>' +
|
'<span class="text-sm truncate" style="max-width:170px; color:#e2e8f0;">'+esc(p.title||'(제목 없음)')+'</span>' +
|
||||||
'<span class="st-badge" style="background:'+st.c+'22; color:'+st.c+'; flex-shrink:0;">'+st.t+'</span></a>';
|
'<span class="badge '+st.cls+'" style="flex-shrink:0;">'+st.t+'</span></a>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// 카테고리 분포
|
// 카테고리 분포 — clickable deep links
|
||||||
const cats = (d.categories && d.categories.categories) || [];
|
const cats = (d.categories && d.categories.categories) || [];
|
||||||
const uncat = (d.categories && d.categories.uncategorized) || 0;
|
const uncat = (d.categories && d.categories.uncategorized) || 0;
|
||||||
const catList = document.getElementById('catList');
|
const catList = document.getElementById('catList');
|
||||||
if(cats.length===0 && !uncat){ catList.innerHTML = '<p class="text-muted text-sm">분류된 영상이 없습니다.</p>'; }
|
if(cats.length===0 && !uncat){ catList.innerHTML = '<p class="text-muted text-sm">분류된 영상이 없습니다.</p>'; }
|
||||||
else {
|
else {
|
||||||
catList.innerHTML = cats.map(c => bar(c.name, c.count, total, c.color || '#7C3AED')).join('') +
|
catList.innerHTML =
|
||||||
bar('미분류', uncat, total, '#475569');
|
cats.map(c => '<a href="/collection?categoryId='+c.id+'" style="text-decoration:none; display:block;">'+bar(c.name, c.count, total, 'var(--primary)')+'</a>').join('') +
|
||||||
|
'<a href="/collection" style="text-decoration:none; display:block;">'+bar('미분류', uncat, total, '#475569')+'</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 출처 & 포맷
|
// 출처 & 포맷 — clickable deep links + 3-tone colors
|
||||||
const shorts = Number(pipe.shorts || 0), longForm = Number(pipe.longForm || 0);
|
const shorts = Number(pipe.shorts || 0), longForm = Number(pipe.longForm || 0);
|
||||||
document.getElementById('sourceFormat').innerHTML =
|
document.getElementById('sourceFormat').innerHTML =
|
||||||
'<div class="text-sm text-muted" style="margin-bottom:2px;">출처</div>' +
|
'<div class="text-sm text-muted" style="margin-bottom:2px;">출처</div>' +
|
||||||
bar('채널 수집', src.CHANNEL, total, '#38bdf8') +
|
'<a href="/collection?source=CHANNEL" style="text-decoration:none; display:block;">'+bar('채널 수집', src.CHANNEL, total, 'var(--primary)')+'</a>' +
|
||||||
bar('검색 수집', src.SEARCH, total, '#a78bfa') +
|
'<a href="/collection?source=SEARCH" style="text-decoration:none; display:block;">'+bar('검색 수집', src.SEARCH, total, 'var(--primary)')+'</a>' +
|
||||||
'<div class="text-sm text-muted" style="margin:10px 0 2px;">포맷</div>' +
|
'<div class="text-sm text-muted" style="margin:10px 0 2px;">포맷</div>' +
|
||||||
bar('Shorts', shorts, total, '#ef4444') +
|
'<a href="/collection?shortsOnly=true" style="text-decoration:none; display:block;">'+bar('Shorts', shorts, total, 'var(--danger)')+'</a>' +
|
||||||
bar('롱폼', longForm, total, '#10b981');
|
'<a href="/collection" style="text-decoration:none; display:block;">'+bar('롱폼', longForm, total, 'var(--success)')+'</a>';
|
||||||
|
|
||||||
if(window.lucide) lucide.createIcons();
|
if(window.lucide) lucide.createIcons();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user