1395 lines
61 KiB
HTML
1395 lines
61 KiB
HTML
<!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&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>
|