POSA_Copyrighter/docs/ai-ml-visual-explainer.html
유창욱 3f7b3a9cf2 chore: initial commit of copyrighter (rights_filter)
Image rights / copyright detection system: SQLite store, HTTP app,
search integrations (Naver, Google Custom Search, Google Cloud Vision
web detection), image analysis (fingerprints, face/person detection,
evidence enrichment, risk scoring), an admin/review layer, governance
and retention policies, batch jobs, and a browser-based operator GUI.

This baseline incorporates a full code-review remediation pass
(46 fixes; 358 tests passing). Highlights:

CRITICAL
- Prevent evidence cascade-delete during the schema-constraint
  migration by disabling FK enforcement around the table rebuild.

Security
- Sandbox served media (neutralize stored XSS from uploaded/collected
  SVGs) via CSP + nosniff on the untrusted media routes.
- Strip embedded EXIF/GPS from external image derivatives before they
  are sent to third-party APIs.
- Return a clean 404 (not an uncaught StopIteration) for PATCH on an
  unknown provider.

Correctness
- LLM-summary failures no longer add +30 to the risk score.
- Decode only explicit JS escapes so Korean image URLs are not mangled.
- Consume search quota only after a successful request.
- Naver/Google adapters map responses inside the failure boundary, so a
  malformed response degrades to evidence instead of crashing enrichment.
- Domain-aware provider attribution; face-box IoU de-duplication; count
  searches (not result items); per-box crop isolation; clamp evidence
  confidence and Google CSE num; real submittedEpoch; and more.

Robustness
- Offline LLM connect fast-fails (short connect timeout) so seed/reload
  requests are not stalled; full read timeout preserved for generation.
- Malformed numeric env vars fall back to defaults instead of crashing
  startup.

Performance
- Per-submission evidence reads (no full-table scan per rescore),
  audit-log LIMIT, lazy active-store lookup, hoisted timestamps.

Tests
- ~24 regression tests added pinning the above fixes.

Runtime data (data/, outputs/, *.sqlite3, *.log), secrets (.env), and
node_modules are gitignored.
2026-06-09 09:50:31 +09:00

