Skip to content

Commit 0949f13

Browse files
committed
feat(compliance): 통제 커버리지 정직성 — reviewed/draft 비율 노출 + draft 라벨
- /controls/tree coverage에 review{reviewed,draft,total,pct} 추가(정직한 검토 진척도) - 컨트롤 트리: 커버리지 옆 '검토완료 58/194' 표시 + 미검토 통제에 draft 배지 - 커버리지 %는 reviewed+증적 연결 통제만 집계임을 툴팁으로 명시. 한/영 i18n
1 parent b61ea86 commit 0949f13

3 files changed

Lines changed: 23 additions & 2 deletions

File tree

src/mori_soc/api/i18n.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ def _i18n_toggle_html(fixed: bool = True) -> str:
303303
"dash.ctl.snap_confirm": "설정된 범위의 전 통제를 지금 일괄 스냅샷할까요?",
304304
"dash.ctl.snap_running": "일괄 스냅샷 중…", "dash.ctl.snap_done": "건 스냅샷됨",
305305
"dash.ctl.zip": "전체 증적 ZIP",
306+
"dash.ctl.reviewed": "검토완료",
307+
"dash.ctl.review_tip": "커버리지 %는 검토완료(reviewed)+증적 연결 통제만 집계합니다. 나머지는 초안(draft)이며 공식 고시 대비 검토 전입니다.",
308+
"dash.ctl.draft": "초안",
309+
"dash.ctl.draft_tip": "초안 — 공식 고시 대비 검토 전",
306310
"dash.ctl.nlp_ttl": "법령/고시 텍스트 → 통제 초안",
307311
"dash.ctl.nlp_help": "법령 원문을 붙여넣으면 통제 초안으로 바꿔서 저장해요. Claude 키가 있으면 더 정확하게, 없으면 조항 단위로 나눠요.",
308312
"dash.ctl.nlp_fw": "프레임워크(예: 개인정보보호법)", "dash.ctl.nlp_ph": "법령/고시 전문을 붙여넣으세요…",
@@ -1094,6 +1098,10 @@ def _i18n_toggle_html(fixed: bool = True) -> str:
10941098
"dash.ctl.snap_confirm": "Snapshot all controls in scope right now?",
10951099
"dash.ctl.snap_running": "Snapshotting…", "dash.ctl.snap_done": " snapshotted",
10961100
"dash.ctl.zip": "All evidence ZIP",
1101+
"dash.ctl.reviewed": "reviewed",
1102+
"dash.ctl.review_tip": "Coverage % counts only reviewed controls that have wired-up evidence. The rest are drafts — not yet validated against the official standard text.",
1103+
"dash.ctl.draft": "draft",
1104+
"dash.ctl.draft_tip": "Draft — not yet reviewed against the official standard text",
10971105
"dash.ctl.nlp_ttl": "Regulation text → control drafts",
10981106
"dash.ctl.nlp_help": "Paste regulation text to convert·save it as draft controls. Precise structuring with a Claude API key; clause-level heuristic otherwise.",
10991107
"dash.ctl.nlp_fw": "Framework (e.g. PIPA)", "dash.ctl.nlp_ph": "Paste the full regulation/notice text…",

