docs: 적재본 545건의 출처 내역 추가 (컴북스 31 / 위원회 514)

문제: 리스트업이 선별 57건만 다뤄서, "표절 탐지에 실제로 쓰이는 판례가 무엇인가"에
답할 수 없었다. 엔진이 로드하는 것은 57건이 아니라 적재본 545건 전부이고
(PRECEDENTS_PATH), 판정 1건마다 유형·태그가 맞는 상위 5건을 인용한다.

또 컴북스 제공분이 얼마나 반영됐는지가 어디에도 기재돼 있지 않았다. 데이터에는
레코드의 excel_duplicate 필드로 남아 있는데 문서에 나오지 않아, 회의에서
"우리가 준 판례는 어떻게 됐나"라는 질문에 답할 수 없는 상태였다.

  컴북스 제공 고유 사건번호        142
    공식 상세 페이지 미확인 → 제외 -103
    엔진 라벨 부족 → 제외            -8
  엔진 적재본에 포함                31  (21.8%)
    이 중 리스트업 선정             10  (A 3 · B 6 · C 1)

제외된 103건은 사건번호만으로는 인용 출처가 되지 않아 적재하지 못했다. 판결문
원문이나 확인 경로를 받으면 적재 가능하므로 컴북스에 회신을 요청해야 하고, 그
목록을 Excel 시트에 그대로 실었다.

Excel 에 시트 2개 추가:
  적재본 전체    545건 전수. 출처(컴북스/위원회)와 리스트업 등급을 함께 표시
  컴북스 제공분  단계별 반영 내역, 적재된 31건, 회신 요청할 103건 목록

주의로 남긴 것: 컴북스 목록의 주제 분류와 실제 판시가 어긋나는 건이 리스트업에도
흘러들어갔다. 컴북스 유래 A등급 3건 중 2012다73493(깃털 공예품)은 어문 사건이
아니며 2017다212095(게임)도 마찬가지다. 인용 전 원문 대조가 필요하다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-08-20 12:57:34 +09:00
parent ba1a0bbc2f
commit a45c79211c
3 changed files with 240 additions and 1 deletions

View File

@ -56,6 +56,43 @@
---
## 2-1. 적재본 545건의 출처 — 표절 탐지에 실제로 쓰이는 판례
**엔진이 런타임에 로드하는 것은 이 리스트업 57건이 아니라 적재본 545건 전부다**
(`PRECEDENTS_PATH=./data/precedents/precedents.jsonl`). 판정 1건마다 저작물 유형과
법적 태그가 맞는 **상위 5건**을 골라 근거로 인용한다(`app/engine/legal_risk.py`).
리스트업 57건은 사람이 읽는 선별 문서이며 엔진 동작에 관여하지 않는다.
| 출처 | 건수 | 비고 |
|---|---:|---|
| 한국저작권위원회 공식 목록 수집분 | 514 | 공식 판례 목록 2,085건에서 제목·라벨 기준으로 선별 |
| **컴북스 제공 엑셀 유래** | **31** | 레코드의 `excel_duplicate` 필드가 `true` |
| **합계 (엔진 적재본)** | **545** | |
### 컴북스 제공분의 반영 내역
| 단계 | 건수 | 설명 |
|---|---:|---|
| 제공 엑셀 고유 사건번호 | 142 | 중복 제거 기준 |
| 공식 상세 페이지 미확인 → 제외 | 103 | 사건번호는 있으나 공식 상세 페이지를 찾지 못한 건. 출처를 임의 생성하지 않기 위해 제외했다 |
| 엔진 라벨 부족 → 제외 | 8 | 저작물 유형·판단 기준·법적 태그가 모두 없는 건 |
| **엔진 적재본에 포함** | **31** | 런타임에 실제로 로드된다 |
| 이 중 본 리스트업에 선정 | 10 | A 3 · B 6 · C 1 |
즉 컴북스가 제공한 142건 중 실제로 엔진에 들어간 것은 **31건(21.8%)** 이다.
**되돌려야 할 항목** — 제외된 103건은 사건번호만으로는 엔진이 인용할 출처가 되지
않아 적재하지 못했다. **판결문 원문이나 확인 경로를 받으면 그대로 적재할 수 있다.**
전체 사건번호 목록은 `selection_audit.json`
`excel_without_official_detail_case_ids` 와 배포용 Excel 의 「컴북스 제공분」 시트에 있다.
> ⚠️ 컴북스 제공 목록의 주제 분류와 실제 판시가 어긋나는 건이 리스트업에도 흘러들어갔다.
> 컴북스 유래 A등급 3건 중 `2012다73493`(깃털 공예품)은 어문 사건이 아니며,
> `2017다212095`(게임)도 마찬가지다. 6-3의 2번 항목과 같은 문제이므로 인용 전
> 원문 대조가 필요하다.
---
## 3. A등급 — 직접 적용 (24건)
| 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 |

