From 3b549dc0c0d0e8eb607240af3bd9a1a91874cac0 Mon Sep 17 00:00:00 2001 From: hbyang Date: Tue, 18 Aug 2026 11:29:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?GPT=20=ED=8C=90=EB=A1=80=20=ED=8C=90=EB=8B=A8=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/index.html | 123 +++++++++++++++++++++++++++++- tests/test_frontend_legal_risk.py | 32 ++++++++ 2 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 tests/test_frontend_legal_risk.py 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) + ? `` + : ""; + const list = (items, emptyText) => items && items.length + ? `` + : ``; + 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 = ` + `; +} + 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