#!/usr/bin/env python3 """ generate_dashboard.py — Stage 2 자동 실행 스크립트 Stage 1 결과물(BO.json, client_goal.json, Fact_Ledger.json, evidence_indexed.json)을 읽어 단일 HTML 파일(Case_Dashboard.html)을 생성한다. 사건 데이터에 의존하지 않으며, Stage 1 스키마만 준수하면 어떤 사건에도 적용 가능하다. 사용법: python generate_dashboard.py [output_dir] - input_dir : Stage 1 결과물 4개 JSON이 있는 폴더 (필수) - output_dir: HTML 저장 폴더 (생략 시 input_dir과 동일) """ import json import html import os import sys from itertools import groupby # ───────────────────────────────────────────── # 1. 유틸리티 # ───────────────────────────────────────────── def load_json(filepath: str): """UTF-8 JSON 파일을 읽어 파이썬 객체로 반환""" with open(filepath, "r", encoding="utf-8") as f: return json.load(f) def truncate(text: str, max_len: int) -> str: """max_len 글자까지 자르고, 초과 시 … 부착""" if not text: return "" return text[:max_len] + ("…" if len(text) > max_len else "") def sanitize_for_mermaid(text: str) -> str: """Mermaid 구문을 깨뜨릴 수 있는 문자를 치환""" if not text: return "" replacements = { '"': "'", # 쌍따옴표 → 홑따옴표 "#": "", # Mermaid 키워드 충돌 방지 "&": "and", # SVG 엔티티 충돌 방지 "<": "<", ">": ">", "{": "(", "}": ")", } for old, new in replacements.items(): text = text.replace(old, new) return text def sanitize_for_timeline(text: str) -> str: """Mermaid Timeline 전용: 콜론도 추가로 치환""" text = sanitize_for_mermaid(text) text = text.replace(":", ":") # 전각 콜론으로 치환 (구분자 충돌 방지) return text def has_evidence_gap(bo: dict) -> bool: """해당 BO가 증거공백인지 판정""" if not bo.get("Evidence"): # 빈 리스트 또는 없음 return True if bo.get("Outcome") == "[증거공백]": return True return False def extract_year(date_str: str) -> str: """날짜 문자열에서 연도 추출. 없으면 '미상'""" if date_str and len(date_str) >= 4: return date_str[:4] return "미상" # ───────────────────────────────────────────── # 2. 섹션별 생성 함수 # ───────────────────────────────────────────── def build_section1(goal_data: dict) -> tuple: """ Section 1: 사건 개요 — primary_goal, constraints 텍스트 생성 Returns: (primary_goal_text, constraints_text) """ primary_goal = goal_data.get("primary_goal", "(목표 미기재)") constraints = goal_data.get("constraints", []) ordinals = ["첫째", "둘째", "셋째", "넷째", "다섯째", "여섯째", "일곱째", "여덟째", "아홉째", "열째"] if constraints: parts = [] for i, c in enumerate(constraints): prefix = ordinals[i] if i < len(ordinals) else f"제{i+1}" parts.append(f"{prefix}, {c}") constraints_text = ". ".join(parts) + "." else: constraints_text = "(제약 사항 없음)" return primary_goal, constraints_text def build_bo_timeline(bo_list: list) -> str: """ Section 2: BO Timeline — Mermaid timeline 구문 생성 규칙: BehaviorTime 정렬 → 연도 section → {date} : {id} - {Performer}, {Action 앞 20자} """ sorted_bos = sorted( bo_list, key=lambda x: x.get("BehaviorTime") or "9999-99-99" ) lines = ["timeline", " title 행위 타임라인"] for year, group in groupby(sorted_bos, key=lambda b: extract_year(b.get("BehaviorTime", ""))): lines.append(f" section {year}") for bo in group: date = bo.get("BehaviorTime", "미상") bo_id = bo.get("id", "?") performer = sanitize_for_timeline(bo.get("Performer", "?")) action = sanitize_for_timeline(truncate(bo.get("Action", ""), 20)) gap = " [증거공백]" if has_evidence_gap(bo) else "" lines.append(f" {date} : {bo_id} - {performer}, {action}{gap}") return "\n".join(lines) def build_causality_flowchart(bo_list: list) -> str: """ Section 3: 인과관계도 — Mermaid flowchart TD 구문 생성 규칙: 노드 {id}["{Performer}: {Action 앞 15자}"], PriorAct → 엣지, 증거공백 → :::gap """ lines = [ "flowchart TD", " classDef gap fill:#fee2e2,stroke:#ef4444,stroke-width:2px", "", ] # 노드 정의 for bo in bo_list: bo_id = bo.get("id", "?") performer = sanitize_for_mermaid(bo.get("Performer", "?")) action = sanitize_for_mermaid(truncate(bo.get("Action", ""), 15)) label = f"{performer}: {action}" gap_class = ":::gap" if has_evidence_gap(bo) else "" lines.append(f' {bo_id}["{label}"]{gap_class}') lines.append("") # 엣지 정의 for bo in bo_list: prior = bo.get("PriorAct") if prior: lines.append(f" {prior} --> {bo.get('id', '?')}") return "\n".join(lines) def build_fact_timeline(fact_list: list) -> str: """ Section 4: 사실원장 타임라인 — Mermaid timeline 구문 생성 규칙: date 정렬 → 연도 section → {date} : {fact_id} - {action 앞 20자} ({credibility}) """ with_date = [f for f in fact_list if f.get("date")] no_date = [f for f in fact_list if not f.get("date")] sorted_facts = sorted(with_date, key=lambda x: x["date"]) lines = ["timeline", " title 사실원장 타임라인"] for year, group in groupby(sorted_facts, key=lambda f: extract_year(f["date"])): lines.append(f" section {year}") for fact in group: date = fact["date"] fid = fact.get("fact_id", "?") action = sanitize_for_timeline(truncate(fact.get("action", ""), 20)) cred = fact.get("credibility", "unknown") lines.append(f" {date} : {fid} - {action} ({cred})") if no_date: lines.append(" section 일자미상") for fact in no_date: fid = fact.get("fact_id", "?") action = sanitize_for_timeline(truncate(fact.get("action", ""), 20)) cred = fact.get("credibility", "unknown") lines.append(f" 미상 : {fid} - {action} ({cred})") return "\n".join(lines) def build_evidence_table(evidence_list: list) -> str: """ Section 5: 증거 매핑표 — HTML 행 생성 필드명 호환: document_type / doc_type, key_info / key_facts """ rows = [] for ev in evidence_list: idx = html.escape(ev.get("evidence_index", "?")) doc_type = html.escape( ev.get("document_type") or ev.get("doc_type", "?") ) title = html.escape(ev.get("title", "?")) key = html.escape( ev.get("key_info") or ev.get("key_facts", "?") ) rows.append( f" \n" f" {idx}\n" f" {doc_type}\n" f" {title}\n" f" {key}\n" f" " ) return "\n".join(rows) # ───────────────────────────────────────────── # 3. HTML 템플릿 # ───────────────────────────────────────────── HTML_TEMPLATE = """ 사건 시각화 대시보드

