포털 인증·계정·사용자 그룹·대시보드·알림 센터·감사 화면·도움말·발송기·스케줄러 (§3·§4·§5·§24)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zzabisagent-lab
2026-10-26 11:26:28 +00:00
co-authored by Claude Fable 5.1
parent 0f11424224
commit c90ac27596
21 changed files with 1005 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
/* portal/dashboard/public/dashboard.js — 대시보드 요약 카드·주의 항목·최근 활동 렌더(30초마다 갱신). */
(() => {
const el = document.getElementById('dash'); if (!el) return;
const ACT = { 'deploy.start': '배포 시작', 'deploy.done': '배포 완료', 'order.created': '주문 제출', 'order.approve': '주문 승인', 'member.invited': '구성원 초대', 'member.joined': '구성원 참여', 'member.role': '역할 변경', 'group.settings': '그룹 설정 변경', 'permission.site': '사이트 권한 변경', 'ownership.group_transferred': '그룹 소유권 이전', 'member.suspended': '구성원 정지' };
const fmtKrw = (n) => `${Number(n || 0).toLocaleString('ko-KR')}원`;
async function load() {
try {
const d = await aw.api('GET', '/api/dashboard'); const s = d.summary;
const card = (title, stat, sub) => `<div class="card"><h2>${aw.esc(title)}</h2><div class="stat">${stat}</div>${sub ? `<div class="sub">${sub}</div>` : ''}</div>`;
el.innerHTML = `<div class="cards">
${card('운영 사이트', `${s.sites.inUse} <span class="sub">/ 구매 ${s.sites.purchased}개</span>`, `사용 중 ${s.sites.inUse} · 예약됨 ${s.sites.reserved} · 미배정 ${s.sites.unallocated}`)}
${card('초안·제작 중', s.drafts, '운영 사이트 수를 사용하지 않습니다')}
${card('사이트 상태', `<span class="badge ok">● 정상 ${s.health.ok}</span> <span class="badge warning">! 경고 ${s.health.warn}</span> <span class="badge failed">✘ 장애 ${s.health.bad}</span>`)}
${card('배포 방식', `관리형 ${s.deploy.managed} · 외부 ${s.deploy.external}`)}
${card('도메인', `${s.domains.total}`, s.domains.action ? `<a href="/domains">조치 필요 ${s.domains.action}개</a>` : '조치 필요 없음')}
${card('메일', `서비스 ${s.mail.services} · 메일함 ${s.mail.mailboxes}`)}
${card('백업 옵션 사이트', s.backup)}
${card('진행 중 작업', s.jobs.length ? s.jobs.map((j) => `${aw.esc(j.label)} ${j.n}`).join(' · ') : '0')}
${card('이번 달 예상 비용', fmtKrw(s.cost.estimated), `결제 대기 ${fmtKrw(s.cost.awaiting)} · ${aw.esc(s.cost.note)}`)}
</div>
<div class="card"><h2>주의 항목</h2>${d.attention.length ? d.attention.map((a) => `<a class="alert ${a.severity}" href="${aw.esc(a.href)}">${a.severity === 'critical' ? '⚠' : '!'} ${aw.esc(a.text)}</a>`).join('') : '<div class="aw-state empty"><b>지금 조치할 항목이 없습니다</b></div>'}</div>
<div class="card"><h2>최근 활동</h2>${d.recent.length ? `<table class="r"><thead><tr><th>시각</th><th>내용</th><th>주체</th><th>결과</th></tr></thead><tbody>${d.recent.map((r) => `<tr><td data-l="시각">${aw.esc(aw.fmt(r.at))}</td><td data-l="내용">${aw.esc(ACT[r.action] || r.action)}${r.subjectType ? ` <span class="sub">${aw.esc(r.subjectType)} ${aw.esc(r.subjectId || '')}</span>` : ''}</td><td data-l="주체">${r.automatic ? '<span class="badge">⚙ 자동</span>' : `<span class="badge">👤 ${aw.esc(r.actor)}</span>`}</td><td data-l="결과">${r.outcome === 'ok' ? '<span class="badge ok">✔ 성공</span>' : `<span class="badge failed">✘ ${aw.esc(r.outcome)}</span>`}</td></tr>`).join('')}</tbody></table>` : '<div class="aw-state empty"><b>최근 활동이 없습니다</b></div>'}</div>
<p class="sub">마지막 갱신 ${aw.esc(aw.fmt(d.generatedAt))}</p>`;
} catch (err) { aw.state(el, err.status === 403 ? 'forbidden' : err.code === 'service_unreachable' ? 'stale' : 'failed', err.message, { retry: load }); }
}
load(); aw.poll(load, 30000);
})();