fix(B08): 마스터 밑수 판독 둘 — 「인」은 숫자가 붙을 때만 · 칸을 이어 붙여 읽지 않음
「인」 오독 넷(원문 대조) - 8-6-2 드론방제·8-6-3 지상방제 「(단위 : 인)」은 **소요인력**이지 공종 밑수가 아님 - 13-2-4 야면석 채집 「(단위: 인 당)」은 표 안이 ㎡당·㎥당 두 줄이라 하나로 못 정함 - ⚠ 넓게 「인」을 버리면 2-2-5 천공기가 사라짐(「천공인부 1인당 1대」) — 숫자가 붙었나로 좁게 가름 칸 이어붙임 — 앞 칸 값 + 뒤 칸 단위로 **원문에 없는 밑수**가 서던 자리 - 13-2-4 「0.36 ㎥당」(0.36 은 뒷길이 60 의 ㎡당 값) - 5-19-2 「30 ㎥당」(30 은 표토두께 ㎝) - ⇒ detect_basis 가 **칸 하나 안에서만** 봄. 「100㎥당」처럼 한 칸에 다 있는 것은 그대로 - 마스터 재생성: 밑수 확보 182 → 180 (없는 값 둘이 빠지고 넷이 미확보로 정직해짐) - tmp/tests/test_b08_master_basis_person.py 신설(5건) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -301,6 +301,22 @@ SOURCE_BASIS_NOTE_RE = re.compile(r"([\d,]*\.?\d*)\s*개소\s*사용\s*당")
|
||||
SOURCE_NOTE_LOOKAHEAD = 8
|
||||
|
||||
|
||||
#: ⚠⚠ **「인」은 품의 단위이지 공종 밑수가 아니다**(2026-09-09 원문 대조로 넷이 오독으로 드러남).
|
||||
#: 8-6-2 드론방제·8-6-3 지상방제 「(단위 : 인)」 — 그 「인」은 **소요인력**이고 공종 밑수는
|
||||
#: ha 다. 13-2-4 야면석 채집 「(단위: 인 당)」 — 표 안이 **㎡당·㎥당** 두 줄이다.
|
||||
#: ⇒ **원문에 「<숫자>인당」이 그대로 있을 때만** 밑수로 인정한다.
|
||||
#: ⚠ **넓게 「인」을 통째로 버리면 2-2-5 천공기가 사라진다** — 그 표는 셀 안에 「천공인부
|
||||
#: **1인당** 1대」로 숫자가 붙어 있다. 그래서 「숫자가 붙었나」로 좁게 가른다.
|
||||
PERSON_UNITS = {"인", "공"}
|
||||
|
||||
|
||||
def person_basis_ok(raw_quantity: str | None, unit: str | None) -> bool:
|
||||
"""「인」 계열 밑수를 인정할 것인가 — **숫자가 붙어 있을 때만** 참."""
|
||||
if unit not in PERSON_UNITS:
|
||||
return True
|
||||
return bool((raw_quantity or "").strip())
|
||||
|
||||
|
||||
def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str | None]:
|
||||
"""표 바로 위 본문에서 밑수를 읽는다. 못 찾으면 `(None, None)` — 1 로 단정하지 않는다.
|
||||
|
||||
@@ -315,6 +331,8 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
break # 앞 표에 닿았다 — 그 위는 남의 밑수다
|
||||
if m := SOURCE_BASIS_RATIO_RE.search(text):
|
||||
raw = (m.group(1) or "").replace(",", "")
|
||||
if not person_basis_ok(raw, m.group(2)):
|
||||
continue # 「인」에 숫자가 안 붙었다 — 품의 단위이지 밑수가 아니다
|
||||
try:
|
||||
quantity = float(raw) if raw else 1.0
|
||||
except ValueError:
|
||||
@@ -324,6 +342,8 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
unit = m.group("u1") or m.group("u2")
|
||||
raw = (m.group(1) if m.group("u1") else m.group(3)) or ""
|
||||
raw = raw.replace(",", "")
|
||||
if not person_basis_ok(raw, unit):
|
||||
continue # 위와 같음 — 「(단위 : 인)」·「(단위: 인 당)」은 밑수가 아니다
|
||||
try:
|
||||
quantity = float(raw) if raw else 1.0
|
||||
except ValueError:
|
||||
@@ -347,14 +367,29 @@ def basis_from_source(lines: list[str], line_no: int) -> tuple[float | None, str
|
||||
|
||||
|
||||
def detect_basis(table: dict[str, Any]) -> tuple[float | None, str | None]:
|
||||
"""「100㎥당」 같은 밑수. 없으면 `(None, None)` — 단위당 1 로 단정하지 않는다."""
|
||||
hay = " ".join(norm(h) for h in table.get("headers", []))
|
||||
hay += " " + " ".join(norm(c) for r in table.get("rows", [])[:2] for c in r)
|
||||
if m := BASIS_RE.search(hay):
|
||||
try:
|
||||
return float(m.group(1).replace(",", "")), m.group(2)
|
||||
except ValueError:
|
||||
return None, m.group(2)
|
||||
"""「100㎥당」 같은 밑수. 없으면 `(None, None)` — 단위당 1 로 단정하지 않는다.
|
||||
|
||||
⚠⚠ **칸 하나 안에서만 본다**(2026-09-09). 여러 칸을 이어 붙여 보면 **앞 칸의 값**과
|
||||
**뒤 칸의 단위**가 붙어 없는 밑수가 생긴다 — 13-2-4 야면석 채집이 그랬다:
|
||||
행 ① 인 부 | ㎡당 | 0.11 | 0.17 | 0.22 | 0.28 | **0.36**
|
||||
행 ② **㎥당** | 0.60 | …
|
||||
이어 붙이면 「0.36 ㎥당」이 되어 **밑수 0.36㎥** 라는 값이 선다(원문에 없는 값).
|
||||
⚠ 그 표는 **㎡당·㎥당 두 줄**이라 애초에 하나로 못 정한다 — 「미확보」가 정직하다.
|
||||
⚠ **「인」은 여기서도 숫자가 붙어야 인정한다** — `BASIS_RE` 가 숫자를 요구하므로
|
||||
2-2-5 「천공인부 **1인당** 1대」는 그대로 서고 「(단위 : 인)」은 안 선다.
|
||||
"""
|
||||
cells = [norm(h) for h in table.get("headers", []) or []]
|
||||
cells += [norm(c) for r in (table.get("rows", []) or [])[:2] for c in r]
|
||||
for cell in cells:
|
||||
if not cell:
|
||||
continue
|
||||
if m := BASIS_RE.search(cell):
|
||||
if not person_basis_ok(m.group(1), m.group(2)):
|
||||
continue
|
||||
try:
|
||||
return float(m.group(1).replace(",", "")), m.group(2)
|
||||
except ValueError:
|
||||
return None, m.group(2)
|
||||
return None, None
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-08T08:33:10+09:00",
|
||||
"generated_at": "2026-09-09T11:44:28+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "3752af1e0328a2faf4947268e449e0bc602a0738cc6a29a78ed71d343cf2e583",
|
||||
"size_bytes": 856832
|
||||
"sha256": "3462899767156784158f724513c793052711806260e60db59d3e2a5fbf1fab21",
|
||||
"size_bytes": 856810
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
@@ -22,8 +22,8 @@
|
||||
},
|
||||
{
|
||||
"file": "basis_missing_2026-01-01.json",
|
||||
"sha256": "244851609c65ba8adbfc318668224cbf3a81032e9271cfda916de435d460dd92",
|
||||
"size_bytes": 18360
|
||||
"sha256": "644b179f7d4b57641fc4ea6df27ef3e3904a03a0bf49c6a4a8bcf44f1195bdb1",
|
||||
"size_bytes": 19018
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -280,6 +280,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 2653
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0127",
|
||||
"section": "5-19-2. 표토펴기 및 고르기",
|
||||
"pum_form": "requirement",
|
||||
"line": 2664
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0129",
|
||||
"section": "5-21. 표토이식",
|
||||
@@ -586,6 +592,24 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 4434
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0228",
|
||||
"section": "8-6-2. 드론방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4516
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0229",
|
||||
"section": "8-6-2. 드론방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4536
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0230",
|
||||
"section": "8-6-3. 지상방제",
|
||||
"pum_form": "requirement",
|
||||
"line": 4557
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0232",
|
||||
"section": "8-7. 방제 실행 등록",
|
||||
@@ -754,6 +778,12 @@
|
||||
"pum_form": "requirement",
|
||||
"line": 6943
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0401",
|
||||
"section": "13-2-4. 야면석 채집(인력)",
|
||||
"pum_form": "requirement",
|
||||
"line": 7025
|
||||
},
|
||||
{
|
||||
"pum_table_id": "F0405",
|
||||
"section": "13-3. 기초다짐 및 뒤채움’ 항을 적용한다.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "work_item_master_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
"generated_at": "2026-09-08T08:33:10+09:00",
|
||||
"generated_at": "2026-09-09T11:44:28+09:00",
|
||||
"dataset_version": {
|
||||
"dataset_id": "pum_forest",
|
||||
"effective_date": "2026-01-01",
|
||||
@@ -21,9 +21,9 @@
|
||||
"tables_attached": 456,
|
||||
"tables_orphan": 19,
|
||||
"form_undetermined": 77,
|
||||
"basis_found": 185,
|
||||
"basis_missing": 135,
|
||||
"basis_grouped": 36
|
||||
"basis_found": 180,
|
||||
"basis_missing": 140,
|
||||
"basis_grouped": 35
|
||||
},
|
||||
"orphan_tables": [
|
||||
{
|
||||
@@ -15789,9 +15789,9 @@
|
||||
"source_line": 2664,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 30.0,
|
||||
"basis_unit": "㎥",
|
||||
"basis_source": "표 안",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -26997,9 +26997,9 @@
|
||||
"source_line": 4516,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -27087,9 +27087,9 @@
|
||||
"source_line": 4536,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -27187,9 +27187,9 @@
|
||||
"source_line": 4557,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
@@ -39551,9 +39551,9 @@
|
||||
"source_line": 7025,
|
||||
"pum_form": "requirement",
|
||||
"form_basis": "직종 표기((인)·인부·공)",
|
||||
"basis_quantity": 1.0,
|
||||
"basis_unit": "인",
|
||||
"basis_source": "본문",
|
||||
"basis_quantity": null,
|
||||
"basis_unit": null,
|
||||
"basis_source": null,
|
||||
"resource_shares": {},
|
||||
"partial_ratio": false,
|
||||
"expression_cells": [],
|
||||
|
||||
Reference in New Issue
Block a user