⚖ 사건 시각화 대시보드

1. 사건 개요

의뢰 목표 (Primary Goal):

{primary_goal}

⚠️ 제약 사항 (Constraints):

{constraints_text}

2. 행위 타임라인 (Behavioral Objects)

{bo_timeline}

3. 인과관계도 (Causality Flow)

{causality_flowchart}

4. 사실원장 타임라인 (Fact Ledger)

{fact_timeline}

5. 증거 매핑표 (Evidence Index)

{evidence_table_rows}
증거번호 문서유형 제목 핵심사실
""" # ───────────────────────────────────────────── # 4. 메인 실행 # ───────────────────────────────────────────── def main(): # 인자 처리 if len(sys.argv) < 2: print("사용법: python generate_dashboard.py [output_dir]") print(" input_dir : Stage 1 결과 JSON 4개가 위치한 폴더") print(" output_dir : Case_Dashboard.html 저장 폴더 (생략 시 input_dir)") sys.exit(1) input_dir = sys.argv[1] output_dir = sys.argv[2] if len(sys.argv) >= 3 else input_dir # ── Task A: 데이터 로드 ── print("=" * 50) print("[Task A] 데이터 로드") print("=" * 50) file_map = { "client_goal.json": "goal_data", "BO.json": "bo_list", "Fact_Ledger.json": "fact_list", "evidence_indexed.json": "evidence_list", } missing = [] for fname in file_map: fpath = os.path.join(input_dir, fname) if os.path.exists(fpath): print(f" [OK] {fname}") else: print(f" [MISSING] {fname}") missing.append(fname) if missing: print(f"\n ERROR: 필수 파일 {len(missing)}개 누락. 중단합니다.") sys.exit(1) goal_data = load_json(os.path.join(input_dir, "client_goal.json")) bo_list = load_json(os.path.join(input_dir, "BO.json")) fact_list = load_json(os.path.join(input_dir, "Fact_Ledger.json")) evidence_list = load_json(os.path.join(input_dir, "evidence_indexed.json")) print(f"\n 로드 완료: BO {len(bo_list)}건, Fact {len(fact_list)}건, Evidence {len(evidence_list)}건") print("TASK A COMPLETE\n") # ── Task B: HTML 생성 ── print("=" * 50) print("[Task B] HTML 생성") print("=" * 50) primary_goal, constraints_text = build_section1(goal_data) print(" [OK] Section 1 — 사건 개요") bo_timeline = build_bo_timeline(bo_list) print(" [OK] Section 2 — 행위 타임라인") causality_flowchart = build_causality_flowchart(bo_list) print(" [OK] Section 3 — 인과관계도") fact_timeline = build_fact_timeline(fact_list) print(" [OK] Section 4 — 사실원장 타임라인") evidence_table_rows = build_evidence_table(evidence_list) print(" [OK] Section 5 — 증거 매핑표") # 템플릿 조립 html_content = HTML_TEMPLATE.format( primary_goal=html.escape(primary_goal), constraints_text=html.escape(constraints_text), bo_timeline=bo_timeline, causality_flowchart=causality_flowchart, fact_timeline=fact_timeline, evidence_table_rows=evidence_table_rows, ) # 저장 os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, "Case_Dashboard.html") with open(output_path, "w", encoding="utf-8") as f: f.write(html_content) print(f"\n → 저장: {output_path}") print("TASK B COMPLETE\n") # 요약 print("=" * 50) print("최종 체크리스트") print("=" * 50) print(f" [v] 입력 파일 4개 로드") print(f" [v] Section 1: Primary Goal + Constraints {len(goal_data.get('constraints', []))}개") print(f" [v] Section 2: BO Timeline — {len(bo_list)}개 행위") print(f" [v] Section 3: Causality Flowchart — {sum(1 for b in bo_list if b.get('PriorAct'))}개 엣지") print(f" [v] Section 4: Fact Ledger — {len(fact_list)}개 사실") print(f" [v] Section 5: Evidence Table — {len(evidence_list)}개 증거") print(f" [v] {output_path} 저장 완료") if __name__ == "__main__": main()