lawdangle-kr 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lawdangle/__init__.py +51 -0
- lawdangle/classifier.py +202 -0
- lawdangle/cli.py +196 -0
- lawdangle/mapper.py +286 -0
- lawdangle/models.py +100 -0
- lawdangle/parser.py +87 -0
- lawdangle/report.py +86 -0
- lawdangle/resolver.py +575 -0
- lawdangle_kr-0.1.0.dist-info/METADATA +189 -0
- lawdangle_kr-0.1.0.dist-info/RECORD +13 -0
- lawdangle_kr-0.1.0.dist-info/WHEEL +4 -0
- lawdangle_kr-0.1.0.dist-info/entry_points.txt +2 -0
- lawdangle_kr-0.1.0.dist-info/licenses/LICENSE +21 -0
lawdangle/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""lawdangle — Dead Cross-Reference Detector for Korean Statutes.
|
|
2
|
+
|
|
3
|
+
현행 법령이 인용하는 대상 법령의 폐지·개명·이관·사문화를 탐지하고 5분류로 태깅한다.
|
|
4
|
+
배포명 `lawdangle-kr` / import명 `lawdangle` (DESIGN.md 네이밍).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .classifier import classify
|
|
10
|
+
from .cli import run, run_law
|
|
11
|
+
from .mapper import (
|
|
12
|
+
MappingSuggestion,
|
|
13
|
+
discover_successors,
|
|
14
|
+
rank_correspondence,
|
|
15
|
+
suggest_mapping,
|
|
16
|
+
suggest_mapping_auto,
|
|
17
|
+
)
|
|
18
|
+
from .models import (
|
|
19
|
+
Category,
|
|
20
|
+
Citation,
|
|
21
|
+
Confidence,
|
|
22
|
+
HistoryInfo,
|
|
23
|
+
LawStatus,
|
|
24
|
+
Result,
|
|
25
|
+
)
|
|
26
|
+
from .parser import parse_citations
|
|
27
|
+
from .resolver import FixtureResolver, LawGoKrResolver, Resolver
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Category",
|
|
33
|
+
"Citation",
|
|
34
|
+
"Confidence",
|
|
35
|
+
"HistoryInfo",
|
|
36
|
+
"LawStatus",
|
|
37
|
+
"Result",
|
|
38
|
+
"classify",
|
|
39
|
+
"run",
|
|
40
|
+
"run_law",
|
|
41
|
+
"parse_citations",
|
|
42
|
+
"Resolver",
|
|
43
|
+
"FixtureResolver",
|
|
44
|
+
"LawGoKrResolver",
|
|
45
|
+
"suggest_mapping",
|
|
46
|
+
"suggest_mapping_auto",
|
|
47
|
+
"discover_successors",
|
|
48
|
+
"rank_correspondence",
|
|
49
|
+
"MappingSuggestion",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
lawdangle/classifier.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""③ Classifier — 5분류 (도구의 심장).
|
|
2
|
+
|
|
3
|
+
DESIGN.md §3 ③ 의사결정 트리 구현.
|
|
4
|
+
핵심 원칙: C/D/E는 자동 단정 금지 — 플래그만 달고 사람이 본다.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from .models import Category, Citation, Confidence, HistoryInfo, LawStatus, Result
|
|
12
|
+
|
|
13
|
+
_ART_KEY_RE = re.compile(r"(제\d+조(?:의\d+)?)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _article_key(article: str | None) -> str | None:
|
|
17
|
+
"""'제17조제2항' → 조(條) 단위 키 '제17조'."""
|
|
18
|
+
if not article:
|
|
19
|
+
return None
|
|
20
|
+
m = _ART_KEY_RE.match(article)
|
|
21
|
+
return m.group(1) if m else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_name(name: str) -> str:
|
|
25
|
+
"""법명 정규화 — 약칭/표기차(가운뎃점ㆍ, 띄어쓰기)를 개명으로 오판 방지.
|
|
26
|
+
|
|
27
|
+
DESIGN.md §6 함정 1.
|
|
28
|
+
"""
|
|
29
|
+
return name.replace("ㆍ", "").replace("·", "").replace(" ", "").strip()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def classify(citation: Citation, history: HistoryInfo) -> Result:
|
|
33
|
+
"""인용 레코드 + 연혁 → 5분류 판정.
|
|
34
|
+
|
|
35
|
+
의사결정 트리(DESIGN.md §3 ③):
|
|
36
|
+
1. 현행 유지? → 법명 동일? → 정상 / 개명(A)
|
|
37
|
+
2. 폐지 → 후속법 수로 B / C / D·E 분기
|
|
38
|
+
"""
|
|
39
|
+
cited_name = citation.cited_law_name
|
|
40
|
+
status = history.status
|
|
41
|
+
|
|
42
|
+
# --- 1. 현행 유지 ---------------------------------------------------- #
|
|
43
|
+
if status == LawStatus.CURRENT:
|
|
44
|
+
current = history.current_name or cited_name
|
|
45
|
+
if _normalize_name(cited_name) == _normalize_name(current):
|
|
46
|
+
# 정상 — 분류 대상 아님. category=None.
|
|
47
|
+
return Result(
|
|
48
|
+
citation=citation,
|
|
49
|
+
history=history,
|
|
50
|
+
category=None,
|
|
51
|
+
confidence=Confidence.HIGH,
|
|
52
|
+
note="현행 유지·법명 일치 (정상)",
|
|
53
|
+
)
|
|
54
|
+
# 현행이지만 인용 법명이 현행명과 다름 → 약칭/부분명/표기차 = A(법명만 교체)
|
|
55
|
+
cause = {
|
|
56
|
+
"alias": "약칭",
|
|
57
|
+
"partial": "부분명(앞부분 생략)",
|
|
58
|
+
}.get(history.name_form, "표기차/개명")
|
|
59
|
+
return Result(
|
|
60
|
+
citation=citation,
|
|
61
|
+
history=history,
|
|
62
|
+
category=Category.A,
|
|
63
|
+
confidence=Confidence.HIGH,
|
|
64
|
+
successor_suggestion=current,
|
|
65
|
+
note=f"{cause}: 「{cited_name}」 → 현행 「{current}」 (풀네임 표기 권장)",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# --- 2. 개명(법인격 동일) → 조문 보존 여부로 A vs B 분기 ------------- #
|
|
69
|
+
if status == LawStatus.RENAMED:
|
|
70
|
+
current = history.current_name
|
|
71
|
+
art_key = _article_key(citation.cited_article)
|
|
72
|
+
|
|
73
|
+
# 인용 조문이 현행본에 살아있으면 깨끗한 개명(A: 법명만 교체).
|
|
74
|
+
if art_key is None or not history.alive_articles or art_key in history.alive_articles:
|
|
75
|
+
return Result(
|
|
76
|
+
citation=citation,
|
|
77
|
+
history=history,
|
|
78
|
+
category=Category.A,
|
|
79
|
+
confidence=Confidence.HIGH,
|
|
80
|
+
successor_suggestion=current,
|
|
81
|
+
note=f"단순 개명: 「{cited_name}」 → 「{current}」 (법명만 교체)",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# 법은 개명됐으나 인용 조문이 현행본에 없음(삭제/전부개정 재번호) → B.
|
|
85
|
+
return Result(
|
|
86
|
+
citation=citation,
|
|
87
|
+
history=history,
|
|
88
|
+
category=Category.B,
|
|
89
|
+
confidence=Confidence.MEDIUM,
|
|
90
|
+
successor_suggestion=current,
|
|
91
|
+
flag=True,
|
|
92
|
+
note=(
|
|
93
|
+
f"개명+조문 이동: 「{cited_name}」→「{current}」(법인격 동일)이나 "
|
|
94
|
+
f"인용 {art_key}이 현행본에 없음(삭제/재번호). 법명+조문 교체 필요 — "
|
|
95
|
+
"--map 으로 대응 조문 확인 [수동]"
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# --- 3. 폐지 → 후속법 분석 ------------------------------------------ #
|
|
100
|
+
if status == LawStatus.REPEALED:
|
|
101
|
+
n = len(history.successors)
|
|
102
|
+
|
|
103
|
+
if n == 1:
|
|
104
|
+
# 후속법 단일. 조문 대응이 깨질 수 있으므로(함정 2) 조문 교체 플래그.
|
|
105
|
+
# 단정 자동: B. 단, 조문번호 변경 가능성은 note로 경고.
|
|
106
|
+
return Result(
|
|
107
|
+
citation=citation,
|
|
108
|
+
history=history,
|
|
109
|
+
category=Category.B,
|
|
110
|
+
confidence=Confidence.MEDIUM,
|
|
111
|
+
successor_suggestion=history.successors[0],
|
|
112
|
+
flag=True,
|
|
113
|
+
note=(
|
|
114
|
+
f"전부개정·이관: 제도가 「{history.successors[0]}」(으)로 승계. "
|
|
115
|
+
"법명+조문 교체 필요 — 조문번호 대응 확인할 것"
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if n >= 2:
|
|
120
|
+
# 후속법 여럿 → 분할 승계 C. 호 단위 수동 매핑 필수.
|
|
121
|
+
return Result(
|
|
122
|
+
citation=citation,
|
|
123
|
+
history=history,
|
|
124
|
+
category=Category.C,
|
|
125
|
+
confidence=Confidence.MANUAL,
|
|
126
|
+
successor_suggestion=", ".join(history.successors),
|
|
127
|
+
flag=True,
|
|
128
|
+
note=(
|
|
129
|
+
"분할 승계(1:N): 내용이 여러 법으로 분산. "
|
|
130
|
+
"호 단위 수동 매핑 필요 [수동]"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# n == 0: 확정 후속법 없음 → 폐지법 상세 산문 신호로 보강.
|
|
135
|
+
# 자동으로 D/E를 단정하지 않는다 — 모두 manual_required(오탐 차단, §3③).
|
|
136
|
+
|
|
137
|
+
if history.absorbed:
|
|
138
|
+
# "일반회계로 통합/흡수" 등 → 후속'법'이 없고 자체 흡수 → D 강신호.
|
|
139
|
+
note = "폐지·흡수통합: " + (history.repeal_reason or "타 회계/제도로 흡수")
|
|
140
|
+
return Result(
|
|
141
|
+
citation=citation,
|
|
142
|
+
history=history,
|
|
143
|
+
category=Category.D,
|
|
144
|
+
confidence=Confidence.MANUAL,
|
|
145
|
+
successor_suggestion=None,
|
|
146
|
+
flag=True,
|
|
147
|
+
note=note + " — 자체 완결(D) 유력, 수동 확인 [수동]",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if history.successor_candidates:
|
|
151
|
+
# 후속법 후보(타법폐지 역추적/제개정이유) — 개수로 B(이관)/C(분할) 구분.
|
|
152
|
+
cands = history.successor_candidates
|
|
153
|
+
joined = ", ".join(cands)
|
|
154
|
+
if len(cands) == 1:
|
|
155
|
+
return Result(
|
|
156
|
+
citation=citation,
|
|
157
|
+
history=history,
|
|
158
|
+
category=Category.B,
|
|
159
|
+
confidence=Confidence.MEDIUM,
|
|
160
|
+
successor_suggestion=cands[0],
|
|
161
|
+
flag=True,
|
|
162
|
+
note=(
|
|
163
|
+
f"폐지·이관 후보: 「{cands[0]}」(으)로 이관 추정. "
|
|
164
|
+
"법명+조문 교체 — --map 으로 대응 조문 확인 [수동]"
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
return Result(
|
|
168
|
+
citation=citation,
|
|
169
|
+
history=history,
|
|
170
|
+
category=Category.C,
|
|
171
|
+
confidence=Confidence.MANUAL,
|
|
172
|
+
successor_suggestion=joined,
|
|
173
|
+
flag=True,
|
|
174
|
+
note=(
|
|
175
|
+
f"폐지·분할 승계 후보: 「{joined}」 — 내용이 여러 법으로 분산. "
|
|
176
|
+
"호 단위 수동 매핑 필요 [수동]"
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# 신호 없음 → D(자체 완결) / E(효과 마비) 갈림. 기본 후보 D, 최종은 사람이.
|
|
181
|
+
return Result(
|
|
182
|
+
citation=citation,
|
|
183
|
+
history=history,
|
|
184
|
+
category=Category.D,
|
|
185
|
+
confidence=Confidence.MANUAL,
|
|
186
|
+
successor_suggestion=None,
|
|
187
|
+
flag=True,
|
|
188
|
+
note=(
|
|
189
|
+
"폐지·후속법 없음: 자체 완결(D)인지 효과 마비(E)인지 수동 확인 필요. "
|
|
190
|
+
"E일 경우 최우선 검토 [수동]"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# --- status UNKNOWN: 조회 실패 -------------------------------------- #
|
|
195
|
+
return Result(
|
|
196
|
+
citation=citation,
|
|
197
|
+
history=history,
|
|
198
|
+
category=None,
|
|
199
|
+
confidence=Confidence.LOW,
|
|
200
|
+
flag=True,
|
|
201
|
+
note="연혁 조회 실패 — 대상 법령 상태 미확인 [수동]",
|
|
202
|
+
)
|
lawdangle/cli.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""`lawdangle <법령ID 또는 파일>` — 한 줄 실행.
|
|
2
|
+
|
|
3
|
+
DESIGN.md §3 ④ / §4.
|
|
4
|
+
파이프라인: parse → resolve → classify → report.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import report
|
|
15
|
+
from .classifier import classify
|
|
16
|
+
from .mapper import enrich_result, suggest_mapping, suggest_mapping_auto
|
|
17
|
+
from .parser import parse_citations
|
|
18
|
+
from .resolver import FixtureResolver, LawGoKrResolver, Resolver
|
|
19
|
+
from .models import Result
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_resolver(args) -> Resolver:
|
|
23
|
+
"""--fixture 우선, 없으면 OC(법제처 API 키)로 라이브 resolver."""
|
|
24
|
+
if args.fixture:
|
|
25
|
+
return FixtureResolver.from_files(*args.fixture)
|
|
26
|
+
oc = args.oc or os.environ.get("LAW_OC")
|
|
27
|
+
if not oc:
|
|
28
|
+
sys.exit(
|
|
29
|
+
"법제처 API 키(OC)가 없습니다. --oc 또는 LAW_OC 환경변수, "
|
|
30
|
+
"또는 오프라인이면 --fixture <연혁.json> 을 지정하세요."
|
|
31
|
+
)
|
|
32
|
+
return LawGoKrResolver(oc)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(
|
|
36
|
+
text: str, resolver: Resolver, *, citing_law: str = "", deep: bool = False
|
|
37
|
+
) -> list[Result]:
|
|
38
|
+
"""파이프라인 한 번: 텍스트 → 판정 결과 리스트.
|
|
39
|
+
|
|
40
|
+
deep=True 면 B/이관·개명+조문이동 건에 구체 대응 조문 제안까지 붙인다
|
|
41
|
+
(라이브 resolver 필요 — 조문 매핑은 네트워크 조회).
|
|
42
|
+
"""
|
|
43
|
+
citations = parse_citations(text, citing_law=citing_law)
|
|
44
|
+
results = [classify(c, resolver.resolve(c.cited_law_name)) for c in citations]
|
|
45
|
+
if deep and isinstance(resolver, LawGoKrResolver):
|
|
46
|
+
results = [enrich_result(r, resolver) for r in results]
|
|
47
|
+
return results
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_law(law_name: str, resolver: LawGoKrResolver, *, deep: bool = False) -> list[Result]:
|
|
51
|
+
"""법령명으로 현행 본문을 직접 가져와 조 단위로 죽은 인용을 분석한다.
|
|
52
|
+
|
|
53
|
+
텍스트 붙여넣기의 약점(판례 혼입·표기깨짐·citing_article 미상)을 피하는
|
|
54
|
+
권장 입력 경로. 각 조문에서 인용을 추출하고 citing_article을 채운다.
|
|
55
|
+
"""
|
|
56
|
+
arts = resolver.current_articles(law_name)
|
|
57
|
+
if not arts:
|
|
58
|
+
raise ValueError(f"현행 법령 「{law_name}」을(를) 찾지 못했습니다(법령명 확인).")
|
|
59
|
+
results: list[Result] = []
|
|
60
|
+
for art_key, body in arts.items():
|
|
61
|
+
for c in parse_citations(body, citing_law=law_name, citing_article=art_key):
|
|
62
|
+
r = classify(c, resolver.resolve(c.cited_law_name))
|
|
63
|
+
if deep:
|
|
64
|
+
r = enrich_result(r, resolver)
|
|
65
|
+
results.append(r)
|
|
66
|
+
return results
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _run_map(args) -> int:
|
|
70
|
+
"""--map: '옛법령 조문 [후속법령]' → 조문 대응 순위 제안(라이브 전용).
|
|
71
|
+
|
|
72
|
+
후속법령 생략 시 본문 기반으로 후속법을 자동 발견(분할 이관 포함).
|
|
73
|
+
"""
|
|
74
|
+
oc = args.oc or os.environ.get("LAW_OC")
|
|
75
|
+
if not oc:
|
|
76
|
+
sys.exit("조문 대응 제안은 법제처 API 키(--oc 또는 LAW_OC)가 필요합니다.")
|
|
77
|
+
if len(args.map) not in (2, 3):
|
|
78
|
+
sys.exit("--map 는 '옛법령 조문' 또는 '옛법령 조문 후속법령' 형식입니다.")
|
|
79
|
+
resolver = LawGoKrResolver(oc)
|
|
80
|
+
|
|
81
|
+
if len(args.map) == 3:
|
|
82
|
+
old_law, article, successor = args.map
|
|
83
|
+
s = suggest_mapping(resolver, old_law, article, successor)
|
|
84
|
+
else:
|
|
85
|
+
old_law, article = args.map
|
|
86
|
+
print("후속법 자동 발견 중… (본문 기반)")
|
|
87
|
+
s = suggest_mapping_auto(resolver, old_law, article)
|
|
88
|
+
if s is None:
|
|
89
|
+
print(f"옛 법 「{old_law}」 {article}의 후속법을 찾지 못했습니다.")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
print(f"옛 법령 : 「{s.old_law}」 {s.old_article}"
|
|
93
|
+
+ (f" (실본문 {s.old_article_version} 시행본)" if s.old_article_version else ""))
|
|
94
|
+
if s.old_snippet:
|
|
95
|
+
print(f"옛 조문 : {s.old_snippet}…")
|
|
96
|
+
print(f"후속법령: 「{s.successor_law}」" + (" (자동발견)" if len(args.map) == 2 else ""))
|
|
97
|
+
print(f"판정 : {'유력' if s.confident else '수동확인'} — {s.note}")
|
|
98
|
+
if s.candidates:
|
|
99
|
+
print("후보(유사도순):")
|
|
100
|
+
for i, c in enumerate(s.candidates, 1):
|
|
101
|
+
print(f" {i}. {c.article} (유사도 {c.score}) {c.snippet}…")
|
|
102
|
+
else:
|
|
103
|
+
print("후보: 없음")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv: list[str] | None = None) -> int:
|
|
108
|
+
p = argparse.ArgumentParser(
|
|
109
|
+
prog="lawdangle",
|
|
110
|
+
description="현행 법령의 죽은 참조(폐지·개명·이관·사문화)를 탐지·5분류.",
|
|
111
|
+
)
|
|
112
|
+
p.add_argument(
|
|
113
|
+
"input",
|
|
114
|
+
nargs="?",
|
|
115
|
+
help="법령 본문 텍스트 파일 경로 (- 는 표준입력). --law/--map 모드에선 생략",
|
|
116
|
+
)
|
|
117
|
+
p.add_argument(
|
|
118
|
+
"--law",
|
|
119
|
+
metavar="법령명",
|
|
120
|
+
help="법령명으로 현행 본문을 가져와 조 단위로 분석(권장 입력, 라이브)",
|
|
121
|
+
)
|
|
122
|
+
p.add_argument(
|
|
123
|
+
"--map",
|
|
124
|
+
nargs="+",
|
|
125
|
+
metavar="옛법령 조문 [후속법령]",
|
|
126
|
+
help="조문 대응 제안: '옛법령 조문' (후속법 자동발견) 또는 '옛법령 조문 후속법령' (직접 지정)",
|
|
127
|
+
)
|
|
128
|
+
p.add_argument("--citing-law", default="", help="인용하는 쪽 법령명(리포트용)")
|
|
129
|
+
p.add_argument("--oc", help="법제처 OPEN API 인증키(OC). 없으면 LAW_OC 환경변수")
|
|
130
|
+
p.add_argument(
|
|
131
|
+
"--fixture",
|
|
132
|
+
nargs="+",
|
|
133
|
+
help="오프라인 연혁 fixture JSON (지정 시 API 미사용)",
|
|
134
|
+
)
|
|
135
|
+
p.add_argument(
|
|
136
|
+
"--format",
|
|
137
|
+
choices=("csv", "json", "summary"),
|
|
138
|
+
default="summary",
|
|
139
|
+
help="출력 형식 (기본 summary)",
|
|
140
|
+
)
|
|
141
|
+
p.add_argument(
|
|
142
|
+
"--deep",
|
|
143
|
+
action="store_true",
|
|
144
|
+
help="B/개명+조문이동 건에 구체 대응 조문까지 매핑(라이브, 느림)",
|
|
145
|
+
)
|
|
146
|
+
p.add_argument("-o", "--output", help="출력 파일 (미지정 시 표준출력)")
|
|
147
|
+
|
|
148
|
+
# Windows 콘솔(cp949) 등에서 한글/em-dash 출력 깨짐·크래시 방지.
|
|
149
|
+
# parse_args() 전에 재설정해야 --help·인자 오류 메시지도 깨지지 않음.
|
|
150
|
+
for stream in (sys.stdout, sys.stderr):
|
|
151
|
+
if hasattr(stream, "reconfigure"):
|
|
152
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
153
|
+
|
|
154
|
+
args = p.parse_args(argv)
|
|
155
|
+
|
|
156
|
+
# --- 조문 대응 제안 모드 -------------------------------------------- #
|
|
157
|
+
if args.map:
|
|
158
|
+
return _run_map(args)
|
|
159
|
+
|
|
160
|
+
# --- 법령명 분석 모드(권장) ----------------------------------------- #
|
|
161
|
+
if args.law:
|
|
162
|
+
oc = args.oc or os.environ.get("LAW_OC")
|
|
163
|
+
if not oc:
|
|
164
|
+
sys.exit("--law 모드는 법제처 API 키(--oc 또는 LAW_OC)가 필요합니다.")
|
|
165
|
+
try:
|
|
166
|
+
results = run_law(args.law, LawGoKrResolver(oc), deep=args.deep)
|
|
167
|
+
except ValueError as e:
|
|
168
|
+
sys.exit(str(e))
|
|
169
|
+
else:
|
|
170
|
+
if not args.input:
|
|
171
|
+
p.error("input 파일이 필요합니다 (또는 --law / --map 모드를 쓰세요)")
|
|
172
|
+
if args.input == "-":
|
|
173
|
+
text = sys.stdin.read()
|
|
174
|
+
else:
|
|
175
|
+
text = Path(args.input).read_text(encoding="utf-8")
|
|
176
|
+
resolver = _build_resolver(args)
|
|
177
|
+
results = run(text, resolver, citing_law=args.citing_law, deep=args.deep)
|
|
178
|
+
|
|
179
|
+
if args.format == "csv":
|
|
180
|
+
out = report.to_csv(results)
|
|
181
|
+
elif args.format == "json":
|
|
182
|
+
out = report.to_json(results)
|
|
183
|
+
else:
|
|
184
|
+
out = report.format_summary(results)
|
|
185
|
+
|
|
186
|
+
if args.output:
|
|
187
|
+
Path(args.output).write_text(out, encoding="utf-8")
|
|
188
|
+
# 요약은 콘솔에도 한 번 보여준다.
|
|
189
|
+
print(report.format_summary(results), file=sys.stderr)
|
|
190
|
+
else:
|
|
191
|
+
print(out)
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
raise SystemExit(main())
|