@@ -3605,7 +3605,8 @@ def render_user_dashboard_html(
36053605 if (cats.length === 0) {
36063606 categoryEl.innerHTML = '<div class=\" empty\" style=\" color:#111827;padding:12px\" >' + tt('dash.dyn.pdca.no_check_data','점검 데이터가 없습니다.') + '</div>';
36073607 } else {
3608- // 통제 카탈로그 트리와 동일하게 프레임워크 → 도메인 → 섹션(카테고리) 계층으로 묶어 매칭.
3608+ // 통제 카탈로그 트리와 동일하게 프레임워크 → 도메인(접기) → 섹션 계층으로 매칭.
3609+ // total = 카탈로그 통제 개수 → 트리 분모(예: 2.보호대책 64)와 정확히 일치.
36093610 const _fwLabel = { 'isms-p': 'ISMS-P', 'iso27001': 'ISO 27001:2022', 'custom': 'Custom / 법령' };
36103611 // cats 는 payload 에서 이미 트리 순서(ISMS-P 먼저 · 섹션번호 자연정렬)로 정렬됨.
36113612 const groups = []; // {fw, domain, rows:[], agg:{}}
@@ -3620,45 +3621,33 @@ def render_user_dashboard_html(
36203621 g.agg.pass += c.pass; g.agg.fail += c.fail; g.agg.warning += c.warning;
36213622 g.agg.not_checked += c.not_checked; g.agg.total += c.total;
36223623 });
3623- let lastFw = null;
3624- const body = groups.map(g => {
3625- let out = '';
3624+ const secTable = rows => `<table style=\" width:100%;border-collapse:collapse;font-size:12px;margin:4px 0 2px 14px\" >
3625+ <tbody>${rows.map(c => `<tr style=\" border-bottom:1px solid #f1f5f9\" >
3626+ <td style=\" padding:4px 8px;color:#111827\" >${escapeHtml(c.category)}</td>
3627+ <td style=\" text-align:right;padding:4px 8px;color:#16a34a;width:42px\" >${c.pass}</td>
3628+ <td style=\" text-align:right;padding:4px 8px;color:#dc2626;width:42px\" >${c.fail}</td>
3629+ <td style=\" text-align:right;padding:4px 8px;color:#ea580c;width:42px\" >${c.warning}</td>
3630+ <td style=\" text-align:right;padding:4px 8px;color:#111827;width:42px\" >${c.not_checked}</td>
3631+ <td style=\" text-align:right;padding:4px 8px;color:#111827;width:42px\" >${c.total}</td></tr>`).join('')}</tbody></table>`;
3632+ const legend = `<div style=\" display:flex;gap:12px;justify-content:flex-end;font-size:11px;color:#111827;margin-bottom:4px\" >
3633+ <span style=\" color:#16a34a\" >Pass</span><span style=\" color:#dc2626\" >Fail</span><span style=\" color:#ea580c\" >Warning</span><span>${tt('dash.dyn.pdca.not_checked','미점검')}</span><span>${tt('dash.dyn.lbl.total','합계')}</span></div>`;
3634+ let lastFw = null, html = legend;
3635+ groups.forEach(g => {
36263636 if (g.fw !== lastFw) {
36273637 lastFw = g.fw;
3628- out += `<tr style=\" background:#f9fafb \" ><td colspan= \" 6 \" style= \" padding:6px 8px ;color:#2563eb;font-weight:700;font- size:12px \" >${escapeHtml(_fwLabel[g.fw]||g.fw||'')}</td></tr >`;
3638+ html += `<div style=\" margin-top:10px;font-weight:700 ;color:#2563eb;font-size:13px \" >${escapeHtml(_fwLabel[g.fw]||g.fw||'')}</div >`;
36293639 }
3640+ const a = g.agg;
3641+ const nums = `<span style=\" color:#16a34a\" >${a.pass}</span> / <span style=\" color:#dc2626\" >${a.fail}</span> / <span style=\" color:#ea580c\" >${a.warning}</span> / ${a.not_checked} / ${a.total}`;
36303642 const domSameAsSec = (g.rows.length === 1 && g.rows[0].category === g.domain);
3631- if (!domSameAsSec) {
3632- out += `<tr style=\" border-bottom:1px solid #e5e7eb\" >
3633- <td style=\" padding:6px 8px;color:#111827;font-weight:700\" >${escapeHtml(g.domain)}</td>
3634- <td style=\" text-align:right;padding:6px 8px;color:#16a34a;font-weight:700\" >${g.agg.pass}</td>
3635- <td style=\" text-align:right;padding:6px 8px;color:#dc2626;font-weight:700\" >${g.agg.fail}</td>
3636- <td style=\" text-align:right;padding:6px 8px;color:#ea580c;font-weight:700\" >${g.agg.warning}</td>
3637- <td style=\" text-align:right;padding:6px 8px;color:#111827;font-weight:700\" >${g.agg.not_checked}</td>
3638- <td style=\" text-align:right;padding:6px 8px;color:#111827;font-weight:700\" >${g.agg.total}</td></tr>`;
3643+ if (domSameAsSec) {
3644+ html += `<div style=\" margin:4px 0 0 4px;color:#111827;font-size:13px;padding:2px 0\" >${escapeHtml(g.domain)} <span style=\" font-size:11px\" >(${nums})</span></div>`;
3645+ } else {
3646+ html += `<details style=\" margin:4px 0 0 4px\" ><summary style=\" cursor:pointer;color:#111827;font-size:13px;padding:2px 0\" >${escapeHtml(g.domain)} <span style=\" font-size:11px\" >(${nums})</span></summary>`
3647+ + secTable(g.rows) + `</details>`;
36393648 }
3640- out += g.rows.map(c => {
3641- const pad = domSameAsSec ? '6px 8px' : '5px 8px 5px 22px';
3642- const fw2 = domSameAsSec ? 'font-weight:700' : 'font-weight:400';
3643- return `<tr style=\" border-bottom:1px solid #f1f5f9\" >
3644- <td style=\" padding:${pad};color:#111827;${fw2}\" >${escapeHtml(c.category)}</td>
3645- <td style=\" text-align:right;padding:${pad};color:#16a34a\" >${c.pass}</td>
3646- <td style=\" text-align:right;padding:${pad};color:#dc2626\" >${c.fail}</td>
3647- <td style=\" text-align:right;padding:${pad};color:#ea580c\" >${c.warning}</td>
3648- <td style=\" text-align:right;padding:${pad};color:#111827\" >${c.not_checked}</td>
3649- <td style=\" text-align:right;padding:${pad};color:#111827\" >${c.total}</td></tr>`;
3650- }).join('');
3651- return out;
3652- }).join('');
3653- categoryEl.innerHTML = `<table style=\" width:100%;border-collapse:collapse;font-size:13px\" >
3654- <thead><tr style=\" color:#111827;border-bottom:1px solid #e5e7eb\" >
3655- <th style=\" text-align:left;padding:6px 8px\" >${tt('dash.dyn.pdca.category','카테고리')}</th>
3656- <th style=\" text-align:right;padding:6px 8px\" >Pass</th>
3657- <th style=\" text-align:right;padding:6px 8px\" >Fail</th>
3658- <th style=\" text-align:right;padding:6px 8px\" >Warning</th>
3659- <th style=\" text-align:right;padding:6px 8px\" >${tt('dash.dyn.pdca.not_checked','미점검')}</th>
3660- <th style=\" text-align:right;padding:6px 8px\" >${tt('dash.dyn.lbl.total','합계')}</th>
3661- </tr></thead><tbody>` + body + '</tbody></table>';
3649+ });
3650+ categoryEl.innerHTML = html;
36623651 }
36633652 }
36643653 // Pending remediations (control_check + trivy + alert)
0 commit comments