src/mori_soc/api/templates/dashboard.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3915,7 +3915,9 @@ def render_user_dashboard_html(
39153915
const cov = data.coverage || {};
39163916
const covEl = document.getElementById('control_tree_coverage');
39173917
if (covEl && cov.lite && cov.full) {
3918-
covEl.textContent = `lite ${cov.lite.pct}% (${cov.lite.covered}/${cov.lite.total}) · full ${cov.full.pct}% (${cov.full.covered}/${cov.full.total})`;
3918+
let t = `lite ${cov.lite.pct}% (${cov.lite.covered}/${cov.lite.total}) · full ${cov.full.pct}% (${cov.full.covered}/${cov.full.total})`;
3919+
covEl.innerHTML = escapeHtml(t)
3920+
+ (cov.review ? ` · <span style=\"color:#a16207\" title=\"${tt('dash.ctl.review_tip','커버리지 %는 검토완료(reviewed)+증적 연결 통제만 집계합니다. 나머지는 초안(draft)이며 공식 고시 대비 검토 전입니다.')}\">${tt('dash.ctl.reviewed','검토완료')} ${cov.review.reviewed}/${cov.review.total} (${cov.review.pct}%)</span>` : '');
39193921
}
39203922
const fwLabel = { 'isms-p': 'ISMS-P', 'iso27001': 'ISO 27001:2022', 'custom': 'Custom / 법령' };
39213923
const smap = data.status_map || {};
@@ -3932,9 +3934,10 @@ def render_user_dashboard_html(
39323934
const clickable = 'cursor:pointer'; // M2-7: 상태 편집 위해 전 항목 클릭 가능
39333935
const st = smap[c.id];
39343936
const stBadge = (st && st.status && st.status !== '미정') ? _ctlStatusBadge(st.status) : '';
3937+
const draftBadge = (c.status && c.status !== 'reviewed') ? `<span title=\"${tt('dash.ctl.draft_tip','초안 — 공식 고시 대비 검토 전')}\" style=\"background:#fef9c3;color:#a16207;border:1px solid #a1620733;padding:0 5px;border-radius:5px;font-size:9px;margin-left:4px;vertical-align:middle\">${tt('dash.ctl.draft','draft')}</span>` : '';
39353938
const pdf = c.mapped ? `<a href=\"/controls/detail/${enc}/evidence.pdf\" target=\"_blank\" title=\"${tt('dash.ctl.pdf','증적 팩 PDF')}\" style=\"margin-left:6px;text-decoration:none;font-size:11px\"></a>` : '';
39363939
const editBtns = _ctlCanEdit ? `<span onclick=\"openControlEditor('${enc}')\" title=\"${tt('dash.ctl.edit','수정')}\" style=\"cursor:pointer;margin-left:6px;font-size:11px\"></span><span onclick=\"deleteControl('${enc}')\" title=\"${tt('dash.ctl.del','삭제')}\" style=\"cursor:pointer;margin-left:3px;font-size:11px\"></span>` : '';
3937-
return `<div style=\"padding:3px 0;${dim}\"><span onclick=\"toggleControlDetail('${enc}', this)\" style=\"${clickable}\"><span style=\"color:#111827;font-size:11px\">${escapeHtml(c.id)}</span> ${escapeHtml(title)}${stBadge}${srcs}</span>${pdf}${editBtns}<div class=\"ctl-detail\" style=\"display:none;margin:4px 0 8px 16px;padding:6px 10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;font-size:12px\"></div></div>`;
3940+
return `<div style=\"padding:3px 0;${dim}\"><span onclick=\"toggleControlDetail('${enc}', this)\" style=\"${clickable}\"><span style=\"color:#111827;font-size:11px\">${escapeHtml(c.id)}</span> ${escapeHtml(title)}${draftBadge}${stBadge}${srcs}</span>${pdf}${editBtns}<div class=\"ctl-detail\" style=\"display:none;margin:4px 0 8px 16px;padding:6px 10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;font-size:12px\"></div></div>`;
39383941
};
39393942
let html = '';
39403943
(data.tree || []).forEach(fw => {

src/mori_soc/services/control_catalog.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,21 @@ def _sort_key(cid: str):
126126
domains.append({"domain": dnode["domain"], "sections": sections})
127127
tree.append({"framework": fw, "domains": domains})
128128

129+
# 카탈로그 검토 진척도(정직성): reviewed vs draft. 커버리지 %는 reviewed+증적 연결
130+
# 통제만 집계되지만, 전체 194 중 몇 %가 실제 검토됐는지를 별도로 노출한다.
131+
total = len(controls)
132+
reviewed = sum(1 for c in controls if str(c.get("status", "draft")) == "reviewed")
129133
return {
130134
"meta": cat.get("meta", {}),
131135
"coverage": {
132136
"lite": _coverage(controls, _LITE_SOURCES),
133137
"full": _coverage(controls, _FULL_SOURCES),
138+
"review": {
139+
"reviewed": reviewed,
140+
"draft": total - reviewed,
141+
"total": total,
142+
"pct": round(reviewed / total * 100, 1) if total else 0.0,
143+
},
134144
},
135145
"tree": tree,
136146
}

0 commit comments

Comments
 (0)