773 lines
25 KiB
HTML

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Copyrighter AI/ML 사용 설명서</title>
<style>
:root {
--bg: #f4f1ea;
--panel: #ffffff;
--panel-soft: #fbfaf6;
--ink: #172124;
--muted: #5f6b6f;
--line: #d7ccbd;
--accent: #24667a;
--green: #28734f;
--red: #a13d35;
--amber: #916300;
--blue-soft: #edf7fb;
--green-soft: #ecf8f1;
--amber-soft: #fff7e2;
--red-soft: #fff0ed;
--shadow: 0 18px 42px rgba(23, 33, 36, 0.12);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Segoe UI", "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
line-height: 1.58;
}
a {
color: inherit;
}
.shell {
width: min(1180px, calc(100vw - 32px));
margin: 0 auto;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 0.95fr) minmax(420px, 1.05fr);
gap: 30px;
align-items: center;
min-height: 84vh;
padding: 42px 0 26px;
}
.eyebrow {
margin: 0 0 10px;
color: var(--accent);
font-size: 12px;
font-weight: 900;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
h3 {
margin: 0;
line-height: 1.18;
}
h1 {
max-width: 760px;
font-size: clamp(38px, 5.3vw, 70px);
}
h2 {
font-size: clamp(28px, 3.1vw, 42px);
}
h3 {
font-size: 18px;
}
p {
margin: 10px 0 0;
}
.lead {
max-width: 670px;
margin-top: 18px;
color: var(--muted);
font-size: 18px;
}
.hero-card,
.panel,
.image-card,
.claim-card,
.metric-card {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.hero-card {
overflow: hidden;
box-shadow: var(--shadow);
}
.hero-card img,
.image-card img {
display: block;
width: 100%;
height: auto;
}
.caption {
padding: 12px 14px;
color: var(--muted);
font-size: 13px;
}
.answer-strip {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin: 0 0 32px;
}
.verdict,
.warning {
padding: 22px;
border-radius: 8px;
}
.verdict {
border: 1px solid #9ec8b0;
background: var(--green-soft);
}
.warning {
border: 1px solid #e5c08a;
background: var(--amber-soft);
}
.verdict strong,
.warning strong {
display: block;
margin-bottom: 6px;
}
.verdict strong {
color: var(--green);
font-size: 28px;
}
.warning strong {
color: var(--amber);
font-size: 20px;
}
section {
padding: 46px 0;
}
.section-head {
display: grid;
grid-template-columns: minmax(0, 0.72fr) minmax(320px, 0.28fr);
gap: 22px;
align-items: end;
margin-bottom: 18px;
}
.section-head p,
.panel p,
.claim-card p,
.metric-card p,
.plain-list li {
color: var(--muted);
}
.claim-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.claim-card,
.metric-card,
.panel {
padding: 16px;
}
.claim-card strong {
display: block;
margin-bottom: 8px;
font-size: 15px;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 24px;
margin-bottom: 12px;
padding: 3px 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--panel-soft);
color: var(--muted);
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.tag.ai {
border-color: #b9cfdb;
background: var(--blue-soft);
color: var(--accent);
}
.tag.ml {
border-color: #a6d4b7;
background: var(--green-soft);
color: var(--green);
}
.tag.guard {
border-color: #e5c08a;
background: var(--amber-soft);
color: var(--amber);
}
.tag.rule {
border-color: #c4b6a2;
background: #f6efe3;
color: #594b36;
}
.two-col {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 16px;
}
.three-col {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.metric-card strong {
display: block;
color: var(--accent);
font-size: 28px;
}
.mermaid-wrap {
padding: 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
overflow: auto;
}
.evidence-stack {
display: grid;
gap: 12px;
}
.evidence-row {
display: grid;
grid-template-columns: 180px minmax(0, 1fr) 170px;
gap: 12px;
align-items: start;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.evidence-row strong,
.evidence-row span {
min-width: 0;
}
.evidence-row span {
color: var(--muted);
}
.score-table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.score-table th,
.score-table td {
padding: 12px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
.score-table th {
background: #efe9dd;
color: #3f4b4d;
font-size: 13px;
}
.score-table td {
color: var(--muted);
font-size: 14px;
}
.score-table tr:last-child td {
border-bottom: 0;
}
.image-grid,
.language-box {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.do,
.dont {
padding: 16px;
border-radius: 8px;
}
.do {
border: 1px solid #9ec8b0;
background: var(--green-soft);
}
.dont {
border: 1px solid #e3aaa4;
background: var(--red-soft);
}
.plain-list {
margin: 12px 0 0;
padding-left: 20px;
}
.plain-list li + li {
margin-top: 8px;
}
.code-list {
display: grid;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.code-list li {
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
font-family: Consolas, "Cascadia Mono", monospace;
font-size: 13px;
}
footer {
padding: 34px 0 52px;
color: var(--muted);
font-size: 13px;
}
@media (max-width: 960px) {
.hero,
.answer-strip,
.section-head,
.two-col,
.image-grid,
.language-box {
grid-template-columns: 1fr;
}
.claim-grid,
.three-col {
grid-template-columns: 1fr 1fr;
}
.evidence-row {
grid-template-columns: 150px minmax(0, 1fr);
}
}
@media (max-width: 620px) {
.claim-grid,
.three-col,
.evidence-row {
grid-template-columns: 1fr;
}
section {
padding: 34px 0;
}
.score-table {
display: block;
overflow-x: auto;
}
}
</style>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
mermaid.initialize({
startOnLoad: true,
theme: "base",
themeVariables: {
primaryColor: "#edf7fb",
primaryTextColor: "#172124",
primaryBorderColor: "#9eb8c4",
lineColor: "#5f6b6f",
secondaryColor: "#ecf8f1",
tertiaryColor: "#fff7e2",
fontFamily: "Segoe UI, Malgun Gothic, sans-serif"
}
});
</script>
</head>
<body>
<main class="shell">
<section class="hero">
<div>
<p class="eyebrow">AI/ML usage explainer</p>
<h1>Copyrighter의 AI/ML은 판정 자동화가 아니라 근거 자동화입니다.</h1>
<p class="lead">
시스템은 제출 이미지를 분석해 유사 이미지, 웹 출처, 인물 존재 신호, 기준 DB 유사도, 검색 증거, LLM 요약을 만듭니다.
최종 저작권 위험 판정은 운영자가 하며, AI/ML 출력은 그 판단을 빠르게 만드는 검토 근거입니다.
</p>
</div>
<figure class="hero-card">
<img src="../web/operator-gui/pitch-assets/case-review.png" alt="Copyrighter 케이스 심사 화면">
<figcaption class="caption">케이스 심사 화면은 위험 점수, 상위 근거, 검색 증거, 요약, 운영자 판정을 한 흐름에 배치합니다.</figcaption>
</figure>
</section>
<div class="answer-strip">
<div class="verdict">
<strong>AI/ML 사용이라고 말할 수 있다.</strong>
<span>컴퓨터 비전 ML, 로컬 생성형 AI, 얼굴/인물 감지, 이미지 유사도 계산이 실제 처리 흐름에 포함되어 있습니다.</span>
</div>
<div class="warning">
<strong>정확한 표현</strong>
<span>“AI가 침해 여부를 확정한다”가 아니라 “AI/ML이 출처 기반 증거와 위험 triage를 생성하고, 운영자가 최종 판단한다”입니다.</span>
</div>
</div>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Definitions</p>
<h2>이 문서에서 AI, ML, 알고리즘은 서로 다른 역할을 합니다.</h2>
</div>
<p>혼동을 줄이려면 “어떤 기술이 무엇을 보고, 무엇을 산출하며, 그 산출물이 점수에 어떻게 반영되는지”를 분리해야 합니다.</p>
</div>
<div class="claim-grid">
<article class="claim-card">
<span class="tag ml">ML</span>
<strong>Google Cloud Vision Web Detection</strong>
<p>이미지에서 웹 엔티티, 동일 이미지, 부분 매칭, 유사 이미지, 출처 페이지 후보를 반환합니다. 외부 ML 서비스가 만든 탐지 결과입니다.</p>
</article>
<article class="claim-card">
<span class="tag ai">Generative AI</span>
<strong>Ollama 로컬 LLM 요약</strong>
<p>저장된 evidence만 입력으로 받아 운영자용 요약을 생성합니다. 새 사실을 만들거나 최종 판정을 내리지 않도록 제한합니다.</p>
</article>
<article class="claim-card">
<span class="tag ml">Classical CV</span>
<strong>얼굴/인물 존재 감지</strong>
<p>OpenCV Haar cascade로 얼굴 박스 존재를 탐지합니다. 동일인 식별, 얼굴 임베딩, 신원 추정은 수행하지 않습니다.</p>
</article>
<article class="claim-card">
<span class="tag rule">Algorithm</span>
<strong>SHA / pHash 이미지 지문</strong>
<p>학습 모델은 아니지만 이미지 내용의 지문을 만들고 해밍 거리로 유사도를 계산해 기준 DB 및 검색 결과 이미지와 비교합니다.</p>
</article>
</div>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Operating pipeline</p>
<h2>한 장의 제출 이미지는 evidence 묶음으로 변환됩니다.</h2>
</div>
<p>위험 점수는 LLM이 직접 만든 값이 아니라, 각 evidence의 유형과 신뢰도에 규칙을 적용해 계산한 triage 점수입니다.</p>
</div>
<div class="mermaid-wrap">
<pre class="mermaid">
flowchart LR
A[제출 이미지] --> B[로컬 전처리]
B --> C[SHA / pHash 지문 생성]
B --> D[얼굴·인물 존재 감지]
B --> E[Google Vision Web Detection]
E --> F[웹 엔티티·동일 이미지·부분 매칭·출처 페이지]
F --> G[Naver 텍스트 검색 보강]
F --> H[레거시 Google 맞춤 검색<br/>비활성 가능]
C --> I[기준 DB 및 검색 결과 이미지 유사도 비교]
D --> J[로컬 인물 존재 evidence]
F --> K[Google evidence]
G --> L[Naver evidence]
H --> L
I --> M[유사도 evidence]
J --> N[규칙 기반 위험 점수]
K --> N
L --> N
M --> N
K --> O[Ollama LLM 요약]
L --> O
M --> O
O --> P[출처 연결 요약 evidence]
N --> Q[운영자 검토]
P --> Q
Q --> R[승인 / 보류 / 반려]
classDef ai fill:#edf7fb,stroke:#24667a,color:#172124;
classDef cv fill:#ecf8f1,stroke:#28734f,color:#172124;
classDef rule fill:#fff7e2,stroke:#916300,color:#172124;
classDef human fill:#fff0ed,stroke:#a13d35,color:#172124;
class E,O ai;
class C,D,I cv;
class N rule;
class Q,R human;
</pre>
</div>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">What comes out</p>
<h2>각 기술은 서로 다른 결과물을 만들고, UI는 이를 한 줄의 판단 근거로 모읍니다.</h2>
</div>
<p>이 구분이 중요합니다. “신뢰도”는 하나의 전역 AI 확률값이 아니라 evidence별 confidence, 유사도, 매칭 유형, 규칙 점수의 조합입니다.</p>
</div>
<div class="evidence-stack">
<div class="evidence-row">
<strong>Google Vision</strong>
<span>웹 엔티티, full/partial/visual image match, matching page, weak label을 생성합니다.</span>
<span>confidence는 Google score 또는 fallback confidence에서 옵니다.</span>
</div>
<div class="evidence-row">
<strong>Naver 검색</strong>
<span>Google/기준 DB에서 나온 이름, 페이지 제목, 라벨을 텍스트 쿼리로 확장해 블로그/웹문서 근거를 모읍니다.</span>
<span>promoted 결과만 위험 점수에 직접 기여합니다.</span>
</div>
<div class="evidence-row">
<strong>pHash 유사도</strong>
<span>64비트 perceptual hash의 해밍 거리로 0.0~1.0 유사도를 계산합니다.</span>
<span>0.9 이상이면 강한 동일/유사 이미지 신호로 취급합니다.</span>
</div>
<div class="evidence-row">
<strong>얼굴/인물 감지</strong>
<span>얼굴 또는 인물이 있는지 presence-only 신호를 제공합니다.</span>
<span>신원 식별 confidence가 아니라 위험 검토 필요성 신호입니다.</span>
</div>
<div class="evidence-row">
<strong>Ollama LLM 요약</strong>
<span>기존 evidence의 출처, 이유, confidence, URL만 보고 내부 운영자용 요약을 생성합니다.</span>
<span>위험 점수에는 직접 가산되지 않습니다.</span>
</div>
</div>
</section>
<section>
<div class="two-col">
<div class="panel">
<p class="eyebrow">Score and confidence</p>
<h2>위험 점수는 AI의 “확률”이 아니라 규칙 기반 triage 점수입니다.</h2>
<p>
evidence에는 confidence가 붙지만, 최종 riskScore는 `RiskScorer`가 evidence 유형별 가중치를 더해 0~100으로 제한한 값입니다.
따라서 “100점 = 100% 침해 확률”이 아니라 “검토 우선순위가 매우 높음”입니다.
</p>
</div>
<div class="panel">
<p class="eyebrow">Band</p>
<h2>점수는 운영 큐 정렬을 위한 구간으로 변환됩니다.</h2>
<ul class="plain-list">
<li>70점 이상: 높음</li>
<li>30점 이상 70점 미만: 중간</li>
<li>30점 미만: 낮음</li>
<li>LLM 요약 evidence는 점수 가산에서 제외됩니다.</li>
</ul>
</div>
</div>
<table class="score-table" aria-label="위험 점수 계산 규칙 요약">
<thead>
<tr>
<th>근거 유형</th>
<th>점수 반영 방식</th>
<th>의미</th>
</tr>
</thead>
<tbody>
<tr>
<td>pHash 유사도</td>
<td>similarity 0.9 이상이면 +80, 그 외 의미 있는 지문 근거는 +30</td>
<td>기준 DB 또는 검색 결과 이미지와 시각적으로 매우 가깝다는 신호</td>
</tr>
<tr>
<td>얼굴/인물 존재</td>
<td>존재 신호가 있으면 +35</td>
<td>초상권/인물 이미지 검토가 필요할 수 있다는 신호</td>
</tr>
<tr>
<td>Google full match</td>
<td>동일 이미지 매칭은 +45</td>
<td>웹에 같은 이미지가 존재한다는 강한 출처 후보</td>
</tr>
<tr>
<td>Google partial/page match</td>
<td>부분 이미지 또는 페이지 매칭은 +35</td>
<td>일부 요소 또는 출처 페이지가 제출 이미지와 관련될 가능성</td>
</tr>
<tr>
<td>Google visual match</td>
<td>시각적 유사 이미지는 +10</td>
<td>약한 참고 신호이며 단독으로 강한 판정 근거가 되지 않음</td>
</tr>
<tr>
<td>Naver promoted 검색 결과</td>
<td>round(50 * confidence)</td>
<td>검색 결과가 기준 후보로 승격될 만큼 관련성이 있다고 본 경우</td>
</tr>
<tr>
<td>Ollama LLM 요약</td>
<td>0점</td>
<td>판정 점수가 아니라 사람이 읽기 쉬운 출처 연결 설명</td>
</tr>
</tbody>
</table>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Trust boundaries</p>
<h2>신뢰도는 단계별로 다르게 해석해야 합니다.</h2>
</div>
<p>설명 자료에서는 “AI 신뢰도” 하나로 뭉뚱그리지 말고 아래처럼 말하는 편이 정확합니다.</p>
</div>
<div class="three-col">
<article class="metric-card">
<strong>Evidence confidence</strong>
<p>Google score, fallback confidence, 검색 승격 confidence처럼 개별 근거에 붙는 값입니다.</p>
</article>
<article class="metric-card">
<strong>Similarity</strong>
<p>pHash 거리에서 나온 이미지 유사도입니다. ML 확률이 아니라 지문 거리 기반 수치입니다.</p>
</article>
<article class="metric-card">
<strong>Risk score</strong>
<p>여러 근거를 규칙으로 합산한 운영 우선순위 점수입니다. 법적 침해 확률이 아닙니다.</p>
</article>
</div>
</section>
<section>
<div class="two-col">
<div class="panel">
<p class="eyebrow">LLM guardrail</p>
<h2>LLM은 근거를 요약할 뿐, 새 결론을 만들지 못하게 설계되어 있습니다.</h2>
<p>
프롬프트는 “제공된 source evidence만 요약하라”, “최종 결정을 내리지 말라”, “근거 없는 주장을 추가하지 말라”로 제한됩니다.
요약 evidence에는 source URL 또는 source evidence id가 연결됩니다.
</p>
</div>
<div class="mermaid-wrap">
<pre class="mermaid">
sequenceDiagram
participant DB as Evidence DB
participant LLM as Ollama 로컬 LLM
participant UI as 운영 콘솔
participant Human as 운영자
DB->>LLM: fingerprint, face, google, naver evidence 전달
LLM->>DB: 출처 연결 요약 evidence 저장
DB->>UI: 원문 evidence + 요약 evidence 표시
Human->>UI: 증거 사용/미사용 선택
Human->>UI: 승인/보류/반려 최종 판정
</pre>
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Visual proof</p>
<h2>운영 화면은 AI/ML 결과가 어떻게 사람이 검토할 수 있는 근거로 바뀌는지 보여줍니다.</h2>
</div>
<p>시연에서는 “AI가 판정했다”가 아니라 “AI/ML이 근거를 정리했고 운영자가 판정한다”는 화면 흐름을 보여주는 것이 좋습니다.</p>
</div>
<div class="image-grid">
<figure class="image-card">
<img src="../web/operator-gui/pitch-assets/evidence-search.png" alt="검색 증거 화면">
<figcaption class="caption">검색 증거: 쿼리, 출처 URL, 이미지, 매칭 유형이 함께 남아 추적 가능한 검토 근거가 됩니다.</figcaption>
</figure>
<figure class="image-card">
<img src="../web/operator-gui/pitch-assets/provider-controls.png" alt="외부 검색 tool 활용 화면">
<figcaption class="caption">외부 검색 tool 활용: Google, Naver, Ollama의 활성 상태, 실패 상태, 사용량을 운영자가 확인합니다.</figcaption>
</figure>
</div>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Recommended wording</p>
<h2>강조 문구는 기술의 힘과 판정 책임의 경계를 함께 담아야 합니다.</h2>
</div>
</div>
<div class="language-box">
<div class="do">
<h3>추천 표현</h3>
<p>Copyrighter는 컴퓨터 비전 ML, 이미지 지문 유사도, 검색 증거 수집, 로컬 LLM 요약을 결합해 이미지 저작권 위험 검토 근거를 자동 생성합니다.</p>
<p>위험 점수는 AI가 내린 법적 결론이 아니라 evidence confidence, 매칭 유형, 이미지 유사도에 기반한 운영 triage 점수입니다.</p>
<p>최종 승인, 보류, 반려는 운영자가 수행하며 모든 근거는 출처와 함께 보존됩니다.</p>
</div>
<div class="dont">
<h3>피해야 할 표현</h3>
<p>AI가 저작권 침해 여부를 자동 판정합니다.</p>
<p>LLM이 원작자, 유명인, 침해 여부를 단독으로 확정합니다.</p>
<p>위험 점수 100은 침해 확률 100%를 의미합니다.</p>
<p>Naver에 이미지를 업로드해 역검색합니다.</p>
</div>
</div>
</section>
<section>
<div class="section-head">
<div>
<p class="eyebrow">Code evidence</p>
<h2>설명서의 근거가 되는 구현 지점입니다.</h2>
</div>
</div>
<ul class="code-list">
<li>src/rights_filter/integrations/cloud_vision_web_detection.py - Google Cloud Vision Web Detection 호출 및 evidence 매핑</li>
<li>src/rights_filter/analysis/llm_assistance.py - Ollama Generate API 기반 source-linked LLM 요약</li>
<li>src/rights_filter/analysis/fingerprints.py - SHA 및 pHash 이미지 지문, 해밍 거리 기반 유사도</li>
<li>src/rights_filter/analysis/face_person_detection.py - OpenCV Haar cascade 기반 얼굴/인물 존재 감지</li>
<li>src/rights_filter/analysis/risk_scoring.py - evidence 유형별 규칙 기반 위험 점수 산정</li>
<li>src/rights_filter/server/sqlite_store.py - evidence 저장, 외부 검색 tool 활용 상태, LLM 요약 자동 생성</li>
<li>docs/operations/image-rights-risk-filter.md - 외부 API, LLM, 데이터 경계 운영 정책</li>
</ul>
</section>
</main>
<footer class="shell">
Copyrighter AI/ML usage explainer. AI/ML 출력은 내부 검수 근거이며 최종 판정은 운영자가 수행합니다.
</footer>
</body>
</html>