feat: 프론트에 GPT 판례 판단 표시
This commit is contained in:
parent
ee2f85a2d9
commit
3b549dc0c0
@ -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 @@
|
||||
<textarea id="text" placeholder="검사할 본문을 붙여넣으세요. 최소 1자."></textarea>
|
||||
|
||||
<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;">
|
||||
<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;">
|
||||
@ -279,6 +301,33 @@
|
||||
<div style="font-size: 11px; color: var(--muted); margin-top: 4px;">
|
||||
ON일 때 공통 표현 사전 + NER 마스킹으로 거짓 양성 방지
|
||||
</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>
|
||||
</details>
|
||||
|
||||
@ -305,6 +354,9 @@
|
||||
<h3 style="font-size: 13px; color: var(--muted); margin: 18px 0 8px;">매칭된 레퍼런스</h3>
|
||||
<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>
|
||||
<div id="elements"></div>
|
||||
|
||||
@ -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 => `<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);
|
||||
}
|
||||
|
||||
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) {
|
||||
const el = document.getElementById("ai-generation");
|
||||
if (!ai || !ai.available) {
|
||||
|
||||
32
tests/test_frontend_legal_risk.py
Normal file
32
tests/test_frontend_legal_risk.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user