diff --git a/app/static/index.html b/app/static/index.html
index 1f4fb3e..eade67a 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -92,6 +92,24 @@
.scorebar-row .label { color: var(--muted); }
.scorebar-row .value { text-align: right; font-variant-numeric: tabular-nums; }
+ .ai-card {
+ background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
+ padding: 14px; margin-top: 8px;
+ }
+ .ai-card.ai-high { border-color: var(--danger); background: rgba(248, 81, 73, 0.08); }
+ .ai-card.ai-medium { border-color: var(--warning); background: rgba(210, 153, 34, 0.08); }
+ .ai-card.ai-low { border-color: var(--success); background: rgba(63, 185, 80, 0.08); }
+ .ai-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
+ .ai-title { font-size: 14px; font-weight: 600; }
+ .ai-score { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
+ .ai-note { color: var(--muted); font-size: 11px; margin-top: 8px; line-height: 1.6; }
+ .ai-warning { color: var(--warning); font-size: 11px; margin-top: 6px; }
+ .ai-segment {
+ margin-top: 8px; padding: 8px 10px; background: var(--bg); border-radius: 4px;
+ font-size: 11px; border-left: 3px solid var(--warning);
+ }
+ .ai-segment .meta { color: var(--muted); margin-bottom: 3px; }
+
.match-card {
background: var(--panel-2); border: 1px solid var(--border); border-radius: 6px;
padding: 14px; margin-top: 10px;
@@ -281,6 +299,9 @@
점수 분석 (삼중 유사도 결합)
+ AI 생성 의심도
+
+
매칭된 레퍼런스
@@ -468,18 +489,21 @@ async function runDetect() {
function renderResult(data, originalText) {
const verdict = document.getElementById("verdict");
+ const displaySimilarity = data.review_summary && Number.isFinite(data.review_summary.similarity_percent)
+ ? data.review_summary.similarity_percent
+ : Math.round(data.confidence * 100);
if (data.is_infringement) {
verdict.className = "verdict infringement";
verdict.innerHTML = `
⚠ 저작권 침해 가능성 확인
- 결합 유사도
- ${(data.confidence * 100).toFixed(2)}%
`;
+ 보정 유사도
+ ${displaySimilarity}%
`;
} else {
verdict.className = "verdict clean";
verdict.innerHTML = `
✓ 침해 신호 없음
- 최상위 유사도
- ${(data.confidence * 100).toFixed(2)}%
`;
+ 보정 유사도 · 검색 후보 점수는 침해 확률이 아님
+ ${displaySimilarity}%
`;
}
document.getElementById("result-body").style.display = "block";
@@ -497,6 +521,8 @@ function renderResult(data, originalText) {
breakdownEl.innerHTML = '매칭된 레퍼런스가 없어 점수 분석을 표시할 수 없습니다.
';
}
+ renderAiGeneration(data.ai_generation, originalText);
+
// 매칭 카드
const matchesEl = document.getElementById("matches");
if (!data.matches || data.matches.length === 0) {
@@ -541,6 +567,68 @@ function renderResult(data, originalText) {
document.getElementById("raw-json").textContent = JSON.stringify(data, null, 2);
}
+function renderAiGeneration(ai, originalText) {
+ const el = document.getElementById("ai-generation");
+ if (!ai || !ai.available) {
+ const note = ai && ai.note
+ ? ai.note
+ : "학습된 AI 생성 탐지 모델이 없어 의심도를 산출하지 않습니다.";
+ const version = ai && ai.model_version ? ai.model_version : "unavailable";
+ el.innerHTML = `
+
+
+ 판정 불가 · 모델 준비 전
+ ${escapeHtml(version)}
+
+
${escapeHtml(note)}
+ ${renderAiWarnings(ai && ai.warnings)}
+
`;
+ return;
+ }
+
+ const level = ["low", "medium", "high"].includes(ai.suspicion_level)
+ ? ai.suspicion_level
+ : "unknown";
+ const labels = { low: "낮음", medium: "중간", high: "높음", unknown: "판정 불가" };
+ const score = Number.isFinite(ai.score) ? `${(ai.score * 100).toFixed(1)}%` : "—";
+ const stub = ai.is_stub
+ ? '미검증 휴리스틱'
+ : `${escapeHtml(ai.model_version || "model")}`;
+ const provenance = ai.provenance && ai.provenance !== "unknown"
+ ? ` · 추정 유형 ${escapeHtml(ai.provenance)}`
+ : "";
+ const segments = (ai.segments || [])
+ .filter(s => s.scored && Number.isFinite(s.score))
+ .sort((a, b) => b.score - a.score)
+ .slice(0, 5)
+ .map(s => {
+ const start = Math.max(0, Number(s.start) || 0);
+ const end = Math.max(start, Number(s.end) || start);
+ const snippet = originalText.substring(start, end).trim();
+ return `
+
구간 ${start}–${end}자 · 의심도 ${(s.score * 100).toFixed(1)}%
+ ${escapeHtml(snippet.length > 160 ? snippet.slice(0, 160) + "…" : snippet)}
+
`;
+ }).join("");
+
+ el.innerHTML = `
+
+
+
AI 생성 의심도 ${labels[level]}${stub}
+
${score}
+
+
검토 우선순위 신호이며 AI 작성 여부를 확정하지 않습니다.${provenance}
+ ${ai.note ? `
${escapeHtml(ai.note)}
` : ""}
+ ${renderAiWarnings(ai.warnings)}
+ ${segments}
+
`;
+}
+
+function renderAiWarnings(warnings) {
+ if (!warnings || warnings.length === 0) return "";
+ return warnings.map(w => `⚠ ${escapeHtml(w)}
`).join("");
+}
+
function renderEvidence(originalText, spans) {
if (!spans || spans.length === 0) return "";
// span 정렬 후 마킹
diff --git a/tests/test_static_ui.py b/tests/test_static_ui.py
new file mode 100644
index 0000000..541809b
--- /dev/null
+++ b/tests/test_static_ui.py
@@ -0,0 +1,24 @@
+from pathlib import Path
+
+
+HTML = (Path(__file__).resolve().parents[1] / "app" / "static" / "index.html").read_text(
+ encoding="utf-8"
+)
+
+
+def test_ai_generation_result_has_visible_frontend_section():
+ assert 'id="ai-generation"' in HTML
+ assert "renderAiGeneration(data.ai_generation, originalText)" in HTML
+ assert "판정 불가 · 모델 준비 전" in HTML
+ assert "AI 생성 의심도" in HTML
+
+
+def test_ai_generation_ui_does_not_present_score_as_a_certain_verdict():
+ assert "AI 작성 여부를 확정하지 않습니다" in HTML
+ assert "미검증 휴리스틱" in HTML
+ assert "ai.available" in HTML
+
+
+def test_clean_verdict_uses_calibrated_review_summary_percentage():
+ assert "data.review_summary.similarity_percent" in HTML
+ assert "검색 후보 점수는 침해 확률이 아님" in HTML