docs: 판례 리스트업 배포용 Excel 추가
CSV 는 UTF-8 BOM 이라 Excel 에서 열리기는 하지만 받는 쪽이 그대로 읽을 수 있는 형태가 아니다. 열 너비·줄바꿈·필터가 없어 판시 요약처럼 긴 칸이 한 줄로 뭉개지고 등급별로 훑어보기도 어렵다. 전달 첨부용 통합문서를 별도로 만든다. docs/판례_리스트업.xlsx — 5개 시트 판례 목록 57건. 등급 색상 구분, 자동 필터, 사건번호까지 틀 고정, 출처 하이퍼링크 요약·방법 검토 단계별 건수(2,085 → 545 → 57), 등급 정의, 핵심 3건 제외 사유 수집 단계 제외분과 적재본에서 뺀 유형 데이터 공백 상담 전 해소가 필요한 4개 항목 + 선덕여왕 항소심 인용 주의 상담 질의 전문가 상담용 질의 5건 표 데이터는 precedent_listup.csv 에서 읽고, 서술 시트는 PRECEDENT_LISTUP.md 를 원본으로 옮겼다. 수치를 스크립트에서 새로 만들지 않는다. 법률 의견서가 아니라 데이터 정리 결과이며 등급·쟁점 요약은 적재 판시를 근거로 부여한 것이라는 단서를 요약 시트에 함께 넣었다. 전달 시 유지해야 한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
66a21f8fd2
commit
ba1a0bbc2f
@ -191,7 +191,10 @@
|
||||
|
||||
## 8. 첨부·원본 위치
|
||||
|
||||
- 본 리스트업 표(CSV, Excel에서 바로 열림): `data/precedents/precedent_listup.csv`
|
||||
- **배포용 Excel(전달 첨부): `docs/판례_리스트업.xlsx`** — 5개 시트
|
||||
(판례 목록 / 요약·방법 / 제외 사유 / 데이터 공백 / 상담 질의).
|
||||
`scripts/build_precedent_listup_xlsx.py` 로 재생성한다.
|
||||
- 원본 표(CSV): `data/precedents/precedent_listup.csv`
|
||||
- 운영 적재본 545건: `data/precedents/precedents.jsonl`
|
||||
- 수집·제외 통계: `data/precedents/selection_audit.json`
|
||||
- 데이터 계층 전체 인벤토리: `docs/PRECEDENT_DATA.md`
|
||||
|
||||
BIN
docs/판례_리스트업.xlsx
Normal file
BIN
docs/판례_리스트업.xlsx
Normal file
Binary file not shown.
472
scripts/build_precedent_listup_xlsx.py
Normal file
472
scripts/build_precedent_listup_xlsx.py
Normal file
@ -0,0 +1,472 @@
|
||||
#!/usr/bin/env python3
|
||||
"""판례 리스트업 → 배포용 Excel(.xlsx) 생성.
|
||||
|
||||
왜 별도로 만드는가:
|
||||
`precedent_listup.csv` 는 UTF-8 BOM 이라 Excel 에서 열리기는 하지만, 받는 쪽이
|
||||
바로 읽을 수 있는 형태는 아니다. 열 너비·줄바꿈·필터가 없어 판시 요약처럼 긴
|
||||
칸이 한 줄로 뭉개지고, 등급별로 훑어보기도 어렵다. 이 스크립트는 같은 데이터를
|
||||
검토자가 그대로 열어 쓸 수 있는 통합문서로 만든다.
|
||||
|
||||
원본(source of truth):
|
||||
- 표 데이터: `data/precedents/precedent_listup.csv`
|
||||
- 서술 내용: `docs/PRECEDENT_LISTUP.md` — 아래 상수는 그 문서에서 옮긴 것이며,
|
||||
문서가 바뀌면 여기도 함께 고쳐야 한다. 수치를 이 파일에서 새로 만들지 말 것.
|
||||
|
||||
사용:
|
||||
python scripts/build_precedent_listup_xlsx.py
|
||||
python scripts/build_precedent_listup_xlsx.py --out docs/판례_리스트업.xlsx
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 서식 — 화면과 인쇄 모두에서 읽히도록 최소한으로만 쓴다
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
INK = "1C1A19"
|
||||
HEAD_FILL = PatternFill("solid", fgColor="2F3B45")
|
||||
HEAD_FONT = Font(name="맑은 고딕", size=10, bold=True, color="FFFFFF")
|
||||
BODY_FONT = Font(name="맑은 고딕", size=10, color=INK)
|
||||
TITLE_FONT = Font(name="맑은 고딕", size=13, bold=True, color="B03A22")
|
||||
SECTION_FONT = Font(name="맑은 고딕", size=11, bold=True, color=INK)
|
||||
NOTE_FONT = Font(name="맑은 고딕", size=9, color="6E6660")
|
||||
LINK_FONT = Font(name="맑은 고딕", size=9, color="1B5E8A", underline="single")
|
||||
|
||||
GRADE_FILL = {
|
||||
"A": PatternFill("solid", fgColor="E4F0E8"),
|
||||
"B": PatternFill("solid", fgColor="FBF1DE"),
|
||||
"C": PatternFill("solid", fgColor="EFEDEB"),
|
||||
}
|
||||
|
||||
HAIRLINE = Side(style="thin", color="D8D3CE")
|
||||
CELL_BORDER = Border(left=HAIRLINE, right=HAIRLINE, top=HAIRLINE, bottom=HAIRLINE)
|
||||
|
||||
TOP_WRAP = Alignment(vertical="top", wrap_text=True)
|
||||
TOP_LEFT = Alignment(vertical="top", horizontal="left")
|
||||
CENTER = Alignment(vertical="center", horizontal="center")
|
||||
|
||||
# 표 열 정의: (CSV 필드, 표시 이름, 너비, 줄바꿈 여부)
|
||||
COLUMNS = [
|
||||
("grade", "등급", 6, False),
|
||||
("case_id", "사건번호", 15, False),
|
||||
("court", "법원", 20, True),
|
||||
("decided", "선고일", 12, False),
|
||||
("topic", "주제", 16, True),
|
||||
("title", "사건명", 34, True),
|
||||
("issue", "쟁점 요약", 52, True),
|
||||
("why", "본 과제 적용 지점", 46, True),
|
||||
("work_types", "저작물 유형", 14, True),
|
||||
("criteria", "판단 기준", 26, True),
|
||||
("legal_tags", "법적 태그", 22, True),
|
||||
("url", "출처", 12, False),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 서술 시트 — 원본은 docs/PRECEDENT_LISTUP.md
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
FUNNEL = [
|
||||
("한국저작권위원회 공식 판례 목록", 2085, "209페이지 전수"),
|
||||
("제목 기준 후보", 702, "자서전·출판물 본문/구조/인용 및 부속 자산 관련"),
|
||||
("사건번호 확인 후 병합", 679, "프로젝트 제공 엑셀 사건번호 142개 역검색 포함"),
|
||||
("엔진 라벨 부족으로 제외", 134, "저작물 유형·판단 기준·법적 태그가 모두 없는 건"),
|
||||
("운영 적재본", 545, "data/precedents/precedents.jsonl"),
|
||||
("어문저작물 + 판단 기준 라벨 보유", 77, "1차 선별 모집단"),
|
||||
("최종 선정", 57, "판시 본문을 개별 확인해 등급 부여"),
|
||||
]
|
||||
|
||||
GRADE_DEF = [
|
||||
("A", "직접 적용",
|
||||
"어문저작물의 표절·유사성·인용 판단 기준을 직접 제시한다. 판정 로직과 결과 리포트의 근거로 바로 인용할 수 있다."),
|
||||
("B", "조건부 적용",
|
||||
"자서전 출판 실무의 주변 쟁점(성명 표시, 대필·공저, 동일성유지, 출판계약, 번역·개정판)을 다룬다. 표절 탐지 자체의 기준은 아니지만 서비스가 다뤄야 할 위험 영역이다."),
|
||||
("C", "참고",
|
||||
"법리는 유용하나 사안이 어문·출판이 아니거나(음악·미술·게임·영상), 가처분·구법 판단이어서 그대로 인용하면 무리가 있다."),
|
||||
]
|
||||
|
||||
KEY_THREE = [
|
||||
("2013도8793", "대법원",
|
||||
"저서 각 장의 11~40%를 인용하고 각주·참고문헌을 달았어도 정당한 인용이 아니라고 본 사례. 출처를 표시했다는 이유만으로 면책되지 않는다는 정량 기준을 제시한다."),
|
||||
("2014다14375", "대법원 (석굴암)",
|
||||
"역사적 사실과 그에 대한 새로운 해석은 아이디어 영역이라는 판시. 실재 인물·사건을 공유하는 자서전 간 오탐을 걸러내는 근거다."),
|
||||
("2018도144", "대법원 (표지갈이)",
|
||||
"저작자 아닌 자를 저작자로 표시해 공표하면 당사자 전원이 동의했더라도 형사처벌 대상이다. 대필 자서전 관행에 직접 걸린다."),
|
||||
]
|
||||
|
||||
EXCLUDED_COLLECTION = [
|
||||
("공식 상세 페이지 미확인", 103,
|
||||
"프로젝트 제공 엑셀의 사건번호 142개 중 한국저작권위원회 공식 사이트에서 상세 페이지를 찾지 못한 건. 출처를 임의 생성하지 않기 위해 적재에서 제외했다. 전체 목록은 selection_audit.json."),
|
||||
("엔진 라벨 부족", 134, "저작물 유형·판단 기준·법적 태그가 모두 없어 무차별 매칭을 유발하는 건"),
|
||||
("제목 후보 탈락", 1380, "공식 목록 2,085건 중 자서전·출판물 범위와 무관한 제목 (약 1,380건)"),
|
||||
]
|
||||
|
||||
EXCLUDED_TYPES = [
|
||||
("노래방·음원 이용 사건", "2001도4100, 2011다101148, 2012다109798", "공연권·복제권 사안으로 어문 표절 기준을 주지 않는다"),
|
||||
("사진·이미지 무단 사용", "2019고정1107, 2020고정1154 등 다수", "동일 이미지 복제 여부가 쟁점이라 유사도 판단 기준이 없다"),
|
||||
("캐릭터·위조상품 밀수", "2019고단9348, 2019고정667", "상품 유통 사안"),
|
||||
("웹툰·영상 불법 업로드", "2019가합570806, 2019고단2659", "전송권 침해이며 본문 대비 판단이 없다"),
|
||||
("폰트·프로그램", "95가합11403, 98나23616, 2020나31370", "저작물 유형이 다르다"),
|
||||
("조세·행정 사건", "2011구합31796, 2016구합54602", "저작권 수입의 과세 문제이며 침해 판단이 아니다"),
|
||||
("저작물 표시만 있고 판시 본문 없음", "다수", "제목·선고일만으로 법리를 추론하지 않는다는 원칙에 따름"),
|
||||
]
|
||||
|
||||
GAPS = [
|
||||
("v1.3 대표판례 29건 중 17건이 운영 적재본에 없음",
|
||||
"2006나16757, 2007가합43936, 2010도4468, 2019가단31377, 96다273, 2023카합21631, "
|
||||
"2021가합588060, 2007가합16095, 2019가단5207564, 2013나2004096, 2012도13718, 2007다354, "
|
||||
"2017도19025, 2008카합968, 2008다53812, 2016고정432, 2006가합8583",
|
||||
"대부분 '공식 상세 페이지 미확인 103건'에 걸린 사건. 분류체계의 근거로는 쓰이지만 엔진이 인용할 출처가 확보되지 않았다."),
|
||||
("v1.3 주제 분류와 실제 판시가 어긋나는 건",
|
||||
"2011다101148, 2012다109798 (노래방 음악저작물) · 2012다73493 (깃털 공예품) · 2017다212095 (게임)",
|
||||
"v1.3에서는 '동일성유지·출판계약' 등으로 묶여 있으나 적재본 판시는 어문 사건이 아니다. 인용 전 원문 대조가 필요하다."),
|
||||
("선고일 미확인 18건",
|
||||
"2010가합1884, 2014다14375, 2013도8793, 2024노803, 2025가단81306, 2021가합558106, "
|
||||
"2024도19189, 2024나13167, 2022가합516598, 2019가합106853, 2024나2061521, 2024가합96852, "
|
||||
"2024나306378, 2024누94, 2025구합31, 2011도5835, 2012다41410, 77다90",
|
||||
"적재본에 선고일 필드가 없어 판시 본문에서 추출했고, 본문이 요지 한 줄뿐인 건은 채우지 못했다."),
|
||||
("자서전·회고록·대필 직접 판례 0건",
|
||||
"545건 전체에서 자서전·회고록·대필·평전·구술·유고 키워드 모두 0회",
|
||||
"인접 유형(교재·수험서·논문·소설·드라마) 법리를 유추 적용해야 하며, 그 유추의 타당성이 상담의 핵심 질의가 되어야 한다."),
|
||||
]
|
||||
|
||||
QUESTIONS = [
|
||||
"자서전처럼 실재 사건·인물·연보를 공유하는 저작물 사이에서, 석굴암·선덕여왕 판례의 "
|
||||
"'역사적 사실과 그 해석은 아이디어' 법리는 어디까지 확장되는가.",
|
||||
"2013도8793의 11~40% 인용 비율을 서비스의 경고 임계값으로 참조하는 것이 타당한가. "
|
||||
"비율 외에 '인용 부분을 구별할 수 있는가'라는 질적 기준을 시스템이 어떻게 반영해야 하는가.",
|
||||
"대필·공저 자서전에서 2018도144(표지갈이)의 형사 법리가 실제로 적용될 위험은 어느 정도인가. "
|
||||
"구술자를 저자로 표기하는 관행은 어떻게 평가되는가.",
|
||||
"자서전의 연보·경력·수상 이력 등 사실 나열 구간은 2019가합537427(수험서)의 "
|
||||
"'창작성 없는 개별 부분에는 저작권 효력이 미치지 않는다'는 판시로 처리해도 되는가.",
|
||||
"A등급 목록에서 빠진 쟁점이 있는가. 특히 자서전 특유의 문제(사자의 명예, 유족 동의, "
|
||||
"제3자 사생활 서술)에 관해 실무상 참조되는 판례를 추가로 알고 있는가.",
|
||||
]
|
||||
|
||||
DISCLAIMER = (
|
||||
"본 문서는 법률 의견서가 아니라 데이터 정리 결과입니다. 등급과 쟁점 요약은 적재된 판시 "
|
||||
"본문을 근거로 부여한 것이며, 법적 효력을 갖는 해석이 아닙니다. 각 사건의 판결 원문은 "
|
||||
"대법원 종합법률정보에서 사건번호로 다시 확인해야 합니다."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 작성 도우미
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _title_block(ws, title: str, subtitle: str = "", width_cols: int = 4) -> int:
|
||||
"""시트 상단 제목. 다음에 쓸 행 번호를 돌려준다."""
|
||||
ws.cell(row=1, column=1, value=title).font = TITLE_FONT
|
||||
row = 2
|
||||
if subtitle:
|
||||
cell = ws.cell(row=2, column=1, value=subtitle)
|
||||
cell.font = NOTE_FONT
|
||||
cell.alignment = TOP_WRAP
|
||||
ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=width_cols)
|
||||
row = 3
|
||||
return row + 1
|
||||
|
||||
|
||||
def _header_row(ws, row: int, labels: list[str]) -> None:
|
||||
for col, label in enumerate(labels, start=1):
|
||||
cell = ws.cell(row=row, column=col, value=label)
|
||||
cell.font = HEAD_FONT
|
||||
cell.fill = HEAD_FILL
|
||||
cell.alignment = CENTER
|
||||
cell.border = CELL_BORDER
|
||||
|
||||
|
||||
def _widths(ws, widths: list[int]) -> None:
|
||||
for i, w in enumerate(widths, start=1):
|
||||
ws.column_dimensions[get_column_letter(i)].width = w
|
||||
|
||||
|
||||
def _body(cell, *, wrap: bool = True) -> None:
|
||||
cell.font = BODY_FONT
|
||||
cell.alignment = TOP_WRAP if wrap else TOP_LEFT
|
||||
cell.border = CELL_BORDER
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 1 — 판례 목록
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sheet_list(wb: Workbook, rows: list[dict]) -> None:
|
||||
ws = wb.create_sheet("판례 목록")
|
||||
counts = Counter(r["grade"] for r in rows)
|
||||
head = _title_block(
|
||||
ws,
|
||||
"자서전·출판물 표절 판단 근거 판례",
|
||||
f"운영 적재본 545건 전수 검토 → 선정 {len(rows)}건 "
|
||||
f"(A {counts['A']} · B {counts['B']} · C {counts['C']}). "
|
||||
"등급 정의와 선정 방법은 '요약·방법' 시트 참조.",
|
||||
width_cols=len(COLUMNS),
|
||||
)
|
||||
|
||||
_header_row(ws, head, [label for _, label, _, _ in COLUMNS])
|
||||
_widths(ws, [w for _, _, w, _ in COLUMNS])
|
||||
|
||||
order = {"A": 0, "B": 1, "C": 2}
|
||||
rows = sorted(rows, key=lambda r: (order.get(r["grade"], 9), r["case_id"]))
|
||||
|
||||
for i, rec in enumerate(rows):
|
||||
r = head + 1 + i
|
||||
for c, (field, _, _, wrap) in enumerate(COLUMNS, start=1):
|
||||
value = rec.get(field, "")
|
||||
cell = ws.cell(row=r, column=c)
|
||||
if field == "url" and value:
|
||||
cell.value = "원문 링크"
|
||||
cell.hyperlink = value
|
||||
cell.font = LINK_FONT
|
||||
cell.alignment = TOP_LEFT
|
||||
cell.border = CELL_BORDER
|
||||
continue
|
||||
cell.value = value
|
||||
_body(cell, wrap=wrap)
|
||||
if field == "grade":
|
||||
cell.alignment = CENTER
|
||||
cell.font = Font(name="맑은 고딕", size=10, bold=True, color=INK)
|
||||
if value in GRADE_FILL:
|
||||
cell.fill = GRADE_FILL[value]
|
||||
|
||||
last = head + len(rows)
|
||||
ws.auto_filter.ref = f"A{head}:{get_column_letter(len(COLUMNS))}{last}"
|
||||
ws.freeze_panes = ws.cell(row=head + 1, column=3)
|
||||
ws.sheet_view.zoomScale = 90
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 2 — 요약·방법
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sheet_summary(wb: Workbook, rows: list[dict]) -> None:
|
||||
ws = wb.create_sheet("요약·방법")
|
||||
_widths(ws, [30, 12, 78])
|
||||
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
|
||||
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 in ("운영 적재본", "최종 선정"):
|
||||
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="■ 등급 정의").font = SECTION_FONT
|
||||
r += 1
|
||||
_header_row(ws, r, ["등급", "구분", "정의"])
|
||||
r += 1
|
||||
for grade, label, desc in GRADE_DEF:
|
||||
ws.cell(row=r, column=1, value=grade).fill = GRADE_FILL[grade]
|
||||
ws.cell(row=r, column=2, value=label)
|
||||
ws.cell(row=r, column=3, value=desc)
|
||||
for c in (1, 2, 3):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
ws.cell(row=r, column=1).alignment = CENTER
|
||||
r += 1
|
||||
|
||||
r += 1
|
||||
ws.cell(row=r, column=1, value="■ 본 과제에 가장 직접적인 3건").font = SECTION_FONT
|
||||
r += 1
|
||||
_header_row(ws, r, ["사건번호", "법원", "의의"])
|
||||
r += 1
|
||||
for case_id, court, why in KEY_THREE:
|
||||
ws.cell(row=r, column=1, value=case_id)
|
||||
ws.cell(row=r, column=2, value=court)
|
||||
ws.cell(row=r, column=3, value=why)
|
||||
for c in (1, 2, 3):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
r += 1
|
||||
|
||||
r += 2
|
||||
note = ws.cell(row=r, column=1, value=DISCLAIMER)
|
||||
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 = 44
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 3 — 제외 사유
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sheet_excluded(wb: Workbook) -> None:
|
||||
ws = wb.create_sheet("제외 사유")
|
||||
_widths(ws, [30, 34, 76])
|
||||
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
|
||||
for reason, n, desc in EXCLUDED_COLLECTION:
|
||||
ws.cell(row=r, column=1, value=reason)
|
||||
ws.cell(row=r, column=2, value=n)
|
||||
ws.cell(row=r, column=3, value=desc)
|
||||
for c in (1, 2, 3):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
ws.cell(row=r, column=2).alignment = CENTER
|
||||
r += 1
|
||||
|
||||
r += 1
|
||||
ws.cell(row=r, column=1, value="■ 적재본에는 있으나 리스트업에서 뺀 유형").font = SECTION_FONT
|
||||
r += 1
|
||||
_header_row(ws, r, ["유형", "대표 사건번호", "제외 사유"])
|
||||
r += 1
|
||||
for kind, cases, reason in EXCLUDED_TYPES:
|
||||
ws.cell(row=r, column=1, value=kind)
|
||||
ws.cell(row=r, column=2, value=cases)
|
||||
ws.cell(row=r, column=3, value=reason)
|
||||
for c in (1, 2, 3):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
r += 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 4 — 데이터 공백
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sheet_gaps(wb: Workbook) -> None:
|
||||
ws = wb.create_sheet("데이터 공백")
|
||||
_widths(ws, [6, 40, 58, 60])
|
||||
r = _title_block(
|
||||
ws, "확인된 데이터 공백 — 상담 시 짚어야 할 항목",
|
||||
"이 표의 항목은 아직 해소되지 않았다. 전문가 상담 전에 원문 확보가 필요하거나, "
|
||||
"상담 질의로 돌려야 하는 건이다.",
|
||||
width_cols=4,
|
||||
)
|
||||
_header_row(ws, r, ["#", "항목", "해당 사건", "설명"])
|
||||
r += 1
|
||||
for i, (item, cases, desc) in enumerate(GAPS, start=1):
|
||||
ws.cell(row=r, column=1, value=i)
|
||||
ws.cell(row=r, column=2, value=item)
|
||||
ws.cell(row=r, column=3, value=cases)
|
||||
ws.cell(row=r, column=4, value=desc)
|
||||
for c in range(1, 5):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
ws.cell(row=r, column=1).alignment = CENTER
|
||||
ws.cell(row=r, column=2).font = Font(name="맑은 고딕", size=10, bold=True, color=INK)
|
||||
r += 1
|
||||
|
||||
r += 1
|
||||
warn = ws.cell(
|
||||
row=r, column=2,
|
||||
value="주의: 2012나17150(선덕여왕 항소심)은 1심과 결론이 뒤집힌 사례이며 "
|
||||
"상고심 판단이 적재본에 없다. 항소심 결론을 확정 법리로 인용하지 말 것.",
|
||||
)
|
||||
warn.font = Font(name="맑은 고딕", size=10, bold=True, color="B03A22")
|
||||
warn.alignment = TOP_WRAP
|
||||
ws.merge_cells(start_row=r, start_column=2, end_row=r, end_column=4)
|
||||
ws.row_dimensions[r].height = 32
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 시트 5 — 상담 질의
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sheet_questions(wb: Workbook) -> None:
|
||||
ws = wb.create_sheet("상담 질의")
|
||||
_widths(ws, [6, 104])
|
||||
r = _title_block(
|
||||
ws, "전문가 상담 시 질의 제안",
|
||||
"자서전 직접 판례가 없다는 점이 확인된 이상, 인접 유형 법리의 유추가 "
|
||||
"타당한지가 상담의 핵심이 되어야 한다.",
|
||||
width_cols=2,
|
||||
)
|
||||
_header_row(ws, r, ["#", "질의"])
|
||||
r += 1
|
||||
for i, q in enumerate(QUESTIONS, start=1):
|
||||
ws.cell(row=r, column=1, value=i)
|
||||
ws.cell(row=r, column=2, value=q)
|
||||
for c in (1, 2):
|
||||
_body(ws.cell(row=r, column=c))
|
||||
ws.cell(row=r, column=1).alignment = CENTER
|
||||
ws.row_dimensions[r].height = 34
|
||||
r += 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_rows(path: Path) -> list[dict]:
|
||||
if not path.exists():
|
||||
raise SystemExit(
|
||||
f"{path} 가 없습니다. 먼저 scripts/build_precedent_listup.py 로 CSV 를 만드세요."
|
||||
)
|
||||
with io.open(path, encoding="utf-8-sig", newline="") as fh:
|
||||
rows = list(csv.DictReader(fh))
|
||||
if not rows:
|
||||
raise SystemExit(f"{path} 에 데이터 행이 없습니다.")
|
||||
return rows
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
ap.add_argument("--csv", type=Path,
|
||||
default=ROOT / "data/precedents/precedent_listup.csv")
|
||||
ap.add_argument("--out", type=Path, default=ROOT / "docs/판례_리스트업.xlsx")
|
||||
args = ap.parse_args()
|
||||
|
||||
rows = load_rows(args.csv)
|
||||
|
||||
wb = Workbook()
|
||||
wb.remove(wb.active) # 기본 시트 제거
|
||||
sheet_list(wb, rows)
|
||||
sheet_summary(wb, rows)
|
||||
sheet_excluded(wb)
|
||||
sheet_gaps(wb)
|
||||
sheet_questions(wb)
|
||||
|
||||
args.out.parent.mkdir(parents=True, exist_ok=True)
|
||||
wb.save(args.out)
|
||||
|
||||
counts = Counter(r["grade"] for r in rows)
|
||||
print(f"생성: {args.out}")
|
||||
print(f" 판례 {len(rows)}건 (A {counts['A']} / B {counts['B']} / C {counts['C']})")
|
||||
print(f" 시트 {len(wb.sheetnames)}개: {', '.join(wb.sheetnames)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in New Issue
Block a user