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건) ## 3. A등급 — 직접 적용 (24건)
| 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 | | 사건번호 | 법원 | 선고일 | 주제 | 쟁점 요약 | 적용 판단 근거·한계 |

Binary file not shown.

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import argparse import argparse
import csv import csv
import io import io
import json
import sys import sys
from collections import Counter from collections import Counter
from pathlib import Path from pathlib import Path
@ -249,6 +250,183 @@ def sheet_list(wb: Workbook, rows: list[dict]) -> None:
ws.sheet_view.zoomScale = 90 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 — 요약·방법 # 시트 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]: def load_rows(path: Path) -> list[dict]:
if not path.exists(): if not path.exists():
raise SystemExit( raise SystemExit(
@ -445,14 +636,23 @@ def main() -> int:
) )
ap.add_argument("--csv", type=Path, ap.add_argument("--csv", type=Path,
default=ROOT / "data/precedents/precedent_listup.csv") 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") ap.add_argument("--out", type=Path, default=ROOT / "docs/판례_리스트업.xlsx")
args = ap.parse_args() args = ap.parse_args()
rows = load_rows(args.csv) rows = load_rows(args.csv)
corpus = load_corpus(args.corpus)
audit = load_audit(args.audit)
wb = Workbook() wb = Workbook()
wb.remove(wb.active) # 기본 시트 제거 wb.remove(wb.active) # 기본 시트 제거
sheet_list(wb, rows) sheet_list(wb, rows)
sheet_corpus(wb, corpus, rows)
sheet_combooks(wb, corpus, rows, audit)
sheet_summary(wb, rows) sheet_summary(wb, rows)
sheet_excluded(wb) sheet_excluded(wb)
sheet_gaps(wb) sheet_gaps(wb)
@ -462,8 +662,10 @@ def main() -> int:
wb.save(args.out) wb.save(args.out)
counts = Counter(r["grade"] for r in rows) 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"생성: {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)}") print(f" 시트 {len(wb.sheetnames)}개: {', '.join(wb.sheetnames)}")
return 0 return 0