o2o-plagiarism-ai/app/static/institution_briefing.html
2026-08-14 13:11:19 +09:00

1395 lines
61 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="O2O 표절탐지 엔진 기관 설명 및 질의응답 자료">
<title>O2O 표절탐지 엔진 — 기관 브리핑</title>
<style>
:root {
--ink: #102a43;
--muted: #627d98;
--line: #d9e2ec;
--paper: #f5f8fb;
--white: #ffffff;
--navy: #0b3558;
--blue: #1277c4;
--blue-soft: #e7f3fb;
--cyan: #12a8b4;
--green: #16835b;
--green-soft: #e7f7f0;
--orange: #e67e22;
--orange-soft: #fff1e4;
--red: #c83e4d;
--red-soft: #fcebee;
--shadow: 0 18px 50px rgba(31, 71, 103, .10);
--radius: 22px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--ink);
background:
radial-gradient(circle at 7% 4%, rgba(18, 119, 196, .10), transparent 28rem),
radial-gradient(circle at 95% 28%, rgba(18, 168, 180, .08), transparent 25rem),
var(--paper);
font-family: Pretendard, "Noto Sans KR", "Apple SD Gothic Neo", system-ui, sans-serif;
line-height: 1.65;
word-break: keep-all;
}
button, input { font: inherit; }
a { color: inherit; }
.wrap { width: min(1180px, calc(100% - 40px)); margin: 0 auto; }
.topbar {
position: sticky;
top: 0;
z-index: 50;
background: rgba(245, 248, 251, .88);
border-bottom: 1px solid rgba(217, 226, 236, .8);
backdrop-filter: blur(16px);
}
.topbar-inner {
min-height: 68px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand { display: flex; align-items: center; gap: 12px; font-weight: 850; letter-spacing: -.03em; }
.brand-mark {
width: 38px;
height: 38px;
display: grid;
place-items: center;
color: white;
border-radius: 12px;
background: linear-gradient(145deg, var(--navy), var(--blue));
box-shadow: 0 8px 18px rgba(18, 119, 196, .22);
font-size: 13px;
}
nav { display: flex; align-items: center; gap: 22px; color: var(--muted); font-size: 14px; }
nav a { text-decoration: none; }
nav a:hover { color: var(--blue); }
.hero {
padding: 86px 0 64px;
display: grid;
grid-template-columns: 1.18fr .82fr;
align-items: center;
gap: 64px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 11px;
border: 1px solid #b9d9ed;
border-radius: 999px;
background: rgba(255, 255, 255, .72);
color: var(--blue);
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
}
.eyebrow::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 0 5px rgba(18, 168, 180, .12);
}
h1 {
margin: 21px 0 20px;
font-size: clamp(42px, 5.6vw, 70px);
line-height: 1.08;
letter-spacing: -.055em;
}
h1 span { color: var(--blue); }
.hero-copy { max-width: 660px; color: var(--muted); font-size: 18px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 46px;
padding: 0 18px;
border: 0;
border-radius: 13px;
cursor: pointer;
text-decoration: none;
font-weight: 800;
}
.button.primary { color: white; background: var(--navy); box-shadow: 0 10px 24px rgba(11, 53, 88, .18); }
.button.secondary { color: var(--navy); background: white; border: 1px solid var(--line); }
.button:hover { transform: translateY(-1px); }
.hero-card {
position: relative;
min-height: 360px;
overflow: hidden;
padding: 32px;
border: 1px solid rgba(255,255,255,.18);
border-radius: 30px;
color: white;
background: linear-gradient(145deg, #092b48, #0e527e 65%, #0b7f89);
box-shadow: 0 25px 70px rgba(11, 53, 88, .24);
}
.hero-card::after {
content: "";
position: absolute;
width: 260px;
height: 260px;
right: -100px;
top: -90px;
border: 1px solid rgba(255,255,255,.18);
border-radius: 50%;
box-shadow: 0 0 0 45px rgba(255,255,255,.03), 0 0 0 90px rgba(255,255,255,.025);
}
.hero-card-label { color: #b9e6ef; font-size: 12px; font-weight: 800; letter-spacing: .12em; }
.hero-card h2 { margin: 10px 0 24px; font-size: 25px; line-height: 1.3; letter-spacing: -.03em; }
.metric-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; position: relative; z-index: 2; }
.metric {
padding: 16px;
border: 1px solid rgba(255,255,255,.13);
border-radius: 16px;
background: rgba(255,255,255,.07);
}
.metric strong { display: block; font-size: 27px; letter-spacing: -.04em; }
.metric span { color: #c7e6f0; font-size: 12px; }
.scope-note {
position: relative;
z-index: 2;
margin-top: 17px;
padding: 13px 14px;
border-radius: 14px;
color: #d8f1f3;
background: rgba(0,0,0,.16);
font-size: 12px;
}
section { padding: 66px 0; }
.section-head { max-width: 760px; margin-bottom: 28px; }
.kicker { color: var(--blue); font-size: 12px; font-weight: 900; letter-spacing: .13em; }
h2.section-title { margin: 7px 0 10px; font-size: clamp(28px, 3vw, 42px); line-height: 1.22; letter-spacing: -.045em; }
.section-desc { color: var(--muted); }
.pipeline-shell {
padding: 28px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(255,255,255,.82);
box-shadow: var(--shadow);
}
.pipeline-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 26px;
}
.live-label { display: flex; align-items: center; gap: 9px; font-size: 13px; color: var(--muted); }
.live-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--green);
box-shadow: 0 0 0 6px rgba(22, 131, 91, .12);
animation: pulse 1.7s infinite;
}
@keyframes pulse { 50% { box-shadow: 0 0 0 10px rgba(22, 131, 91, 0); } }
.controls { display: flex; gap: 8px; }
.control {
min-width: 78px;
padding: 8px 12px;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--ink);
background: white;
cursor: pointer;
font-size: 12px;
font-weight: 800;
}
.control:hover { border-color: var(--blue); color: var(--blue); }
.pipeline {
position: relative;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
padding-top: 28px;
}
.track {
position: absolute;
top: 10px;
left: 7%;
right: 7%;
height: 3px;
border-radius: 2px;
background: #d9e8f2;
}
.track-fill {
width: 0;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, var(--blue), var(--cyan));
transition: width .7s ease;
}
.packet {
position: absolute;
z-index: 4;
left: 7%;
top: 1px;
width: 20px;
height: 20px;
transform: translateX(-50%);
border: 4px solid white;
border-radius: 50%;
background: var(--blue);
box-shadow: 0 3px 12px rgba(18,119,196,.4);
transition: left .7s cubic-bezier(.2,.7,.2,1), background .25s;
}
.stage {
position: relative;
min-height: 230px;
padding: 18px 15px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--white);
transition: transform .35s ease, border-color .35s ease, box-shadow .35s ease, background .35s ease;
}
.stage.active {
transform: translateY(-7px);
border-color: #86c5eb;
background: #f8fcff;
box-shadow: 0 14px 28px rgba(18, 119, 196, .12);
}
.stage.done { border-color: #abdcca; }
.stage-no { color: var(--blue); font-size: 11px; font-weight: 900; letter-spacing: .12em; }
.stage-icon {
width: 45px;
height: 45px;
display: grid;
place-items: center;
margin: 14px 0;
border-radius: 14px;
color: var(--blue);
background: var(--blue-soft);
font-size: 21px;
}
.stage.active .stage-icon { color: white; background: linear-gradient(145deg, var(--blue), var(--cyan)); }
.stage h3 { margin: 0 0 7px; font-size: 16px; letter-spacing: -.025em; }
.stage p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.58; }
.stage-output {
margin-top: 13px;
padding-top: 10px;
border-top: 1px dashed var(--line);
color: var(--blue);
font: 700 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
}
.explain-panel {
margin-top: 20px;
padding: 18px 20px;
border-radius: 16px;
color: white;
background: var(--navy);
display: grid;
grid-template-columns: 120px 1fr;
gap: 20px;
align-items: center;
}
.explain-panel .step-now { color: #8edee3; font-size: 12px; font-weight: 900; letter-spacing: .08em; }
.explain-panel strong { display: block; margin-top: 3px; font-size: 18px; }
.explain-panel p { margin: 0; color: #c9deec; font-size: 13px; }
.implementation-shell {
position: relative;
overflow: hidden;
padding: 28px;
border: 1px solid var(--line);
border-radius: var(--radius);
background:
linear-gradient(rgba(255,255,255,.93), rgba(255,255,255,.93)),
linear-gradient(90deg, rgba(18,119,196,.08) 1px, transparent 1px),
linear-gradient(rgba(18,119,196,.08) 1px, transparent 1px);
background-size: auto, 24px 24px, 24px 24px;
box-shadow: var(--shadow);
}
.implementation-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 18px;
}
.implementation-toolbar strong { display: block; font-size: 15px; }
.implementation-toolbar span { color: var(--muted); font-size: 12px; }
.implementation-visual {
position: relative;
min-height: 390px;
}
.implementation-route {
position: absolute;
z-index: 0;
inset: 0;
width: 100%;
height: 100%;
overflow: visible;
pointer-events: none;
}
.implementation-route .route-base {
fill: none;
stroke: #d6e6f0;
stroke-width: 8;
stroke-linecap: round;
stroke-linejoin: round;
}
.implementation-route .route-flow {
fill: none;
stroke: url(#flowGradient);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 8 13;
animation: routeDash 1.1s linear infinite;
}
@keyframes routeDash { to { stroke-dashoffset: -21; } }
.implementation-route .route-packet {
fill: var(--cyan);
stroke: white;
stroke-width: 4;
filter: drop-shadow(0 4px 5px rgba(18,119,196,.35));
}
.implementation-route .route-halo {
fill: rgba(18,168,180,.16);
transform-origin: center;
animation: routeHalo 1.2s ease-in-out infinite;
}
@keyframes routeHalo { 50% { r: 13px; opacity: .3; } }
.implementation-grid {
position: absolute;
z-index: 2;
inset: 16px 10px;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(2, 150px);
align-content: space-between;
gap: 56px 28px;
pointer-events: none;
}
.code-node {
position: relative;
min-width: 0;
padding: 14px;
border: 1px solid #cfdee8;
border-radius: 15px;
background: rgba(255,255,255,.98);
box-shadow: 0 7px 18px rgba(31,71,103,.08);
pointer-events: auto;
transition: transform .35s ease, border-color .35s, box-shadow .35s, background .35s;
}
.code-node:nth-child(6) { grid-column: 5; grid-row: 2; }
.code-node:nth-child(7) { grid-column: 4; grid-row: 2; }
.code-node:nth-child(8) { grid-column: 3; grid-row: 2; }
.code-node:nth-child(9) { grid-column: 2; grid-row: 2; }
.code-node:nth-child(10) { grid-column: 1; grid-row: 2; }
.code-node.active {
transform: scale(1.055);
border-color: var(--cyan);
background: #f7feff;
box-shadow: 0 0 0 4px rgba(18,168,180,.10), 0 15px 32px rgba(18,119,196,.17);
}
.code-node.done { border-color: #a9d7c5; }
.code-step {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
color: var(--blue);
font-size: 10px;
font-weight: 900;
letter-spacing: .08em;
}
.code-kind {
padding: 2px 6px;
border-radius: 6px;
color: var(--muted);
background: #edf3f7;
font-size: 8px;
letter-spacing: .04em;
}
.code-node h3 { margin: 10px 0 3px; font-size: 14px; line-height: 1.3; letter-spacing: -.02em; }
.code-node code {
display: block;
overflow: hidden;
color: var(--blue);
font: 700 10px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
text-overflow: ellipsis;
white-space: nowrap;
}
.code-node p { margin: 8px 0 0; color: var(--muted); font-size: 10px; line-height: 1.45; }
.implementation-detail {
display: grid;
grid-template-columns: 135px 1fr 1fr;
gap: 18px;
align-items: center;
margin-top: 18px;
padding: 17px 19px;
border-radius: 15px;
color: white;
background: var(--navy);
}
.implementation-detail .detail-label { color: #8edee3; font-size: 10px; font-weight: 900; letter-spacing: .09em; }
.implementation-detail strong { display: block; margin-top: 3px; font-size: 16px; }
.implementation-detail div:not(:first-child) { padding-left: 18px; border-left: 1px solid rgba(255,255,255,.15); }
.implementation-detail span { display: block; color: #c9deec; font-size: 11px; }
.implementation-legend {
display: flex;
flex-wrap: wrap;
gap: 8px 16px;
margin-top: 15px;
color: var(--muted);
font-size: 11px;
}
.implementation-legend span { display: inline-flex; align-items: center; gap: 6px; }
.implementation-legend i { width: 9px; height: 9px; border-radius: 3px; background: var(--blue-soft); border: 1px solid #9ccdea; }
.implementation-legend span:nth-child(2) i { background: var(--green-soft); border-color: #9fd0bd; }
.implementation-legend span:nth-child(3) i { background: var(--orange-soft); border-color: #ebba86; }
.lab-grid { display: grid; grid-template-columns: .9fr 1.1fr; gap: 22px; }
.card {
border: 1px solid var(--line);
border-radius: var(--radius);
background: rgba(255,255,255,.9);
box-shadow: var(--shadow);
}
.scenario-card { padding: 26px; }
.scenario-card h3, .result-card h3 { margin: 0 0 8px; font-size: 20px; letter-spacing: -.03em; }
.small { color: var(--muted); font-size: 13px; }
.scenario-list { display: grid; gap: 9px; margin-top: 22px; }
.scenario {
width: 100%;
padding: 14px 15px;
text-align: left;
border: 1px solid var(--line);
border-radius: 13px;
background: white;
cursor: pointer;
color: var(--ink);
}
.scenario strong { display: block; font-size: 14px; }
.scenario span { color: var(--muted); font-size: 11px; }
.scenario.active { border-color: var(--blue); background: var(--blue-soft); box-shadow: inset 4px 0 var(--blue); }
.scenario-note {
margin-top: 16px;
padding: 13px;
border-radius: 12px;
background: var(--orange-soft);
color: #8b4d12;
font-size: 12px;
}
.result-card { overflow: hidden; }
.result-head {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 25px 26px 18px;
border-bottom: 1px solid var(--line);
}
.verdict {
align-self: flex-start;
padding: 7px 11px;
border-radius: 999px;
font-size: 12px;
font-weight: 900;
}
.verdict.risk { color: var(--red); background: var(--red-soft); }
.verdict.clean { color: var(--green); background: var(--green-soft); }
.scores { padding: 22px 26px 26px; }
.score {
display: grid;
grid-template-columns: 112px 1fr 48px;
gap: 12px;
align-items: center;
margin: 13px 0;
font-size: 13px;
}
.score-track { height: 9px; overflow: hidden; border-radius: 9px; background: #e9f0f5; }
.score-fill {
height: 100%;
width: 0;
border-radius: inherit;
background: linear-gradient(90deg, var(--blue), var(--cyan));
transition: width .8s cubic-bezier(.2,.8,.2,1);
}
.score-value { text-align: right; font-variant-numeric: tabular-nums; font-weight: 800; }
.formula {
display: grid;
grid-template-columns: 1fr auto;
gap: 16px;
align-items: center;
margin-top: 21px;
padding: 17px;
border-radius: 14px;
color: #d7e9f5;
background: var(--navy);
font-size: 12px;
}
.formula strong { display: block; color: white; font-size: 17px; }
.total-score { color: #8edee3; font-size: 33px; font-weight: 900; letter-spacing: -.04em; }
.signal {
margin-top: 14px;
padding: 14px 16px;
border: 1px solid #b9d9ed;
border-radius: 13px;
background: #f6fbfe;
font-size: 12px;
}
.signal strong { color: var(--blue); }
.principles {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.principle { padding: 24px; }
.principle-num { color: var(--blue); font-size: 12px; font-weight: 900; }
.principle h3 { margin: 10px 0 8px; font-size: 18px; }
.principle p { margin: 0; color: var(--muted); font-size: 13px; }
.status-table {
width: 100%;
overflow: hidden;
border-collapse: separate;
border-spacing: 0;
border: 1px solid var(--line);
border-radius: 18px;
background: white;
box-shadow: var(--shadow);
}
.status-table th, .status-table td { padding: 15px 18px; border-bottom: 1px solid var(--line); text-align: left; font-size: 13px; }
.status-table tr:last-child td { border-bottom: 0; }
.status-table th { color: var(--muted); background: #f8fafc; font-size: 11px; letter-spacing: .05em; }
.status-table td:first-child { font-weight: 800; }
.pill { display: inline-flex; padding: 4px 9px; border-radius: 999px; font-size: 11px; font-weight: 900; white-space: nowrap; }
.pill.done { color: var(--green); background: var(--green-soft); }
.pill.progress { color: #996012; background: var(--orange-soft); }
.pill.pending { color: var(--red); background: var(--red-soft); }
.qa-layout { display: grid; grid-template-columns: 260px 1fr; gap: 24px; align-items: start; }
.qa-nav {
position: sticky;
top: 90px;
padding: 18px;
}
.qa-filter {
display: block;
width: 100%;
margin: 5px 0;
padding: 10px 12px;
border: 0;
border-radius: 10px;
text-align: left;
color: var(--muted);
background: transparent;
cursor: pointer;
font-weight: 750;
}
.qa-filter.active { color: var(--blue); background: var(--blue-soft); }
.qa-list { display: grid; gap: 10px; }
details {
overflow: hidden;
border: 1px solid var(--line);
border-radius: 15px;
background: white;
transition: border-color .2s, box-shadow .2s;
}
details[open] { border-color: #9ccdea; box-shadow: 0 10px 25px rgba(18,119,196,.08); }
summary {
position: relative;
padding: 18px 54px 18px 20px;
cursor: pointer;
list-style: none;
font-weight: 850;
letter-spacing: -.015em;
}
summary::-webkit-details-marker { display: none; }
summary::after {
content: "+";
position: absolute;
top: 14px;
right: 18px;
width: 29px;
height: 29px;
display: grid;
place-items: center;
border-radius: 9px;
color: var(--blue);
background: var(--blue-soft);
font-size: 20px;
}
details[open] summary::after { content: ""; }
.answer { padding: 0 20px 20px; color: var(--muted); font-size: 14px; }
.answer strong { color: var(--ink); }
.answer .say {
margin-top: 11px;
padding: 13px 15px;
border-left: 3px solid var(--blue);
border-radius: 0 10px 10px 0;
color: var(--ink);
background: #f3f8fc;
}
.qa-item.hidden { display: none; }
.warning {
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.warning > div { padding: 28px; }
.dont { color: #7f2632; background: var(--red-soft); }
.do { color: #0d6345; background: var(--green-soft); }
.warning h3 { margin-top: 0; }
.warning ul { margin-bottom: 0; padding-left: 20px; font-size: 13px; }
.check-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
.check {
display: flex;
gap: 13px;
align-items: flex-start;
padding: 17px;
}
.check input { width: 18px; height: 18px; margin-top: 3px; accent-color: var(--green); }
.check strong { display: block; font-size: 14px; }
.check span { color: var(--muted); font-size: 12px; }
footer { margin-top: 60px; padding: 36px 0; color: #bed2df; background: var(--navy); font-size: 12px; }
.footer-inner { display: flex; justify-content: space-between; gap: 20px; }
.reveal { opacity: 0; transform: translateY(18px); transition: opacity .65s ease, transform .65s ease; }
.reveal.visible { opacity: 1; transform: none; }
@media (max-width: 960px) {
nav { display: none; }
.hero, .lab-grid, .qa-layout { grid-template-columns: 1fr; }
.hero { gap: 36px; padding-top: 58px; }
.pipeline { grid-template-columns: repeat(2, 1fr); }
.track, .packet { display: none; }
.stage { min-height: 190px; }
.principles { grid-template-columns: 1fr; }
.qa-nav { position: static; display: flex; flex-wrap: wrap; gap: 4px; }
.qa-filter { width: auto; }
.implementation-visual { min-height: 760px; }
.implementation-route { display: none; }
.implementation-grid {
position: relative;
inset: auto;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: none;
gap: 12px;
}
.code-node:nth-child(n) { grid-column: auto; grid-row: auto; }
.implementation-detail { grid-template-columns: 1fr 1fr; }
.implementation-detail > div:first-child { grid-column: 1 / -1; }
.implementation-detail div:nth-child(2) { padding-left: 0; border-left: 0; }
}
@media (max-width: 640px) {
.wrap { width: min(100% - 24px, 1180px); }
h1 { font-size: 42px; }
section { padding: 48px 0; }
.hero-card { min-height: auto; padding: 24px; }
.pipeline-shell { padding: 16px; }
.pipeline { grid-template-columns: 1fr; }
.pipeline-top, .footer-inner { align-items: flex-start; flex-direction: column; }
.explain-panel { grid-template-columns: 1fr; }
.score { grid-template-columns: 95px 1fr 42px; }
.status-table { display: block; overflow-x: auto; }
.warning, .check-grid { grid-template-columns: 1fr; }
.implementation-shell { padding: 16px; }
.implementation-toolbar { align-items: flex-start; flex-direction: column; }
.implementation-visual { min-height: auto; }
.implementation-grid { grid-template-columns: 1fr; }
.implementation-detail { grid-template-columns: 1fr; }
.implementation-detail > div:first-child { grid-column: auto; }
.implementation-detail div:not(:first-child) { padding: 10px 0 0; border-left: 0; border-top: 1px solid rgba(255,255,255,.15); }
}
@media print {
.topbar, .hero-actions, .controls, .scenario-card, footer { display: none !important; }
body { background: white; }
section, .hero { padding: 24px 0; }
.hero { grid-template-columns: 1fr 1fr; }
.reveal { opacity: 1; transform: none; }
.card, .pipeline-shell, .status-table { box-shadow: none; break-inside: avoid; }
details { break-inside: avoid; }
details .answer { display: block; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="wrap topbar-inner">
<div class="brand"><span class="brand-mark">O2O</span> 표절탐지 엔진 브리핑</div>
<nav aria-label="페이지 목차">
<a href="#operation">작동 원리</a>
<a href="#implementation">구현 순서</a>
<a href="#status">구현 현황</a>
<a href="#qa">예상 질문</a>
<a href="#checklist">준비 체크</a>
</nav>
</div>
</header>
<main>
<div class="wrap hero">
<div>
<span class="eyebrow">국책과제 기관 대응 자료</span>
<h1>표절을 단정하지 않고<br><span>근거를 구조화</span>합니다.</h1>
<p class="hero-copy">
입력 문서와 기준 코퍼스를 비교해 본문 의미, 형태소 구조, 인물과 모티프의 유사도를 계산하고,
요소만 교체한 부분 표절까지 검토할 수 있도록 원문·점수·일치 구간을 제공합니다.
</p>
<div class="hero-actions">
<a class="button primary" href="#operation">작동 원리 재생 ↓</a>
<a class="button secondary" href="#qa">기관 예상 질문 보기</a>
</div>
</div>
<aside class="hero-card" aria-label="과제 핵심 지표">
<div class="hero-card-label">PROJECT SCOPE · O2O</div>
<h2>스토리 요약·분석 및<br>표절 검출 기술 고도화</h2>
<div class="metric-row">
<div class="metric"><strong>4</strong><span>복합 유사도 신호</span></div>
<div class="metric"><strong>3단</strong><span>후보 검색·정밀 분석·분류</span></div>
<div class="metric"><strong>97%</strong><span>2단계 Precision 목표</span></div>
<div class="metric"><strong>39</strong><span>침해 검토 케이스 체계</span></div>
</div>
<div class="scope-note">97%는 최종 목표치입니다. 현재 달성치로 표현하지 않으며, 실제 자서전 정답셋 기반 평가가 필요합니다.</div>
</aside>
</div>
<section id="operation">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">01 · HOW IT WORKS</div>
<h2 class="section-title">한 문서가 검토 결과가 되기까지</h2>
<div class="section-desc">재생 버튼을 누르면 실제 구현 순서에 따라 데이터가 이동합니다.</div>
</div>
<div class="pipeline-shell reveal">
<div class="pipeline-top">
<div class="live-label"><span class="live-dot"></span><span id="runState">자동 재생 중</span></div>
<div class="controls">
<button class="control" id="toggleAnimation" type="button">일시정지</button>
<button class="control" id="resetAnimation" type="button">처음부터</button>
</div>
</div>
<div class="pipeline" id="pipeline">
<div class="track"><div class="track-fill" id="trackFill"></div></div>
<div class="packet" id="packet" aria-hidden="true"></div>
<article class="stage" data-index="0">
<div class="stage-no">STEP 01</div>
<div class="stage-icon"></div>
<h3>문서 입력</h3>
<p>검사할 본문과 문서 ID를 수신하고 요청 옵션을 확인합니다.</p>
<div class="stage-output">text + doc_id</div>
</article>
<article class="stage" data-index="1">
<div class="stage-no">STEP 02</div>
<div class="stage-icon"></div>
<h3>자서전 전처리</h3>
<p>공통 표현을 제거하고 개체명을 마스킹해 일상 문장 오탐을 줄입니다.</p>
<div class="stage-output">normalized_text</div>
</article>
<article class="stage" data-index="2">
<div class="stage-no">STEP 03</div>
<div class="stage-icon"></div>
<h3>LSH 후보 검색</h3>
<p>MinHash·LSH로 대규모 코퍼스에서 비교 가능성이 있는 원문 후보를 찾습니다.</p>
<div class="stage-output">candidate_docs[]</div>
</article>
<article class="stage" data-index="3">
<div class="stage-no">STEP 04</div>
<div class="stage-icon"></div>
<h3>복합 유사도</h3>
<p>KoSimCSE 의미, lemma 구조, 인물, 모티프 신호를 가중 결합합니다.</p>
<div class="stage-output">weighted_score</div>
</article>
<article class="stage" data-index="4">
<div class="stage-no">STEP 05</div>
<div class="stage-icon"></div>
<h3>군집·부분 표절</h3>
<p>구조는 유지하고 인물·모티프만 교체했는지 요소 단위로 분해합니다.</p>
<div class="stage-output">partial_signal</div>
</article>
<article class="stage" data-index="5">
<div class="stage-no">STEP 06</div>
<div class="stage-icon"></div>
<h3>검토 결과</h3>
<p>원문, 세부 점수, 일치 구간과 검토용 태그를 함께 제공합니다.</p>
<div class="stage-output">evidence + verdict</div>
</article>
</div>
<div class="explain-panel" aria-live="polite">
<div><div class="step-now">현재 단계</div><strong id="activeTitle">문서 입력</strong></div>
<p id="activeDescription">판정에 사용할 원문과 문서 정보를 API에서 수신합니다.</p>
</div>
</div>
</div>
</section>
<section id="implementation">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">02 · IMPLEMENTATION SEQUENCE</div>
<h2 class="section-title">코드에서는 이 순서로 실행됩니다</h2>
<div class="section-desc">
개념 설명을 실제 모듈 호출 순서로 펼친 그림입니다. 빛나는 노드를 따라가면 API 요청부터 응답 생성까지의 구현 경로를 확인할 수 있습니다.
</div>
</div>
<div class="implementation-shell reveal">
<div class="implementation-toolbar">
<div>
<strong>POST /v1/plagiarism/detect</strong>
<span>FastAPI 요청 한 건의 실제 실행 경로</span>
</div>
<div class="controls">
<button class="control" id="implementationToggle" type="button">일시정지</button>
<button class="control" id="implementationReset" type="button">처음부터</button>
</div>
</div>
<div class="implementation-visual">
<svg class="implementation-route" id="implementationRoute" viewBox="0 0 1000 390" preserveAspectRatio="none" aria-hidden="true">
<defs>
<linearGradient id="flowGradient" x1="0" x2="1">
<stop offset="0" stop-color="#1277c4"/>
<stop offset="1" stop-color="#12a8b4"/>
</linearGradient>
<path id="codeRoutePath" d="M100 91 H900 V310 H100"/>
</defs>
<use href="#codeRoutePath" class="route-base"/>
<use href="#codeRoutePath" class="route-flow"/>
<circle class="route-halo" r="10">
<animateMotion dur="20s" repeatCount="indefinite" rotate="auto">
<mpath href="#codeRoutePath"/>
</animateMotion>
</circle>
<circle class="route-packet" r="7">
<animateMotion dur="20s" repeatCount="indefinite" rotate="auto">
<mpath href="#codeRoutePath"/>
</animateMotion>
</circle>
</svg>
<div class="implementation-grid" id="implementationGrid">
<article class="code-node active" data-code-index="0">
<div class="code-step"><span>01 · REQUEST</span><span class="code-kind">API</span></div>
<h3>FastAPI 라우터</h3>
<code>routes.detect()</code>
<p>JSON 요청을 Pydantic 스키마로 검증합니다.</p>
</article>
<article class="code-node" data-code-index="1">
<div class="code-step"><span>02 · ADAPTER</span><span class="code-kind">SCHEMA</span></div>
<h3>요청 어댑터</h3>
<code>detect_request()</code>
<p>doc_id·text·options를 탐지기로 전달합니다.</p>
</article>
<article class="code-node" data-code-index="2">
<div class="code-step"><span>03 · CONTROL</span><span class="code-kind">ENGINE</span></div>
<h3>파이프라인 제어</h3>
<code>detector.detect()</code>
<p>임계값과 자서전 모드를 결정합니다.</p>
</article>
<article class="code-node" data-code-index="3">
<div class="code-step"><span>04 · NORMALIZE</span><span class="code-kind">NLP</span></div>
<h3>자서전 전처리</h3>
<code>preprocess_for_autobiography()</code>
<p>공통 표현 제거와 개체명 마스킹을 수행합니다.</p>
</article>
<article class="code-node" data-code-index="4">
<div class="code-step"><span>05 · FEATURES</span><span class="code-kind">NLP</span></div>
<h3>요소·형태소 추출</h3>
<code>extract() + extract_lemmas()</code>
<p>인물·모티프·키워드와 lemma를 만듭니다.</p>
</article>
<article class="code-node" data-code-index="5">
<div class="code-step"><span>06 · RETRIEVE</span><span class="code-kind">INDEX</span></div>
<h3>후보 원문 검색</h3>
<code>LshIndex.query()</code>
<p>MinHash 기반으로 후보 문서 ID를 찾습니다.</p>
</article>
<article class="code-node" data-code-index="6">
<div class="code-step"><span>07 · SCORE</span><span class="code-kind">MODEL</span></div>
<h3>정밀 유사도 계산</h3>
<code>DualSimilarityIndex.query()</code>
<p>KoSimCSE와 세부 신호를 가중 결합합니다.</p>
</article>
<article class="code-node" data-code-index="7">
<div class="code-step"><span>08 · CLUSTER</span><span class="code-kind">ANALYSIS</span></div>
<h3>부분 표절 분해</h3>
<code>ClusterIndex.partial_signal()</code>
<p>유지·변경된 구성요소를 구분합니다.</p>
</article>
<article class="code-node" data-code-index="8">
<div class="code-step"><span>09 · CLASSIFY</span><span class="code-kind">RULE</span></div>
<h3>태그·케이스 매핑</h3>
<code>_assign_tags() + find_case()</code>
<p>검토용 법령 태그와 케이스를 연결합니다.</p>
</article>
<article class="code-node" data-code-index="9">
<div class="code-step"><span>10 · RESPONSE</span><span class="code-kind">JSON</span></div>
<h3>응답 직렬화</h3>
<code>DetectResponse</code>
<p>점수·원문·증거 구간을 JSON으로 반환합니다.</p>
</article>
</div>
</div>
<div class="implementation-detail" aria-live="polite">
<div>
<div class="detail-label">실행 중인 모듈</div>
<strong id="codeActiveTitle">FastAPI 라우터</strong>
</div>
<div>
<div class="detail-label">INPUT</div>
<span id="codeActiveInput">DetectRequest · JSON 본문</span>
</div>
<div>
<div class="detail-label">OUTPUT</div>
<span id="codeActiveOutput">검증된 doc_id · text · options</span>
</div>
</div>
<div class="implementation-legend">
<span><i></i> API·모델 호출</span>
<span><i></i> 완료된 처리 단계</span>
<span><i></i> 규칙 기반 검토 분류</span>
</div>
</div>
</div>
</section>
<section id="simulator">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">03 · EXPLAINABLE SIGNALS</div>
<h2 class="section-title">시나리오에 따라 달라지는 판단 근거</h2>
<div class="section-desc">기관 시연에서 사용할 수 있는 대표적인 네 가지 상황입니다. 버튼을 눌러 점수 변화를 확인하세요.</div>
</div>
<div class="lab-grid">
<div class="card scenario-card reveal">
<h3>시연 시나리오</h3>
<div class="small">각 결과는 작동 원리를 설명하기 위한 예시 수치입니다.</div>
<div class="scenario-list">
<button class="scenario active" data-scenario="copy" type="button"><strong>원문 그대로 복사</strong><span>본문과 형태소가 모두 높음</span></button>
<button class="scenario" data-scenario="paraphrase" type="button"><strong>어미·표현 변경</strong><span>표면은 낮아져도 lemma가 포착</span></button>
<button class="scenario" data-scenario="swap" type="button"><strong>인물만 교체</strong><span>구조 유지 + 등장인물 변경</span></button>
<button class="scenario" data-scenario="clean" type="button"><strong>비관련 문서</strong><span>모든 신호가 임계값 미만</span></button>
</div>
<div class="scenario-note">시연용 예시와 공식 성능평가 데이터를 혼용하지 않습니다. 공식 수치는 고정 테스트셋과 실행 로그로 제시합니다.</div>
</div>
<div class="card result-card reveal">
<div class="result-head">
<div><h3 id="resultTitle">원문 그대로 복사</h3><div class="small" id="resultSub">문장 구조와 의미가 함께 일치합니다.</div></div>
<span class="verdict risk" id="verdict">검토 필요</span>
</div>
<div class="scores">
<div class="score"><span>본문 의미 · 30%</span><div class="score-track"><div class="score-fill" data-score="text"></div></div><span class="score-value" data-value="text">96%</span></div>
<div class="score"><span>형태소 · 45%</span><div class="score-track"><div class="score-fill" data-score="lemma"></div></div><span class="score-value" data-value="lemma">94%</span></div>
<div class="score"><span>인물 · 15%</span><div class="score-track"><div class="score-fill" data-score="character"></div></div><span class="score-value" data-value="character">86%</span></div>
<div class="score"><span>모티프 · 10%</span><div class="score-track"><div class="score-fill" data-score="motif"></div></div><span class="score-value" data-value="motif">78%</span></div>
<div class="formula">
<div>가중 결합 결과<strong id="formulaText">.30×.96 + .45×.94 + .15×.86 + .10×.78</strong></div>
<div class="total-score" id="totalScore">92%</div>
</div>
<div class="signal"><strong id="signalTitle">near_duplicate</strong><br><span id="signalText">원문과 거의 동일한 문서로 검토 우선순위가 높습니다.</span></div>
</div>
</div>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">04 · RESPONSE PRINCIPLES</div>
<h2 class="section-title">기관에는 세 층위로 답합니다</h2>
</div>
<div class="principles">
<article class="card principle reveal"><div class="principle-num">01 · 기술</div><h3>무엇을 계산하는가</h3><p>모델 이름보다 입력, 전처리, 네 가지 유사도, 임계값, 증거 생성 순서로 설명합니다.</p></article>
<article class="card principle reveal"><div class="principle-num">02 · 검증</div><h3>어떻게 재현하는가</h3><p>고정 데이터셋, 라벨 기준, 실행환경, Confusion Matrix와 원본 로그를 제시합니다.</p></article>
<article class="card principle reveal"><div class="principle-num">03 · 경계</div><h3>무엇을 확정하지 않는가</h3><p>표절 위험 신호와 법률상 저작권 침해 확정을 분리해 과장 없이 설명합니다.</p></article>
</div>
</div>
</section>
<section id="status">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">05 · PLAN VS. IMPLEMENTATION</div>
<h2 class="section-title">계획서 약속과 현재 구현 현황</h2>
<div class="section-desc">“완료”, “골격 준비”, “데이터 필요”를 분리해 답변하는 것이 핵심입니다.</div>
</div>
<table class="status-table reveal">
<thead><tr><th>계획서 항목</th><th>현재 구현</th><th>상태</th><th>기관 대응 표현</th></tr></thead>
<tbody>
<tr><td>복합 유사도 탐지</td><td>KoSimCSE + lemma + 인물 + 모티프</td><td><span class="pill done">구현</span></td><td>세부 점수와 일치 구간 시연 가능</td></tr>
<tr><td>군집 기반 부분 표절</td><td>요소별 군집 및 교체 신호</td><td><span class="pill done">구현</span></td><td>인물 교체 시나리오로 설명</td></tr>
<tr><td>Human Feedback</td><td>선호 데이터 생성·변환 파이프라인</td><td><span class="pill progress">골격 준비</span></td><td>사람 선호 라벨 확보 후 실제 학습</td></tr>
<tr><td>Precision 97%</td><td>평가 도구 및 내부 보고서</td><td><span class="pill pending">최종 검증 필요</span></td><td>자서전 정답셋·고정 임계값으로 평가 예정</td></tr>
<tr><td>39개 침해 케이스</td><td>분류체계와 API 응답 매핑</td><td><span class="pill progress">데이터 필요</span></td><td>케이스별 평가 샘플 확보 필요</td></tr>
<tr><td>기관 통합</td><td>단건·배치·분류체계 API</td><td><span class="pill progress">E2E 필요</span></td><td>외부 DB 계약 및 연계시험 예정</td></tr>
</tbody>
</table>
</div>
</section>
<section id="qa">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">06 · EXPECTED Q&amp;A</div>
<h2 class="section-title">기관 예상 질문과 권장 답변</h2>
<div class="section-desc">질문을 선택하면 답변 요지와 실제 말하기 문장을 함께 확인할 수 있습니다.</div>
</div>
<div class="qa-layout">
<aside class="card qa-nav reveal" aria-label="질문 분류">
<button class="qa-filter active" data-filter="all" type="button">전체 질문</button>
<button class="qa-filter" data-filter="tech" type="button">기술·알고리즘</button>
<button class="qa-filter" data-filter="metric" type="button">데이터·성능</button>
<button class="qa-filter" data-filter="legal" type="button">저작권·책임 경계</button>
<button class="qa-filter" data-filter="security" type="button">보안·운영</button>
</aside>
<div class="qa-list reveal">
<details class="qa-item" data-category="tech" open>
<summary>단순 문장 복사만 찾는 것 아닌가요?</summary>
<div class="answer">문자열 일치만 보는 방식이 아닙니다. 의미 임베딩, 형태소 기본형, 인물, 모티프를 함께 비교합니다.<div class="say">“어미를 바꾼 문장은 lemma 신호로, 인물을 바꾼 문장은 군집 기반 요소 교체 신호로 분석합니다.”</div></div>
</details>
<details class="qa-item" data-category="tech">
<summary>왜 KoSimCSE를 선택했나요?</summary>
<div class="answer">한국어 문장 의미 유사도에 사용할 수 있는 로컬 임베딩 모델이며, 원문을 외부 API에 보내지 않고 처리할 수 있습니다.<div class="say">“한국어 의미 비교 성능, 데이터 외부 전송 최소화, 호출비용 제거를 고려한 구현 선택입니다.”</div></div>
</details>
<details class="qa-item" data-category="tech">
<summary>군집화가 실제로 무엇을 찾아내나요?</summary>
<div class="answer">문서들의 lemma·키워드·인물·모티프 특징을 비교해 본문 구조는 유지하면서 특정 요소만 바뀐 패턴을 찾습니다.<div class="say">“유지된 요소와 변경된 요소를 나눠 element_swap_plagiarism 신호로 제공합니다.”</div></div>
</details>
<details class="qa-item" data-category="metric">
<summary>Precision 97%를 이미 달성했나요?</summary>
<div class="answer"><strong>아직 최종 달성으로 말하면 안 됩니다.</strong> 97%는 계획서의 2단계 목표입니다. 내부 출판 데이터 평가는 존재하지만 자서전 도메인의 고정 정답셋 평가가 필요합니다.<div class="say">“알고리즘과 평가 파이프라인은 준비됐으며, 실제 자서전 표절·비표절 정답셋으로 최종 Precision을 검증할 예정입니다.”</div></div>
</details>
<details class="qa-item" data-category="metric">
<summary>평가 데이터는 어떻게 만들고 분리하나요?</summary>
<div class="answer">원문, 표절 변형문, 비표절문, 변형 유형, 사람 정답 라벨을 갖춘 데이터셋이 필요합니다. 문서 원본 단위로 학습·검증·테스트를 분리해 누수를 막아야 합니다.<div class="say">“동일 원문에서 파생된 문장이 학습과 테스트에 동시에 들어가지 않도록 원문 그룹 단위로 분할합니다.”</div></div>
</details>
<details class="qa-item" data-category="metric">
<summary>임계값 0.85는 어떤 근거인가요?</summary>
<div class="answer">현재 기본값은 정밀도 우선의 보수적 설정입니다. 최종 운영값은 자서전 검증셋에서 임계값별 Precision·Recall 곡선을 확인해 고정해야 합니다.<div class="say">“0.85는 시작점이며, 최종값은 고정 평가셋의 오탐 비용을 반영해 결정하고 변경 이력을 관리합니다.”</div></div>
</details>
<details class="qa-item" data-category="legal">
<summary>엔진이 저작권 침해를 자동 확정하나요?</summary>
<div class="answer">아닙니다. 유사성과 재사용 가능성을 선별하는 검토 지원 엔진입니다. 이용허락, 인용 목적, 계약 관계는 별도 확인이 필요합니다.<div class="say">“자동 결과는 검토 우선순위와 근거를 제공하며, 법률상 침해 확정은 권한 정보와 전문가 검토를 거칩니다.”</div></div>
</details>
<details class="qa-item" data-category="legal">
<summary>법령 태그와 케이스 ID는 법적 판단인가요?</summary>
<div class="answer">유사도 분포를 규칙으로 매핑한 <strong>검토용 추정 태그</strong>입니다. 법률 결론이 아니라 후속 검토를 구조화하기 위한 메타데이터입니다.<div class="say">“태그는 검토 분류 편의를 위한 후보값이며, 확정적 법률 의견으로 사용하지 않습니다.”</div></div>
</details>
<details class="qa-item" data-category="tech">
<summary>생성형 AI가 작성한 글도 판별하나요?</summary>
<div class="answer">AI 작성 여부를 판별하는 탐지기가 아닙니다. 작성 주체와 무관하게 기준 코퍼스의 원문이나 구성요소가 재사용됐는지를 분석합니다.<div class="say">“AI 생성 탐지가 아니라 원저작물 대비 유사성과 요소 재사용 탐지입니다.”</div></div>
</details>
<details class="qa-item" data-category="security">
<summary>원고가 외부 AI 서비스로 전송되나요?</summary>
<div class="answer">기본 의미 유사도는 로컬 KoSimCSE로 처리할 수 있습니다. OpenAI 요소 추출은 선택 기능이므로 운영 정책에 따라 비활성화할 수 있습니다.<div class="say">“기관 운영환경에서는 로컬 처리 구성을 기본으로 하고 외부 호출 여부를 설정과 로그로 통제할 수 있습니다.”</div></div>
</details>
<details class="qa-item" data-category="security">
<summary>코퍼스가 커져도 운영할 수 있나요?</summary>
<div class="answer">LSH 후보 검색 구조는 확장성을 고려했습니다. 다만 현재는 대규모 부하시험 전이므로 응답시간, 메모리, 인덱스 재구축 시간을 별도로 측정해야 합니다.<div class="say">“확장 구조는 구현됐으며, 목표 문서 수를 확정한 뒤 부하시험 수치로 운영 용량을 제시하겠습니다.”</div></div>
</details>
<details class="qa-item" data-category="metric">
<summary>오탐과 미탐은 어떻게 관리하나요?</summary>
<div class="answer">임계값별 Confusion Matrix를 관리하고, 공통 문구·인용·인물 교체 등 유형별 오류를 분류해야 합니다. 사용자 검토 결과는 향후 선호 데이터로 축적할 수 있습니다.<div class="say">“단일 정확도뿐 아니라 표절 유형별 오탐·미탐과 임계값 변화까지 버전별로 관리합니다.”</div></div>
</details>
</div>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">07 · WORDING GUIDE</div>
<h2 class="section-title">표현 하나가 신뢰도를 좌우합니다</h2>
</div>
<div class="warning reveal">
<div class="dont">
<h3>피해야 할 표현</h3>
<ul>
<li>“AI가 저작권 침해를 확정합니다.”</li>
<li>“Precision 97%를 달성했습니다.”</li>
<li>“Human Feedback 학습이 완료됐습니다.”</li>
<li>“모든 종류의 표절을 탐지합니다.”</li>
</ul>
</div>
<div class="do">
<h3>권장 표현</h3>
<ul>
<li>“유사성 근거를 제공하는 검토 지원 엔진입니다.”</li>
<li>“97%는 최종 목표이며 자서전 정답셋 검증이 필요합니다.”</li>
<li>“선호 데이터 파이프라인을 준비한 상태입니다.”</li>
<li>“텍스트 기반 탐지 가능 범위와 한계를 구분합니다.”</li>
</ul>
</div>
</div>
</div>
</section>
<section id="checklist">
<div class="wrap">
<div class="section-head reveal">
<div class="kicker">08 · BEFORE THE REVIEW</div>
<h2 class="section-title">기관 질의 전 준비 체크리스트</h2>
<div class="section-desc">체크 상태는 현재 브라우저에 자동 저장됩니다.</div>
</div>
<div class="check-grid" id="checkGrid">
<label class="card check reveal"><input type="checkbox"><span><strong>고정 평가셋 확정</strong><span>원문 그룹 기준 분할, 표절 유형 및 정답 라벨 포함</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>Precision 보고서</strong><span>임계값별 Precision·Recall·F1 및 Confusion Matrix</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>재현 환경 복구</strong><span>Ubuntu 22.04·Python 3.9·고정 의존성·모델 파일</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>전체 테스트 통과</strong><span>실행 명령, 원본 로그, 커밋 해시 함께 보관</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>5개 시연 시나리오</strong><span>복사·패러프레이즈·인물교체·공통문구·비관련</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>백업 영상과 JSON</strong><span>네트워크·모델 오류에 대비한 시연 백업</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>요구사항 추적표</strong><span>계획서 문구 ↔ 코드 ↔ API ↔ 테스트 연결</span></span></label>
<label class="card check reveal"><input type="checkbox"><span><strong>한계 및 후속계획</strong><span>미검증 항목, 데이터 요청, 통합 일정 명시</span></span></label>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap footer-inner">
<span>O2O 표절탐지 엔진 · 기관 설명용 인터랙티브 브리핑</span>
<span>자동 판정이 아닌 근거 기반 검토 지원</span>
</div>
</footer>
<script>
const stages = [...document.querySelectorAll(".stage")];
const titles = ["문서 입력", "자서전 전처리", "LSH 후보 검색", "복합 유사도 계산", "군집·부분 표절 분석", "근거 기반 결과"];
const descriptions = [
"판정에 사용할 원문과 문서 정보를 API에서 수신합니다.",
"자서전의 상투적 공통 문구와 개체명 영향을 줄여 오탐 가능성을 낮춥니다.",
"MinHash·LSH로 정밀 비교할 레퍼런스 후보를 빠르게 선별합니다.",
"본문 의미 30%, lemma 45%, 인물 15%, 모티프 10%를 가중 결합합니다.",
"본문 구조는 유지됐지만 인물이나 모티프가 교체된 패턴을 요소 단위로 확인합니다.",
"판정 점수뿐 아니라 원문, 세부 점수, 일치 구간과 검토용 태그를 반환합니다."
];
const positions = [7, 24.2, 41.4, 58.6, 75.8, 93];
let stageIndex = 0;
let playing = true;
let timer;
function renderStage(index) {
stageIndex = index;
stages.forEach((stage, i) => {
stage.classList.toggle("active", i === index);
stage.classList.toggle("done", i < index);
});
document.getElementById("packet").style.left = positions[index] + "%";
document.getElementById("trackFill").style.width = ((positions[index] - 7) / 86 * 100) + "%";
document.getElementById("activeTitle").textContent = titles[index];
document.getElementById("activeDescription").textContent = descriptions[index];
}
function schedule() {
clearInterval(timer);
if (!playing) return;
timer = setInterval(() => renderStage((stageIndex + 1) % stages.length), 2100);
}
document.getElementById("toggleAnimation").addEventListener("click", event => {
playing = !playing;
event.currentTarget.textContent = playing ? "일시정지" : "계속 재생";
document.getElementById("runState").textContent = playing ? "자동 재생 중" : "재생 일시정지";
schedule();
});
document.getElementById("resetAnimation").addEventListener("click", () => {
renderStage(0);
playing = true;
document.getElementById("toggleAnimation").textContent = "일시정지";
document.getElementById("runState").textContent = "자동 재생 중";
schedule();
});
stages.forEach((stage, index) => stage.addEventListener("click", () => renderStage(index)));
renderStage(0);
schedule();
const codeNodes = [...document.querySelectorAll(".code-node")];
const codeTitles = [
"FastAPI 라우터", "요청 어댑터", "파이프라인 제어", "자서전 전처리", "요소·형태소 추출",
"후보 원문 검색", "정밀 유사도 계산", "부분 표절 분해", "태그·케이스 매핑", "응답 직렬화"
];
const codeInputs = [
"DetectRequest · JSON 본문",
"검증된 DetectRequest",
"doc_id · text · metadata · options",
"원본 text · 자서전 모드 설정",
"원본 text · 정규화 text",
"정규화 text · MinHash 인덱스",
"후보 문서 · 요소 · lemma · 임베딩",
"SimilarityHit · query 요소",
"세부 유사도 · 임계값 통과 match",
"MatchResult[] · 분석 메타데이터"
];
const codeOutputs = [
"검증된 doc_id · text · options",
"PlagiarismDetector.detect() 호출",
"threshold · autobio_mode",
"query_text",
"ExtractedElements · query_lemmas",
"candidate_ids · LSH Jaccard",
"가중 score · evidence spans",
"partial_signal · 유지/변경 요소",
"tags · case_id · case_title",
"DetectResponse · JSON"
];
const codeRouteTimes = [0, 2.2, 4.4, 6.6, 8.8, 11.2, 13.4, 15.6, 17.8, 19.8];
const implementationSvg = document.getElementById("implementationRoute");
let codeIndex = 0;
let codePlaying = true;
let codeTimer;
function renderCodeStep(index) {
codeIndex = index;
codeNodes.forEach((node, i) => {
node.classList.toggle("active", i === index);
node.classList.toggle("done", i < index);
});
document.getElementById("codeActiveTitle").textContent = codeTitles[index];
document.getElementById("codeActiveInput").textContent = codeInputs[index];
document.getElementById("codeActiveOutput").textContent = codeOutputs[index];
}
function scheduleCodeSequence() {
clearInterval(codeTimer);
if (!codePlaying) return;
codeTimer = setInterval(() => renderCodeStep((codeIndex + 1) % codeNodes.length), 2000);
}
document.getElementById("implementationToggle").addEventListener("click", event => {
codePlaying = !codePlaying;
event.currentTarget.textContent = codePlaying ? "일시정지" : "계속 재생";
if (typeof implementationSvg.pauseAnimations === "function") {
codePlaying ? implementationSvg.unpauseAnimations() : implementationSvg.pauseAnimations();
}
scheduleCodeSequence();
});
document.getElementById("implementationReset").addEventListener("click", () => {
renderCodeStep(0);
codePlaying = true;
document.getElementById("implementationToggle").textContent = "일시정지";
if (typeof implementationSvg.setCurrentTime === "function") {
implementationSvg.setCurrentTime(0);
implementationSvg.unpauseAnimations();
}
scheduleCodeSequence();
});
codeNodes.forEach((node, index) => node.addEventListener("click", () => {
renderCodeStep(index);
if (typeof implementationSvg.setCurrentTime === "function") implementationSvg.setCurrentTime(codeRouteTimes[index]);
}));
renderCodeStep(0);
scheduleCodeSequence();
const scenarioData = {
copy: {
title: "원문 그대로 복사", sub: "문장 구조와 의미가 함께 일치합니다.",
scores: { text: 96, lemma: 94, character: 86, motif: 78 },
verdict: "검토 필요", clean: false, signal: "near_duplicate",
note: "원문과 거의 동일한 문서로 검토 우선순위가 높습니다."
},
paraphrase: {
title: "어미·표현 변경", sub: "표면 표현은 달라도 형태소 구조가 남습니다.",
scores: { text: 68, lemma: 91, character: 73, motif: 70 },
verdict: "검토 필요", clean: false, signal: "structural_similarity",
note: "lemma 유사도가 높아 패러프레이즈 또는 어미 변경 가능성을 확인합니다."
},
swap: {
title: "인물만 교체", sub: "본문·키워드는 유지하고 등장인물이 달라집니다.",
scores: { text: 72, lemma: 89, character: 12, motif: 76 },
verdict: "요소 교체 의심", clean: false, signal: "element_swap_plagiarism",
note: "구조와 모티프는 유지됐지만 인물 요소가 변경된 패턴입니다."
},
clean: {
title: "비관련 문서", sub: "모든 비교 신호가 낮게 나타납니다.",
scores: { text: 19, lemma: 14, character: 0, motif: 10 },
verdict: "유의 매칭 없음", clean: true, signal: "none",
note: "설정된 검토 임계값을 넘는 레퍼런스가 없습니다."
}
};
function setScenario(key) {
const data = scenarioData[key];
document.querySelectorAll(".scenario").forEach(button => button.classList.toggle("active", button.dataset.scenario === key));
document.getElementById("resultTitle").textContent = data.title;
document.getElementById("resultSub").textContent = data.sub;
const verdict = document.getElementById("verdict");
verdict.textContent = data.verdict;
verdict.className = "verdict " + (data.clean ? "clean" : "risk");
const weights = { text: .30, lemma: .45, character: .15, motif: .10 };
let total = 0;
Object.entries(data.scores).forEach(([name, value]) => {
document.querySelector(`[data-score="${name}"]`).style.width = value + "%";
document.querySelector(`[data-value="${name}"]`).textContent = value + "%";
total += value * weights[name];
});
const s = data.scores;
document.getElementById("formulaText").textContent = `.30×.${String(s.text).padStart(2,"0")} + .45×.${String(s.lemma).padStart(2,"0")} + .15×.${String(s.character).padStart(2,"0")} + .10×.${String(s.motif).padStart(2,"0")}`;
document.getElementById("totalScore").textContent = Math.round(total) + "%";
document.getElementById("signalTitle").textContent = data.signal;
document.getElementById("signalText").textContent = data.note;
}
document.querySelectorAll(".scenario").forEach(button => button.addEventListener("click", () => setScenario(button.dataset.scenario)));
requestAnimationFrame(() => setScenario("copy"));
document.querySelectorAll(".qa-filter").forEach(button => {
button.addEventListener("click", () => {
document.querySelectorAll(".qa-filter").forEach(item => item.classList.remove("active"));
button.classList.add("active");
const filter = button.dataset.filter;
document.querySelectorAll(".qa-item").forEach(item => item.classList.toggle("hidden", filter !== "all" && item.dataset.category !== filter));
});
});
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add("visible");
observer.unobserve(entry.target);
}
});
}, { threshold: .12 });
document.querySelectorAll(".reveal").forEach(element => observer.observe(element));
const checklist = [...document.querySelectorAll("#checkGrid input")];
try {
const saved = JSON.parse(localStorage.getItem("o2oInstitutionChecklist") || "[]");
checklist.forEach((item, index) => item.checked = Boolean(saved[index]));
checklist.forEach(item => item.addEventListener("change", () => {
localStorage.setItem("o2oInstitutionChecklist", JSON.stringify(checklist.map(input => input.checked)));
}));
} catch (_) {
// localStorage가 차단된 브라우저에서도 체크 동작 자체는 유지한다.
}
</script>
</body>
</html>