diff --git a/app/static/index.html b/app/static/index.html
index eade67a..a53f67e 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -49,7 +49,7 @@
}
.panel h2 { font-size: 14px; margin: 0 0 14px; color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
label { display: block; margin-top: 12px; font-size: 12px; color: var(--muted); }
- input[type="text"], textarea {
+ input[type="text"], textarea, select {
width: 100%; padding: 10px 12px; margin-top: 4px;
background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
font-family: inherit; font-size: 13px;
@@ -194,6 +194,28 @@
border-radius: 4px; font-size: 12px; line-height: 1.6; margin-top: 14px;
}
+ .legal-risk-card {
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
+ padding: 14px; margin-top: 8px;
+ }
+ .legal-risk-card.legal-likely { border-color: var(--danger); background: rgba(248, 81, 73, 0.07); }
+ .legal-risk-card.legal-unlikely { border-color: var(--success); background: rgba(63, 185, 80, 0.07); }
+ .legal-risk-card.legal-insufficient { border-color: var(--warning); background: rgba(210, 153, 34, 0.07); }
+ .legal-risk-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
+ .legal-risk-title { font-size: 15px; font-weight: 700; }
+ .legal-risk-meta { color: var(--muted); font-size: 11px; margin-top: 3px; }
+ .legal-risk-confidence { font-size: 22px; font-weight: 700; white-space: nowrap; }
+ .legal-risk-section { margin-top: 12px; }
+ .legal-risk-section strong { display: block; color: var(--muted); font-size: 11px; margin-bottom: 5px; }
+ .legal-risk-list { margin: 0; padding-left: 18px; font-size: 12px; }
+ .legal-risk-list li { margin: 3px 0; }
+ .precedent-chip {
+ display: inline-block; margin: 2px 4px 2px 0; padding: 3px 8px;
+ color: var(--accent); background: rgba(88, 166, 255, 0.1);
+ border: 1px solid rgba(88, 166, 255, 0.45); border-radius: 12px; font-size: 11px;
+ }
+ .legal-disclaimer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); color: var(--warning); font-size: 11px; }
+
.loader {
display: inline-block; width: 14px; height: 14px;
border: 2px solid var(--muted); border-top-color: var(--text);
@@ -256,7 +278,7 @@
- ⚙ 고급 옵션 (임계값, 자서전 모드)
+ ⚙ 고급 옵션 (임계값, 자서전·법적 맥락)
@@ -279,6 +301,33 @@
ON일 때 공통 표현 사전 + NER 마스킹으로 거짓 양성 방지
+
+
+
+
+
+
+
+
+
+
+
+
+ 사람이 실제로 확인한 사실만 선택하세요. 서버와 GPT는 미제공 사실을 추론하지 않습니다.
+
@@ -305,6 +354,9 @@
매칭된 레퍼런스
+ 판례 기반 법적 검토 보조
+
+
추출된 구성요소
@@ -453,6 +505,14 @@ async function runDetect() {
text: text,
metadata: (title || author) ? { title: title || null, author: author || null } : null,
options: options,
+ legal_context: {
+ work_type: document.getElementById("legal-work-type").value,
+ access_evidence: document.getElementById("legal-access-evidence").value === "unknown"
+ ? null
+ : document.getElementById("legal-access-evidence").value === "true",
+ protected_expression_reviewed: document.getElementById("legal-protected-reviewed").checked,
+ rights_verified: document.getElementById("legal-rights-verified").checked,
+ },
};
const btn = document.getElementById("submit-btn");
@@ -549,6 +609,8 @@ function renderResult(data, originalText) {
}).join("");
}
+ renderLegalRisk(data.legal_risk);
+
// 추출 요소
const e = data.extracted_elements || {};
const chips = (arr) => (arr || []).map(s => `${escapeHtml(String(s))}`).join("");
@@ -567,6 +629,63 @@ function renderResult(data, originalText) {
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
}
+function renderLegalRisk(risk) {
+ const el = document.getElementById("legal-risk");
+ if (!risk) {
+ el.innerHTML = '';
+ return;
+ }
+
+ const verdict = risk.llm_verdict || "insufficient_evidence";
+ const verdictLabels = {
+ likely: "침해 가능성 검토 필요",
+ unlikely: "침해 가능성 낮음",
+ insufficient_evidence: "판단 근거 부족",
+ };
+ const verdictClass = {
+ likely: "legal-likely",
+ unlikely: "legal-unlikely",
+ insufficient_evidence: "legal-insufficient",
+ }[verdict] || "legal-insufficient";
+ const methodLabels = {
+ llm: "GPT 판례 비교",
+ rule_based: "규칙 기반",
+ rule_fallback: "GPT 실패 · 규칙 폴백",
+ };
+ const selectedIds = risk.llm_matched_precedent_ids && risk.llm_matched_precedent_ids.length
+ ? risk.llm_matched_precedent_ids
+ : (risk.precedent_ids || []);
+ const confidence = Number.isFinite(risk.llm_confidence)
+ ? `${(risk.llm_confidence * 100).toFixed(0)} / 100 자기평가
`
+ : "";
+ const list = (items, emptyText) => items && items.length
+ ? `${items.map(item => `- ${escapeHtml(item)}
`).join("")}
`
+ : `${escapeHtml(emptyText)}
`;
+ const precedents = selectedIds.length
+ ? selectedIds.map(id => `${escapeHtml(id)}`).join("")
+ : '선택된 관련 판례 없음
';
+ const modelMeta = risk.judge_model
+ ? ` · ${escapeHtml(risk.judge_model)} · ${escapeHtml(risk.judge_prompt_version || "prompt")}`
+ : "";
+
+ el.innerHTML = `
+
+
+
+
${escapeHtml(verdictLabels[verdict] || verdict)}
+
${escapeHtml(methodLabels[risk.judgment_method] || risk.judgment_method || "판단 방식 미상")} · 위험도 ${escapeHtml(risk.risk_level || "미정")}${modelMeta}
+
+ ${confidence}
+
+
관련 판례 사건번호${precedents}
+
판단을 지지하는 근거${list(risk.supporting_reasons, "GPT 근거 없음 또는 규칙 기반 결과")}
+
반대 근거${list(risk.counter_reasons, "제공된 반대 근거 없음")}
+
추가 확인 필요${list(risk.missing_factors, "추가 확인사항 없음")}
+ ${risk.judge_note ? `
${escapeHtml(risk.judge_note)}
` : ""}
+
${escapeHtml(risk.disclaimer || "판례 기반 검토 보조이며 법률상 침해 확정이 아닙니다.")}
+
`;
+}
+
function renderAiGeneration(ai, originalText) {
const el = document.getElementById("ai-generation");
if (!ai || !ai.available) {
diff --git a/tests/test_frontend_legal_risk.py b/tests/test_frontend_legal_risk.py
new file mode 100644
index 0000000..32963cf
--- /dev/null
+++ b/tests/test_frontend_legal_risk.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+
+
+HTML = Path("app/static/index.html").read_text(encoding="utf-8")
+
+
+def test_frontend_sends_human_verified_legal_context():
+ for field_id in (
+ "legal-work-type",
+ "legal-access-evidence",
+ "legal-protected-reviewed",
+ "legal-rights-verified",
+ ):
+ assert f'id="{field_id}"' in HTML
+ assert "legal_context:" in HTML
+
+
+def test_frontend_renders_llm_legal_judgment_and_safety_fields():
+ assert 'id="legal-risk"' in HTML
+ assert "function renderLegalRisk" in HTML
+ for field in (
+ "llm_verdict",
+ "llm_confidence",
+ "llm_matched_precedent_ids",
+ "supporting_reasons",
+ "counter_reasons",
+ "missing_factors",
+ "disclaimer",
+ ):
+ assert field in HTML
+ assert "escapeHtml(item)" in HTML
+ assert "법률상 침해 확정이 아닙니다" in HTML