diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py index ba5cb8d7..525b1d64 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py @@ -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 diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index fbf2c438..9d997dbb 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -155,6 +155,14 @@ def _leaf_row( if item.application_ratio_pct is not None: # ⚠ 곱하지 않는다 — B08 이 이미 곱한 값이다. 산출근거로만 적는다. row.note = f"반영률 {item.application_ratio_pct}% 적용 후 수량" + elif item.application_ratio_breakdown: + # ⚠ **「율 없음」이 아니라 「갈래마다 다름」이다.** 율이 갈리는 줄은 B08 이 `pct` 를 + # 비우고 갈래로만 보낸다. 그 사실을 안 적으면 **값은 맞는데 왜 그 수량인지**를 + # 사람이 못 본다 — 값이 맞아도 그것은 반쪽이다(2026-09-09 두 창 확인). + parts = ", ".join( + f"{name} {value}%" for name, value in item.application_ratio_breakdown.items() + ) + row.note = f"반영률이 갈래마다 다릅니다 — {parts} (적용 후 수량)" if not item.in_bill: # 검산용 줄 — 수량은 보이되 **단가를 안 붙인다**(PLAN 8-7 ㉡ 와 같은 성격). diff --git a/main.py b/main.py index b3f96e6f..a3967f44 100644 --- a/main.py +++ b/main.py @@ -50,6 +50,7 @@ from B05_Profile.B05_Profile_Router_Lifecycle import router as b05_route_lifecyc from B05_Profile.B05_Profile_Router_Replan import router as b05_route_replan_router from B05_Profile.B05_Profile_Structures_Router import router as b05_structures_router from B06_Section.B06_Section_Router import router as b06_section_router +from B06_Section.B06_Section_Router_Stations import router as b06_section_stations_router from B06_Section.B06_Section_Router_Confirm import ( router as b06_section_confirm_router, ) @@ -538,6 +539,7 @@ app.include_router(b05_corridor_router, dependencies=protected_with_company) app.include_router(b05_route_replan_router, dependencies=protected_with_company) app.include_router(b05_structures_router, dependencies=protected_with_company) app.include_router(b06_section_router, dependencies=protected_with_company) +app.include_router(b06_section_stations_router, dependencies=protected_with_company) app.include_router(b06_section_confirm_router, dependencies=protected_with_company) app.include_router(b06_section_haul_plan_router, dependencies=protected_with_company) app.include_router(b07_design_router, dependencies=protected_with_company) diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 2abebbdb..9be25944 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-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 } ] } \ 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 a15cf0d1..2627bc66 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 @@ -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. 기초다짐 및 뒤채움’ 항을 적용한다.", 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 44658daf..bb11ffc2 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 @@ -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": [],