diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Table.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Table.py index 61ace190..5fb3eff1 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Table.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster_Table.py @@ -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 diff --git a/resources/data_master_labels/labels_2026-01-01.json b/resources/data_master_labels/labels_2026-01-01.json index c279dd06..dc6ba126 100644 --- a/resources/data_master_labels/labels_2026-01-01.json +++ b/resources/data_master_labels/labels_2026-01-01.json @@ -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", diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 5f4db7e7..bea6ed74 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -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 } ] } \ No newline at end of file diff --git a/resources/data_work_item_master/_manifest_const.json b/resources/data_work_item_master/_manifest_const.json index 0341a752..62ca6624 100644 --- a/resources/data_work_item_master/_manifest_const.json +++ b/resources/data_work_item_master/_manifest_const.json @@ -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 } ] } \ No newline at end of file diff --git a/resources/data_work_item_master/basis_missing_2026-01-01.json b/resources/data_work_item_master/basis_missing_2026-01-01.json index 6b7cdc49..05a8ce81 100644 --- a/resources/data_work_item_master/basis_missing_2026-01-01.json +++ b/resources/data_work_item_master/basis_missing_2026-01-01.json @@ -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. 뭉기기", diff --git a/resources/data_work_item_master/const_basis_missing_2026-01-01.json b/resources/data_work_item_master/const_basis_missing_2026-01-01.json index fed14479..ea0565ac 100644 --- a/resources/data_work_item_master/const_basis_missing_2026-01-01.json +++ b/resources/data_work_item_master/const_basis_missing_2026-01-01.json @@ -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 설치", diff --git a/resources/data_work_item_master/const_work_item_master_2026-01-01.json b/resources/data_work_item_master/const_work_item_master_2026-01-01.json index 08b6ae8d..babb2cc8 100644 --- a/resources/data_work_item_master/const_work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/const_work_item_master_2026-01-01.json @@ -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": [], diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index 8a33ff1a..322c7736 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_2026-01-01.json @@ -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": [], diff --git a/resources/tester/test_work_item_key_registry.py b/resources/tester/test_work_item_key_registry.py index 9c5a13a9..42e4f67b 100644 --- a/resources/tester/test_work_item_key_registry.py +++ b/resources/tester/test_work_item_key_registry.py @@ -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