feat: 프론트에 GPT 판례 판단 표시

This commit is contained in:
hbyang 2026-08-18 11:29:04 +09:00
parent ee2f85a2d9
commit 3b549dc0c0
2 changed files with 153 additions and 2 deletions

View File

@ -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; } .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); } 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; width: 100%; padding: 10px 12px; margin-top: 4px;
background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
font-family: inherit; font-size: 13px; font-family: inherit; font-size: 13px;
@ -194,6 +194,28 @@
border-radius: 4px; font-size: 12px; line-height: 1.6; margin-top: 14px; 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 { .loader {
display: inline-block; width: 14px; height: 14px; display: inline-block; width: 14px; height: 14px;
border: 2px solid var(--muted); border-top-color: var(--text); border: 2px solid var(--muted); border-top-color: var(--text);
@ -256,7 +278,7 @@
<textarea id="text" placeholder="검사할 본문을 붙여넣으세요. 최소 1자."></textarea> <textarea id="text" placeholder="검사할 본문을 붙여넣으세요. 최소 1자."></textarea>
<details style="margin-top: 14px;"> <details style="margin-top: 14px;">
<summary style="cursor: pointer; font-size: 12px; color: var(--accent); user-select: none;">⚙ 고급 옵션 (임계값, 자서전 모드)</summary> <summary style="cursor: pointer; font-size: 12px; color: var(--accent); user-select: none;">⚙ 고급 옵션 (임계값, 자서전·법적 맥락)</summary>
<div style="margin-top: 10px; padding: 12px; background: var(--panel-2); border-radius: 6px;"> <div style="margin-top: 10px; padding: 12px; background: var(--panel-2); border-radius: 6px;">
<label style="margin-top: 0;">임계값 (similarity threshold) <span id="threshold-value" style="color: var(--accent); font-weight: 600;">0.85</span></label> <label style="margin-top: 0;">임계값 (similarity threshold) <span id="threshold-value" style="color: var(--accent); font-weight: 600;">0.85</span></label>
<input type="range" id="threshold-slider" min="0.05" max="0.99" step="0.01" value="0.85" style="width: 100%; margin-top: 6px;"> <input type="range" id="threshold-slider" min="0.05" max="0.99" step="0.01" value="0.85" style="width: 100%; margin-top: 6px;">
@ -279,6 +301,33 @@
<div style="font-size: 11px; color: var(--muted); margin-top: 4px;"> <div style="font-size: 11px; color: var(--muted); margin-top: 4px;">
ON일 때 공통 표현 사전 + NER 마스킹으로 거짓 양성 방지 ON일 때 공통 표현 사전 + NER 마스킹으로 거짓 양성 방지
</div> </div>
<label style="margin-top: 14px;">판례 검색용 저작물 유형</label>
<select id="legal-work-type">
<option value="literary">문학·출판 (literary)</option>
<option value="musical">음악 (musical)</option>
<option value="visual">미술·사진·영상 (visual)</option>
<option value="software">컴퓨터프로그램 (software)</option>
</select>
<label style="margin-top: 12px;">원저작물 접근·의거 가능성</label>
<select id="legal-access-evidence">
<option value="unknown">미확인</option>
<option value="true">확인됨</option>
<option value="false">확인되지 않음</option>
</select>
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 10px;">
<label style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); margin: 0;">
<input type="checkbox" id="legal-protected-reviewed"> 보호되는 창작적 표현 검토 완료
</label>
<label style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text); margin: 0;">
<input type="checkbox" id="legal-rights-verified"> 권리·허락·인용 요건 확인 완료
</label>
</div>
<div style="font-size: 11px; color: var(--warning); margin-top: 6px;">
사람이 실제로 확인한 사실만 선택하세요. 서버와 GPT는 미제공 사실을 추론하지 않습니다.
</div>
</div> </div>
</details> </details>
@ -305,6 +354,9 @@
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">매칭된 레퍼런스</h3> <h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">매칭된 레퍼런스</h3>
<div id="matches"></div> <div id="matches"></div>
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">판례 기반 법적 검토 보조</h3>
<div id="legal-risk"></div>
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">추출된 구성요소</h3> <h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">추출된 구성요소</h3>
<div id="elements"></div> <div id="elements"></div>
@ -453,6 +505,14 @@ async function runDetect() {
text: text, text: text,
metadata: (title || author) ? { title: title || null, author: author || null } : null, metadata: (title || author) ? { title: title || null, author: author || null } : null,
options: options, 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"); const btn = document.getElementById("submit-btn");
@ -549,6 +609,8 @@ function renderResult(data, originalText) {
}).join(""); }).join("");
} }
renderLegalRisk(data.legal_risk);
// 추출 요소 // 추출 요소
const e = data.extracted_elements || {}; const e = data.extracted_elements || {};
const chips = (arr) => (arr || []).map(s => `<span class="chip">${escapeHtml(String(s))}</span>`).join(""); const chips = (arr) => (arr || []).map(s => `<span class="chip">${escapeHtml(String(s))}</span>`).join("");
@ -567,6 +629,63 @@ function renderResult(data, originalText) {
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2); document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
} }
function renderLegalRisk(risk) {
const el = document.getElementById("legal-risk");
if (!risk) {
el.innerHTML = '<div class="legal-risk-card"><div class="legal-risk-meta">판례 기반 검토 결과가 없습니다.</div></div>';
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)
? `<div class="legal-risk-confidence">${(risk.llm_confidence * 100).toFixed(0)}<small style="font-size: 11px; color: var(--muted);"> / 100 자기평가</small></div>`
: "";
const list = (items, emptyText) => items && items.length
? `<ul class="legal-risk-list">${items.map(item => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
: `<div class="legal-risk-meta">${escapeHtml(emptyText)}</div>`;
const precedents = selectedIds.length
? selectedIds.map(id => `<span class="precedent-chip">${escapeHtml(id)}</span>`).join("")
: '<div class="legal-risk-meta">선택된 관련 판례 없음</div>';
const modelMeta = risk.judge_model
? ` · ${escapeHtml(risk.judge_model)} · ${escapeHtml(risk.judge_prompt_version || "prompt")}`
: "";
el.innerHTML = `
<div class="legal-risk-card ${verdictClass}">
<div class="legal-risk-head">
<div>
<div class="legal-risk-title">${escapeHtml(verdictLabels[verdict] || verdict)}</div>
<div class="legal-risk-meta">${escapeHtml(methodLabels[risk.judgment_method] || risk.judgment_method || "판단 방식 미상")} · 위험도 ${escapeHtml(risk.risk_level || "미정")}${modelMeta}</div>
</div>
${confidence}
</div>
<div class="legal-risk-section"><strong>관련 판례 사건번호</strong>${precedents}</div>
<div class="legal-risk-section"><strong>판단을 지지하는 근거</strong>${list(risk.supporting_reasons, "GPT 근거 없음 또는 규칙 기반 결과")}</div>
<div class="legal-risk-section"><strong>반대 근거</strong>${list(risk.counter_reasons, "제공된 반대 근거 없음")}</div>
<div class="legal-risk-section"><strong>추가 확인 필요</strong>${list(risk.missing_factors, "추가 확인사항 없음")}</div>
${risk.judge_note ? `<div class="legal-risk-meta" style="margin-top: 10px;">${escapeHtml(risk.judge_note)}</div>` : ""}
<div class="legal-disclaimer">${escapeHtml(risk.disclaimer || "판례 기반 검토 보조이며 법률상 침해 확정이 아닙니다.")}</div>
</div>`;
}
function renderAiGeneration(ai, originalText) { function renderAiGeneration(ai, originalText) {
const el = document.getElementById("ai-generation"); const el = document.getElementById("ai-generation");
if (!ai || !ai.available) { if (!ai || !ai.available) {

View File

@ -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