fix(dashboard): null-ratio badge color, escape categoryId, robust refresh selector, animate once
This commit is contained in:
parent
2d6c962567
commit
8fe90e431f
@ -14,7 +14,7 @@
|
|||||||
<h1 class="text-xl font-bold mb-1">Dashboard</h1>
|
<h1 class="text-xl font-bold mb-1">Dashboard</h1>
|
||||||
<p class="text-muted">수집 → 큐레이션 → 재가공 → 발행 파이프라인 현황</p>
|
<p class="text-muted">수집 → 큐레이션 → 재가공 → 발행 파이프라인 현황</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" onclick="loadDashboard()">
|
<button id="refreshBtn" class="btn btn-secondary px-3 py-2 flex items-center gap-1" onclick="loadDashboard()">
|
||||||
<i data-lucide="refresh-cw" style="width:16px;"></i> 새로고침
|
<i data-lucide="refresh-cw" style="width:16px;"></i> 새로고침
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -110,6 +110,8 @@
|
|||||||
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; }
|
||||||
|
|
||||||
|
let dashboardAnimated = false;
|
||||||
|
|
||||||
function countUp(el, target){
|
function countUp(el, target){
|
||||||
target = Number(target||0);
|
target = Number(target||0);
|
||||||
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){ el.textContent = fmt(target); return; }
|
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){ el.textContent = fmt(target); return; }
|
||||||
@ -123,6 +125,12 @@
|
|||||||
requestAnimationFrame(tick);
|
requestAnimationFrame(tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setKpi(id, value){
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if(!el) return;
|
||||||
|
if(dashboardAnimated) el.textContent = fmt(value); else countUp(el, value);
|
||||||
|
}
|
||||||
|
|
||||||
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(()=>({}));
|
||||||
@ -142,7 +150,7 @@
|
|||||||
function ratioBadgeClass(r){ const v=Number(r); return v>=10?'badge-danger':(v>=2?'badge-warning':'badge-success'); }
|
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');
|
const refreshIcon = document.querySelector('#refreshBtn svg, #refreshBtn i');
|
||||||
if (refreshIcon) refreshIcon.classList.add('animate-spin');
|
if (refreshIcon) refreshIcon.classList.add('animate-spin');
|
||||||
let d;
|
let d;
|
||||||
try {
|
try {
|
||||||
@ -159,14 +167,14 @@
|
|||||||
const pub = d.publish || {}, pbs = pub.byStatus || {};
|
const pub = d.publish || {}, pbs = pub.byStatus || {};
|
||||||
|
|
||||||
// KPI (count-up for numbers, textContent for captions)
|
// KPI (count-up for numbers, textContent for captions)
|
||||||
countUp(document.getElementById('kTotal'), total);
|
setKpi('kTotal', total);
|
||||||
document.getElementById('kTotalCap').textContent = '채널 ' + fmt(src.CHANNEL) + ' · 검색 ' + fmt(src.SEARCH);
|
document.getElementById('kTotalCap').textContent = '채널 ' + fmt(src.CHANNEL) + ' · 검색 ' + fmt(src.SEARCH);
|
||||||
countUp(document.getElementById('kNew'), bs.NEW);
|
setKpi('kNew', bs.NEW);
|
||||||
document.getElementById('kReviewCap').textContent = '검토중 ' + fmt(bs.REVIEWING);
|
document.getElementById('kReviewCap').textContent = '검토중 ' + fmt(bs.REVIEWING);
|
||||||
countUp(document.getElementById('kTarget'), bs.TARGET);
|
setKpi('kTarget', bs.TARGET);
|
||||||
countUp(document.getElementById('kDone'), bs.DONE);
|
setKpi('kDone', bs.DONE);
|
||||||
document.getElementById('kExcludedCap').textContent = '제외 ' + fmt(bs.EXCLUDED);
|
document.getElementById('kExcludedCap').textContent = '제외 ' + fmt(bs.EXCLUDED);
|
||||||
countUp(document.getElementById('kPublished'), pbs.PUBLISHED);
|
setKpi('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
|
// 깔때기 (총 수집 대비) — 3-tone color scheme
|
||||||
@ -192,7 +200,7 @@
|
|||||||
'<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 class="badge '+ratioBadgeClass(v.viewsPerSubRatio)+'" style="flex-shrink:0;">'+ratio+'</span></a>';
|
'<span class="badge '+(v.viewsPerSubRatio != null ? ratioBadgeClass(v.viewsPerSubRatio) : 'badge-muted')+'" style="flex-shrink:0;">'+ratio+'</span></a>';
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +232,7 @@
|
|||||||
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 =
|
catList.innerHTML =
|
||||||
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('') +
|
cats.map(c => '<a href="/collection?categoryId='+encodeURIComponent(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>';
|
'<a href="/collection" style="text-decoration:none; display:block;">'+bar('미분류', uncat, total, '#475569')+'</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,6 +247,7 @@
|
|||||||
'<a href="/collection" style="text-decoration:none; display:block;">'+bar('롱폼', longForm, total, 'var(--success)')+'</a>';
|
'<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();
|
||||||
|
dashboardAnimated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user