Binary file not shown.

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import argparse
import csv
import io
import json
import sys
from collections import Counter
from pathlib import Path
@ -249,6 +250,183 @@ def sheet_list(wb: Workbook, rows: list[dict]) -> None:
ws.sheet_view.zoomScale = 90
# ---------------------------------------------------------------------------
# 시트 — 적재본 전체 (엔진이 실제로 쓰는 판례)
# ---------------------------------------------------------------------------
CORPUS_COLUMNS = [
("_source", "출처", 12, False),
("_grade", "등급", 6, False),
("case_id", "사건번호", 15, False),
("title", "사건명", 44, True),
("_work", "저작물 유형", 14, True),
("_tags", "법적 태그", 24, True),
("_criteria", "판단 기준", 26, True),
("holding_summary", "판시 요약", 74, True),
("source_registered_date", "등록일", 11, False),
("source_url", "출처", 12, False),
]
def sheet_corpus(wb: Workbook, corpus: list[dict], rows: list[dict]) -> None:
"""545건 전수. 어느 것이 컴북스 제공분이고 어느 것이 리스트업에 뽑혔는지 표시한다."""
ws = wb.create_sheet("적재본 전체")
grade_of = {r["case_id"]: r["grade"] for r in rows}
n_excel = sum(1 for r in corpus if r.get("excel_duplicate"))
head = _title_block(
ws,
f"엔진 적재본 전체 {len(corpus)}건 — 표절 탐지에 실제로 사용되는 판례",
f"런타임에 이 {len(corpus)}건을 모두 로드하고, 판정 1건마다 저작물 유형·법적 태그가 "
f"맞는 상위 5건을 근거로 인용한다(app/engine/legal_risk.py). "
f"출처는 컴북스 제공 {n_excel}건, 한국저작권위원회 공식 목록 {len(corpus) - n_excel}건. "
"'등급'이 채워진 행이 리스트업 57건이다.",
width_cols=len(CORPUS_COLUMNS),
)
_header_row(ws, head, [label for _, label, _, _ in CORPUS_COLUMNS])
_widths(ws, [w for _, _, w, _ in CORPUS_COLUMNS])
order = {"A": 0, "B": 1, "C": 2}
corpus = sorted(
corpus,
key=lambda r: (
0 if r.get("excel_duplicate") else 1,
order.get(grade_of.get(r["case_id"], ""), 9),
r["case_id"],
),
)
for i, rec in enumerate(corpus):
r = head + 1 + i
grade = grade_of.get(rec["case_id"], "")
derived = {
"_source": "컴북스" if rec.get("excel_duplicate") else "위원회",
"_grade": grade,
"_work": " / ".join(rec.get("work_types") or []),
"_tags": " / ".join(rec.get("legal_tags") or []),
"_criteria": " / ".join(rec.get("criteria") or []),
}
for c, (field, _, _, wrap) in enumerate(CORPUS_COLUMNS, start=1):
cell = ws.cell(row=r, column=c)
if field == "source_url":
url = rec.get("source_url") or ""
if url:
cell.value = "원문 링크"
cell.hyperlink = url
cell.font = LINK_FONT
cell.alignment = TOP_LEFT
cell.border = CELL_BORDER
continue
cell.value = derived.get(field, rec.get(field, "") or "")
_body(cell, wrap=wrap)
if field == "_grade":
cell.alignment = CENTER
if grade in GRADE_FILL:
cell.fill = GRADE_FILL[grade]
cell.font = Font(name="맑은 고딕", size=10, bold=True, color=INK)
elif field == "_source":
cell.alignment = CENTER
if rec.get("excel_duplicate"):
cell.font = Font(name="맑은 고딕", size=10, bold=True, color="B03A22")
last = head + len(corpus)
ws.auto_filter.ref = f"A{head}:{get_column_letter(len(CORPUS_COLUMNS))}{last}"
ws.freeze_panes = ws.cell(row=head + 1, column=4)
ws.sheet_view.zoomScale = 85
# ---------------------------------------------------------------------------
# 시트 — 컴북스 제공분 내역
# ---------------------------------------------------------------------------
def sheet_combooks(wb: Workbook, corpus: list[dict], rows: list[dict], audit: dict) -> None:
ws = wb.create_sheet("컴북스 제공분")
_widths(ws, [40, 10, 74])
grade_of = {r["case_id"]: r["grade"] for r in rows}
kept = [r for r in corpus if r.get("excel_duplicate")]
picked = [r for r in kept if r["case_id"] in grade_of]
missing = audit.get("excel_without_official_detail_case_ids", [])
total = audit.get("excel_unique_case_count", 0)
r = _title_block(
ws, "컴북스 제공 판례의 반영 내역",
"회의에서 '우리가 준 판례는 어떻게 됐는가'에 답하기 위한 표다. "
"제공받은 사건번호가 모두 적재된 것은 아니며, 빠진 건에는 사유가 있다.",
width_cols=3,
)
ws.cell(row=r, column=1, value="■ 단계별 반영").font = SECTION_FONT
r += 1
_header_row(ws, r, ["단계", "건수", "설명"])
r += 1
funnel = [
("제공 엑셀 고유 사건번호", total, "중복 제거 기준"),
("공식 상세 페이지 미확인 → 제외", len(missing),
"한국저작권위원회 공식 사이트에서 상세 페이지를 찾지 못한 건. 출처를 임의 생성하지 않기 위해 제외했다. 아래 목록 참조."),
("엔진 라벨 부족 → 제외", total - len(missing) - len(kept),
"저작물 유형·판단 기준·법적 태그가 모두 없어 무차별 매칭을 유발하는 건"),
("엔진 적재본에 포함", len(kept), "런타임에 실제로 로드된다"),
("리스트업에 선정", len(picked),
f"A {sum(1 for x in picked if grade_of[x['case_id']] == 'A')} · "
f"B {sum(1 for x in picked if grade_of[x['case_id']] == 'B')} · "
f"C {sum(1 for x in picked if grade_of[x['case_id']] == 'C')}"),
]
for name, n, note in funnel:
ws.cell(row=r, column=1, value=name)
ws.cell(row=r, column=2, value=n)
ws.cell(row=r, column=3, value=note)
for c in (1, 2, 3):
_body(ws.cell(row=r, column=c))
ws.cell(row=r, column=2).alignment = CENTER
if name.startswith("엔진 적재본"):
for c in (1, 2, 3):
ws.cell(row=r, column=c).font = Font(name="맑은 고딕", size=10, bold=True, color=INK)
r += 1
r += 1
ws.cell(row=r, column=1, value=f"■ 적재된 {len(kept)}").font = SECTION_FONT
r += 1
_header_row(ws, r, ["사건번호", "등급", "사건명"])
r += 1
for rec in sorted(kept, key=lambda x: (grade_of.get(x["case_id"], "Z"), x["case_id"])):
g = grade_of.get(rec["case_id"], "")
ws.cell(row=r, column=1, value=rec["case_id"])
ws.cell(row=r, column=2, value=g or "")
ws.cell(row=r, column=3, value=rec.get("title", ""))
for c in (1, 2, 3):
_body(ws.cell(row=r, column=c))
ws.cell(row=r, column=2).alignment = CENTER
if g in GRADE_FILL:
ws.cell(row=r, column=2).fill = GRADE_FILL[g]
r += 1
r += 1
ask = ws.cell(
row=r, column=1,
value=f"■ 출처 확보가 필요한 {len(missing)}건 — 컴북스 회신 요청 항목",
)
ask.font = Font(name="맑은 고딕", size=11, bold=True, color="B03A22")
r += 1
note = ws.cell(
row=r, column=1,
value="사건번호는 받았으나 한국저작권위원회 공식 상세 페이지를 찾지 못해 적재하지 못한 건이다. "
"판결문 원문이나 확인 경로를 주시면 그대로 적재할 수 있다. "
"사건번호만으로는 엔진이 인용할 출처가 되지 않아 임의로 만들지 않았다.",
)
note.font = NOTE_FONT
note.alignment = TOP_WRAP
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=3)
ws.row_dimensions[r].height = 32
r += 2
per_row = 3
for i in range(0, len(missing), per_row):
for j, cid in enumerate(missing[i:i + per_row]):
cell = ws.cell(row=r, column=1 + j, value=cid)
_body(cell, wrap=False)
r += 1
# ---------------------------------------------------------------------------
# 시트 2 — 요약·방법
# ---------------------------------------------------------------------------
@ -427,6 +605,19 @@ def sheet_questions(wb: Workbook) -> None:
# ---------------------------------------------------------------------------
def load_corpus(path: Path) -> list[dict]:
"""엔진이 실제로 로드하는 적재본. 리스트업 57건이 아니라 이쪽이 런타임 후보 풀이다."""
if not path.exists():
raise SystemExit(f"{path} 가 없습니다.")
return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
def load_audit(path: Path) -> dict:
if not path.exists():
raise SystemExit(f"{path} 가 없습니다.")
return json.loads(path.read_text(encoding="utf-8"))
def load_rows(path: Path) -> list[dict]:
if not path.exists():
raise SystemExit(
@ -445,14 +636,23 @@ def main() -> int:
)
ap.add_argument("--csv", type=Path,
default=ROOT / "data/precedents/precedent_listup.csv")
ap.add_argument("--corpus", type=Path,
default=ROOT / "data/precedents/precedents.jsonl",
help="엔진이 실제로 로드하는 적재본")
ap.add_argument("--audit", type=Path,
default=ROOT / "data/precedents/selection_audit.json")
ap.add_argument("--out", type=Path, default=ROOT / "docs/판례_리스트업.xlsx")
args = ap.parse_args()
rows = load_rows(args.csv)
corpus = load_corpus(args.corpus)
audit = load_audit(args.audit)
wb = Workbook()
wb.remove(wb.active) # 기본 시트 제거
sheet_list(wb, rows)
sheet_corpus(wb, corpus, rows)
sheet_combooks(wb, corpus, rows, audit)
sheet_summary(wb, rows)
sheet_excluded(wb)
sheet_gaps(wb)
@ -462,8 +662,10 @@ def main() -> int:
wb.save(args.out)
counts = Counter(r["grade"] for r in rows)
n_excel = sum(1 for r in corpus if r.get("excel_duplicate"))
print(f"생성: {args.out}")
print(f" 판례 {len(rows)}건 (A {counts['A']} / B {counts['B']} / C {counts['C']})")
print(f" 적재본 {len(corpus)}건 — 컴북스 {n_excel} / 위원회 {len(corpus) - n_excel}")
print(f" 리스트업 {len(rows)}건 (A {counts['A']} / B {counts['B']} / C {counts['C']})")
print(f" 시트 {len(wb.sheetnames)}개: {', '.join(wb.sheetnames)}")
return 0