feat(b08): 표머리 몫꼴에서 밑수 읽기 — 꼴마다 한 번씩 · 산림 18 · 건설 23 이 한꺼번에 채워짐

안티그래비티 자리 명세(2dab2ce2)가 준 것은 **값이 아니라 읽는 규칙**이었음(155 중 142가
「표머리 명시 단위」 자리표시). 그래서 하나씩 넣지 않고 꼴을 코드로 짬.

- 본문 쪽 몫꼴(`SOURCE_BASIS_RATIO_RE`)은 이미 있었으나 **표머리는 안 보고 있었음** —
  `인/ha`·`인/100kg`·`소요인력(인/본당)`·`재료비(1km 소요량기준)` 을 읽게 함. **분모가 밑수**
- ⚠ 반쪽 진실보다 「미확보」 — 거절하는 자리 둘
  · 분모가 둘인 몫(`ℓ/일,대`) — 대수를 안 곱해 조용히 적게 섬
  · 표머리끼리 어긋나는 표(`주연료 (ℓ/hr)` + `조종원 (인/일)`) — 먼저 걸린 것을 고르면 반은 틀림
- 차례가 뜻 — 표 안 「…당」이 먼저, 표머리 몫꼴은 그 뒤. **덮어쓴 것 0 · 사라진 것 0**

밑수 확보 산림 204 → 222(미확보 115 → 99) · 건설 515 → 538(447 → 439)
지문 한 번 갱신 — 값이 움직인 것이 아니라 **곱하면 안 되던 줄이 곱할 수 있게 된 것**
이름표 줄 수도 맞춤(basis_missing 115→99 · const 447→439)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
This commit is contained in:
2026-09-17 18:38:53 +09:00
co-authored by Claude Opus 5
parent be4addb7f7
commit 85255f5ec5
9 changed files with 232 additions and 288 deletions
@@ -312,6 +312,49 @@ def basis_from_name(name: str) -> tuple[float | None, str | None]:
return None, None
#: 밑수가 될 수 있는 **세는 단위**(분모 자리).
_DENOM = "㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대|일|시간|hr"
#: 값이 붙는 **재는 단위**(분자 자리). 이쪽이 단위꼴이어야 「몫」이지 규격이 아니다.
_NUMER = "인|공||L|l|㎏|kg|톤|ton|m|㎝|cm|매|본|개|주|대|시간|hr|㎥|m3|㎡|m2"
#: ⚠ **표머리에 「당」 없이 몫으로만 적힌 밑수** — `인/ha` · `/일,대` · `m/본` (명세 꼴 B).
#: 본문 쪽은 `SOURCE_BASIS_RATIO_RE` 가 이미 보는데 **표머리는 안 보고 있었다.**
#: ⚠ 좁게 잡는다 — **양쪽이 다 단위꼴일 때만**. 안 그러면 `(무한궤도,0.7㎥)` 같은
#: 규격 칸이 밑수로 둔갑한다(옛날에 실제로 다섯 건 걸렸다).
HEADER_RATIO_RE = re.compile(
rf"(?:^|[(\s])(?:단위\s*[:]\s*)?(?:{_NUMER})\s*/\s*([\d,]*\.?\d*)\s*({_DENOM})\s*당?(?:$|[,)\s])"
)
#: `재료비(1km 소요량기준)` 꼴 — 「당」 대신 「소요량기준」으로 적은 표머리.
HEADER_BASIS_NOTE_RE = re.compile(rf"([\d,]*\.?\d*)\s*({_DENOM})\s*소요량\s*기준")
#: 분모가 둘인 몫 — `ℓ/일,대` · `인/㎥·일`. 밑수가 하나로 안 정해진다.
COMPOUND_DENOM_RE = re.compile(rf"/\s*[\d,]*\.?\d*\s*(?:{_DENOM})\s*[,,·]\s*(?:{_DENOM})")
def basis_from_header(cell: str) -> tuple[float | None, str | None]:
"""표머리 한 칸에서 몫꼴 밑수를 읽는다. 못 읽으면 `(None, None)`.
⚠ 분모가 밑수다 — `인/ha` 는 「1ha당 몇 인」이라 밑수는 `ha` 다. 분자(인)를 밑수로
읽으면 뜻이 뒤집힌다.
"""
# ⚠ **분모가 둘인 몫은 거절한다** — `ℓ/일,대` 는 「하루에 한 대당」이라 밑수가 둘이다.
# 한쪽(일)만 적으면 대수를 안 곱해 조용히 적게 선다. 반쪽 진실보다 「미확보」가 정직하다.
if COMPOUND_DENOM_RE.search(cell):
return None, None
for pattern in (HEADER_RATIO_RE, HEADER_BASIS_NOTE_RE):
if m := pattern.search(cell):
raw = (m.group(1) or "").replace(",", "")
unit = m.group(2)
if not person_basis_ok(raw, unit):
continue
try:
return (float(raw) if raw else 1.0), unit
except ValueError:
return 1.0, unit
return None, None
def detect_basis(table: dict[str, Any]) -> tuple[float | None, str | None]:
"""「100㎥당」 같은 밑수. 없으면 `(None, None)` — 단위당 1 로 단정하지 않는다.
@@ -336,6 +379,17 @@ def detect_basis(table: dict[str, Any]) -> tuple[float | None, str | None]:
return float(m.group(1).replace(",", "")), m.group(2)
except ValueError:
return None, m.group(2)
# 「당」이 없는 몫꼴은 **표머리에서만** 본다(줄 값에는 규격이 섞여 있다).
# ⚠ 표머리가 **서로 다른 분모**를 말하면 거절한다 — 건설 8장 기계경비표가 한 표 안에서
# `주연료 (/hr)` 와 `조종원 (인/일)` 을 함께 적는다. 먼저 걸린 것을 고르면 반은 틀린다.
found = {
basis_from_header(cell)
for cell in (norm(h) for h in table.get("headers", []) or [])
if cell
}
found = {hit for hit in found if hit[1]}
if len(found) == 1:
return found.pop()
return None, None
@@ -6327,7 +6327,7 @@
"name_ko": "밑수 못 찾은 표",
"summary": "기준 수량을 못 읽은 품셈 표. 1 단위당으로 단정하면 곱셈이 틀린다.",
"shape": "list",
"rows": 115,
"rows": 99,
"columns": [
{
"key": "pum_table_id",
@@ -7315,7 +7315,7 @@
"name_ko": "밑수 미확보 표",
"summary": "기준 수량을 못 찾은 표.",
"shape": "list",
"rows": 447,
"rows": 439,
"columns": [
{
"key": "pum_table_id",
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-17T18:22:17+09:00",
"generated_at": "2026-09-17T18:29:07+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": "ffa114016725768042a7b59f4daf6ab7188595e8c649208c659dcd9787c5ca5d",
"size_bytes": 925084
"sha256": "ef9a9d0424a2b602b74640e6bdb49da3ccdd31c873367d404a09d1ede7efd882",
"size_bytes": 925160
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -22,8 +22,8 @@
},
{
"file": "basis_missing_2026-01-01.json",
"sha256": "d57f26c736829ef40c50dc616d96358db785295d1aa0d5959aa7bc9eaa9be842",
"size_bytes": 15845
"sha256": "15306da88ffa36bbe33b65c0f290fcaab94888e494e61546b1b6a2949d814a90",
"size_bytes": 13680
}
]
}
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-17T18:22:18+09:00",
"generated_at": "2026-09-17T18:29:08+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_const",
@@ -12,8 +12,8 @@
"files": [
{
"file": "const_work_item_master_2026-01-01.json",
"sha256": "9744ac236de8f08582a3aa36b50125aef76acac18094bd06d23d442183fffe0c",
"size_bytes": 3780649
"sha256": "f81019fc9699d01c074f55fc1e8074d16ce7b0a81c0291fcbb63fb5094b3c9b5",
"size_bytes": 3780743
},
{
"file": "const_form_undetermined_2026-01-01.json",
@@ -22,8 +22,8 @@
},
{
"file": "const_basis_missing_2026-01-01.json",
"sha256": "0ae14342e37ba5f394c36cc4dd8cea1b505b13d6520e0a12979c21676d6ffcdb",
"size_bytes": 74228
"sha256": "65f401c717fcbb97323d60240f8207a1d8e7598c2f8a5f6e1b57e2f0b637bd85",
"size_bytes": 72968
}
]
}
@@ -16,12 +16,6 @@
"pum_form": "requirement",
"line": 1328
},
{
"pum_table_id": "F0049",
"section": "2-1-4. 페인트 및 마킹테이프",
"pum_form": "requirement",
"line": 1402
},
{
"pum_table_id": "F0050",
"section": "2-1-4. 페인트 및 마킹테이프",
@@ -52,36 +46,12 @@
"pum_form": "requirement",
"line": 1579
},
{
"pum_table_id": "F0075",
"section": "3-1. 경계표시",
"pum_form": "requirement",
"line": 1731
},
{
"pum_table_id": "F0077",
"section": "3-3. 작업로 설치",
"pum_form": "requirement",
"line": 1754
},
{
"pum_table_id": "F0078",
"section": "3-4-3. 임산물 운반로 및 작업로 보수비 산정",
"pum_form": "requirement",
"line": 1815
},
{
"pum_table_id": "F0080",
"section": "3-6. 산물 임내정리",
"pum_form": "requirement",
"line": 1867
},
{
"pum_table_id": "F0081",
"section": "3-7. 재해산물 수집",
"pum_form": "requirement",
"line": 1887
},
{
"pum_table_id": "F0082",
"section": "3-8. 드론 영상 촬영",
@@ -106,12 +76,6 @@
"pum_form": "requirement",
"line": 1957
},
{
"pum_table_id": "F0088",
"section": "4-3. 위험목 베기",
"pum_form": "requirement",
"line": 2012
},
{
"pum_table_id": "F0089",
"section": "4-4. 가지정리",
@@ -184,12 +148,6 @@
"pum_form": "requirement",
"line": 2919
},
{
"pum_table_id": "F0153",
"section": "6-1. 비료주기",
"pum_form": "requirement",
"line": 3062
},
{
"pum_table_id": "F0154",
"section": "6-2-1. 둘레베기",
@@ -304,12 +262,6 @@
"pum_form": "requirement",
"line": 3556
},
{
"pum_table_id": "F0181",
"section": "7-7-2. 숲가꾸기, 병해충방제",
"pum_form": "requirement",
"line": 3580
},
{
"pum_table_id": "F0182",
"section": "7-8-1. 가선설치",
@@ -328,30 +280,6 @@
"pum_form": "requirement",
"line": 3631
},
{
"pum_table_id": "F0185",
"section": "7-9-1. 수확",
"pum_form": "requirement",
"line": 3656
},
{
"pum_table_id": "F0186",
"section": "7-9-2. 숲가꾸기, 소나무재선충병방제",
"pum_form": "requirement",
"line": 3673
},
{
"pum_table_id": "F0189",
"section": "7-11. 동력상하차기(우드그래플) 집재-수확",
"pum_form": "productivity",
"line": 3723
},
{
"pum_table_id": "F0190",
"section": "7-11. 동력상하차기(우드그래플) 집재-수확",
"pum_form": "productivity",
"line": 3731
},
{
"pum_table_id": "F0191",
"section": "7-11. 동력상하차기(우드그래플) 집재-수확",
@@ -508,12 +436,6 @@
"pum_form": "requirement",
"line": 4583
},
{
"pum_table_id": "F0235",
"section": "8-9. 잔가지줍기",
"pum_form": "requirement",
"line": 4623
},
{
"pum_table_id": "F0236",
"section": "8-10. 그물망 피복",
@@ -532,18 +454,6 @@
"pum_form": "requirement",
"line": 4675
},
{
"pum_table_id": "F0241",
"section": "9-4-1. 암파쇄",
"pum_form": "productivity",
"line": 4719
},
{
"pum_table_id": "F0244",
"section": "9-5-2. 깎기(90%)",
"pum_form": "productivity",
"line": 4759
},
{
"pum_table_id": "F0251",
"section": "9-8-1. T=30㎝ 미만",
@@ -664,12 +574,6 @@
"pum_form": "requirement",
"line": 7415
},
{
"pum_table_id": "F0430",
"section": "13-10-2. 나무 말뚝박기",
"pum_form": "requirement",
"line": 7491
},
{
"pum_table_id": "F0437",
"section": "13-12-1. 뭉기기",
@@ -166,12 +166,6 @@
"pum_form": "requirement",
"line": 211
},
{
"pum_table_id": "C0176",
"section": "3-3-6 암발파(대규모발파 TYPE-Ⅵ)('20, '26년 보완)(㎥당)",
"pum_form": "productivity",
"line": 229
},
{
"pum_table_id": "C0183",
"section": "3-4 쌓기3-4-1 흙쌓기('25년 신설)",
@@ -220,12 +214,6 @@
"pum_form": "requirement",
"line": 660
},
{
"pum_table_id": "C0217",
"section": "3-10 개간3-10-1 답면고르기('03년 신설)",
"pum_form": "productivity",
"line": 722
},
{
"pum_table_id": "C0218",
"section": "3-11 스마트 토공3-11-1 머신 가이던스(MG) 굴착기('23년 신설, '24, '26년 보완)",
@@ -760,18 +748,6 @@
"pum_form": "requirement",
"line": 2415
},
{
"pum_table_id": "C0618",
"section": "8-3-5 [40]콘크리트기계(4108) 콘크리트 배치플랜트",
"pum_form": "productivity",
"line": 2422
},
{
"pum_table_id": "C0619",
"section": "8-3-5 [40]콘크리트기계(4108) 콘크리트 배치플랜트",
"pum_form": "productivity",
"line": 2431
},
{
"pum_table_id": "C0620",
"section": "8-3-5 [40]콘크리트기계(4108) 콘크리트 배치플랜트",
@@ -796,12 +772,6 @@
"pum_form": "productivity",
"line": 2479
},
{
"pum_table_id": "C0625",
"section": "8-3-5 [40]콘크리트기계(4108) 콘크리트 배치플랜트",
"pum_form": "productivity",
"line": 2486
},
{
"pum_table_id": "C0640",
"section": "8-3-6 [50]골재생산기계 등(5105) 크러셔(이동식) ('11년 보완)",
@@ -1006,12 +976,6 @@
"pum_form": "requirement",
"line": 512
},
{
"pum_table_id": "C0849",
"section": "3-2-2 기계굴착의 능력('07, '20년 보완)",
"pum_form": "productivity",
"line": 83
},
{
"pum_table_id": "C0856",
"section": "3-2-5 터널굴착 1발파당 작업인원('07, '20년 보완)(1발파당)",
@@ -2212,24 +2176,12 @@
"pum_form": "requirement",
"line": 2092
},
{
"pum_table_id": "C1788",
"section": "13-6-6 Stop-Log 설치",
"pum_form": "requirement",
"line": 2280
},
{
"pum_table_id": "C1789",
"section": "13-6-6 Stop-Log 설치",
"pum_form": "requirement",
"line": 2300
},
{
"pum_table_id": "C1792",
"section": "13-6-7 수문 Hoist 설치",
"pum_form": "requirement",
"line": 2325
},
{
"pum_table_id": "C1796",
"section": "80713-6-8 Spiral Casing 설치",
@@ -4,7 +4,7 @@
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"toc_edition": "2026-01-01",
"generated_at": "2026-09-17T18:22:18+09:00",
"generated_at": "2026-09-17T18:29:08+09:00",
"dataset_version": {
"dataset_id": "pum_const",
"effective_date": "2026-01-01",
@@ -39,8 +39,8 @@
"tables_attached": 2192,
"tables_orphan": 0,
"form_undetermined": 881,
"basis_found": 515,
"basis_missing": 447,
"basis_found": 538,
"basis_missing": 439,
"basis_grouped": 54,
"axis_roles": {
"group": 258,
@@ -790,9 +790,9 @@
"source_line": 197,
"pum_form": "reference",
"form_basis": "품셈 제1장(적용기준)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "㎥",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -825,9 +825,9 @@
"source_line": 203,
"pum_form": "reference",
"form_basis": "품셈 제1장(적용기준)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "㎥",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -1099,9 +1099,9 @@
"source_line": 273,
"pum_form": "reference",
"form_basis": "품셈 제1장(적용기준)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -11829,9 +11829,9 @@
"source_line": 229,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -14878,9 +14878,9 @@
"source_line": 722,
"pum_form": "productivity",
"form_basis": "헤더 '㎡/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -32589,9 +32589,9 @@
"source_line": 783,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [
@@ -33351,9 +33351,9 @@
"source_line": 936,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "m",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -33530,9 +33530,9 @@
"source_line": 982,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "m",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -33618,9 +33618,9 @@
"source_line": 996,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "m",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -33763,9 +33763,9 @@
"source_line": 1019,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "m",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -41847,9 +41847,9 @@
"source_line": 2330,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -42586,9 +42586,9 @@
"source_line": 2422,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -42643,9 +42643,9 @@
"source_line": 2431,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -43069,9 +43069,9 @@
"source_line": 2486,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -43198,9 +43198,9 @@
"source_line": 2501,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "m",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -43274,9 +43274,9 @@
"source_line": 2508,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -59146,9 +59146,9 @@
"source_line": 83,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -137687,9 +137687,9 @@
"source_line": 978,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -145817,9 +145817,9 @@
"source_line": 2122,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ton",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -145938,9 +145938,9 @@
"source_line": 2149,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ton",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -146441,9 +146441,9 @@
"source_line": 2254,
"pum_form": "undetermined",
"form_basis": "헤더·첫 행에 단위·밑수·직종 표지 없음",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ton",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -146592,9 +146592,9 @@
"source_line": 2280,
"pum_form": "requirement",
"form_basis": "'수량'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "일",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -146829,9 +146829,9 @@
"source_line": 2325,
"pum_form": "requirement",
"form_basis": "'수량'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "일",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -4,7 +4,7 @@
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"toc_edition": "산림청고시제2025-82호",
"generated_at": "2026-09-17T18:22:17+09:00",
"generated_at": "2026-09-17T18:29:07+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -25,9 +25,9 @@
"tables_attached": 453,
"tables_orphan": 22,
"form_undetermined": 14,
"basis_found": 204,
"basis_missing": 115,
"basis_grouped": 48,
"basis_found": 222,
"basis_missing": 99,
"basis_grouped": 49,
"axis_roles": {
"general_provision": 78,
"group": 85,
@@ -4635,9 +4635,9 @@
"source_line": 1402,
"pum_form": "requirement",
"form_basis": "'소요량'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "km",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -6196,9 +6196,9 @@
"source_line": 1731,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -6298,9 +6298,9 @@
"source_line": 1754,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "km",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -6560,9 +6560,9 @@
"source_line": 1867,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -6632,9 +6632,9 @@
"source_line": 1887,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -7260,9 +7260,9 @@
"source_line": 2012,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "본",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -12388,9 +12388,9 @@
"source_line": 3062,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 100.0,
"basis_unit": "kg",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -13043,9 +13043,9 @@
"source_line": 3234,
"pum_form": "reference",
"form_basis": "헤더 '할인'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -14808,9 +14808,9 @@
"source_line": 3580,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -15212,9 +15212,9 @@
"source_line": 3656,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -15306,9 +15306,9 @@
"source_line": 3673,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -15382,9 +15382,9 @@
"source_line": 3692,
"pum_form": "coefficient",
"form_basis": "사람 판정 — 본문 「(단위 : %)」 — 집재재적·거리별 수집량 증가율",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -15529,9 +15529,9 @@
"source_line": 3723,
"pum_form": "productivity",
"form_basis": "사람 판정 — 표 바로 위 「(㎥/1대, 1일)」 — 대당 일 작업량",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -15602,9 +15602,9 @@
"source_line": 3731,
"pum_form": "productivity",
"form_basis": "사람 판정 — 위와 같음(최대집재거리 80m 이하)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "ha",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -20625,9 +20625,9 @@
"source_line": 4623,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "본",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -21111,9 +21111,9 @@
"source_line": 4719,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -21446,9 +21446,9 @@
"source_line": 4759,
"pum_form": "productivity",
"form_basis": "헤더 '㎥/hr'",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "hr",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -37895,9 +37895,9 @@
"source_line": 7491,
"pum_form": "requirement",
"form_basis": "직종 표기((인)·인부·공)",
"basis_quantity": null,
"basis_unit": null,
"basis_source": null,
"basis_quantity": 1.0,
"basis_unit": "개",
"basis_source": "표 안",
"resource_shares": {},
"partial_ratio": false,
"expression_cells": [],
@@ -41,14 +41,16 @@ from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import ( # noqa: E402
MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
#: 열쇠를 얹기 **전** 벌(`work_items`)의 지문. 2026-09-17 데스크탑_서브가 뜬 값.
#: ⚠ 이 값이 흔들리면 열쇠 작업이 값을 건드렸다는 뜻이다 — 지문을 고치지 말고 원인을 찾을 것.
#: ⚠ 이 값이 흔들리면 값이 움직였다는 뜻이다 — 지문을 고치지 말고 **원인을 먼저 찾을 것.**
#: 2026-09-17 한 번 갱신 — 표머리 몫꼴(`인/ha`·`인/100kg`)에서 **밑수 18개를 새로 읽음.**
#: ⚠ **덮어쓴 것 0 · 사라진 것 0**(더하기만) — 곱하면 안 되던 줄이 곱할 수 있게 된 것뿐이다.
#: 2026-09-17 랩탑 메인 — 목차 오기 둘을 본문 번호로 바로잡아(브레인 지시) **네 줄만** 바뀌어 새로 뜸
#: (옛 f8b8fba1…). 네 줄 모습은 `test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로` 가 잼 ·
#: 미판정·밑수 목록 파일은 한 글자도 안 바뀜.
#: 2026-09-17 랩탑 메인 둘째 — 헛단위 뺌(인용을 제목으로 읽은 13-3 표 셋 · 부록 사례 번호 겹침 일곱)으로 다시 뜸
#: (옛 be88f730…). 바뀐 줄은 13-3·4-1·4-2·2-1·2-2·3-1·3-2·3-3·13-4-1·13-5-2 의 표 귀속뿐 —
#: `test_work_item_master_toc` 가 자리를 잼 · 단가표 대조는 그 파일 머리.
BASELINE_WORK_ITEMS_SHA = "2accafe5e366c32eb6e95123642b4330e28537b2199dee851247e996fcd839da"
BASELINE_WORK_ITEMS_SHA = "40746aa3b4a2ac6a78323590069242088f1fb44f4662dc2f240afe303f385978"
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
ADDED_FIELDS = (
@@ -327,6 +329,38 @@ def test_갈래_문자열이_열쇠라_흔들림을_못박음(master: dict) -> N
assert len(messy) == 6, f"공백·물결표만 다른 짝이 {len(messy)} — 6 이던 것"
def test_표머리_몫꼴_밑수_읽기() -> None:
"""「당」 없이 몫으로만 적힌 표머리에서 밑수를 읽는다 — 분모가 밑수다(명세 꼴 B).
반쪽만 읽느니 미확보 낫다 분모가 둘이거나(`/,`) 표머리끼리 어긋나면 거절한다.
"""
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Table import basis_from_header, detect_basis
assert basis_from_header("(단위 : 인/ha)") == (1.0, "ha")
assert basis_from_header("소요인력 (인/100kg)") == (100.0, "kg")
assert basis_from_header("소요인력(인/본당)") == (1.0, "")
assert basis_from_header("재료비(1km 소요량기준)") == (1.0, "km")
assert basis_from_header("주연료 (/일,대)") == (None, None) # 분모가 둘 — 거절
assert basis_from_header("기계명(주재료)") == (None, None) # 몫이 아님
# 표머리끼리 어긋나면 표 전체를 미확보로 둔다(건설 8장 기계경비표)
= {"headers": ["분류번호", "주연료 (/hr)", "조종원 (인/일)"], "rows": []}
assert detect_basis() == (None, None)
# 하나로 모이면 읽는다
표2 = {"headers": ["구 분", "정리산물(㎥/ha)", "비고"], "rows": []}
assert detect_basis(표2) == (1.0, "ha")
def test_밑수_채움은_더하기만_함(master: dict) -> None:
"""⚠ 이미 있던 밑수를 덮지 않는다 — 덮으면 금액이 조용히 움직인다.
(`BASIS_RE`) 먼저고, 표머리 몫꼴은 ** **에만 본다.
"""
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Table import detect_basis
= {"headers": ["(단위 : 인/ha)", "100㎡당"], "rows": []}
assert detect_basis() == (100.0, "") # 「당」이 붙은 쪽이 이긴다
def test_어느_구실도_지우지_않음() -> None:
"""브레인 승인(2026-09-17) — 「덜어내기」가 아니라 「가름만」."""
import json as _json