diff --git a/B09_Estimation/B09_Estimation_UI_Page.ts b/B09_Estimation/B09_Estimation_UI_Page.ts index 1a2d3a5d..e82e33f0 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.ts +++ b/B09_Estimation/B09_Estimation_UI_Page.ts @@ -108,6 +108,9 @@ interface UnitPriceDetailDto { expense: string; total: string; sum_matches: boolean; + /** 품셈 표에 있는데 아직 안 붙은 줄 — 있으면 이 단가는 **붙은 줄만의 값**이다. */ + unattached: string[]; + unattached_note: string; rows: UnitPriceDetailRow[]; } @@ -355,6 +358,17 @@ function buildUnitPriceDetail( ` · ${detail.sum_matches ? L("B09_Estimation_UP_SumOk") : L("B09_Estimation_UP_SumBad")}`; wrap.append(caption); + // ⚠ **일부만 선 단가는 반드시 말한다.** 안 말하면 조용히 싼 값이 내역서에 그대로 든다 + // (2026-09-09 실측: 일위대가가 선 141 공종 중 70 공종이 이 자리 — 초류종자살포는 + // 자재 다섯·장비 셋이 빠진 채 인력 둘만으로 서 있었다). + if (detail.unattached_note) { + const gap = document.createElement("div"); + gap.className = "b09-hint"; + gap.style.fontWeight = "600"; + gap.textContent = detail.unattached_note; + wrap.append(gap); + } + // 행별로 0.1원 미만을 버리므로 전정밀 합과 끝자리가 어긋난다 — **정상이다.** // 숨기면 나중에 「합계가 안 맞는다」며 계산을 고치려 든다. if (detail.precise_total !== detail.total) { diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 538a4989..09e86a52 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -92,6 +92,12 @@ class UnitPriceBuild: #: ⚠ 일위대가가 **아예 안 선** 경우에도 남는다 — 「일위대가 없음」과 「성분이 빠져 #: 못 세움」은 할 일이 다르므로 화면에서 갈라 보여야 한다(2026-09-08 산마루측구). component_gaps: dict[str, str] = field(default_factory=dict) + #: 공종코드 → **표에 있는데 못 붙은 줄 이름들**. + #: ⚠ 값이 있다는 것은 그 단가가 **표의 일부만으로 서 있다**는 뜻이다 — 대개 자재·기계 + #: 카탈로그가 없어서다. 막지는 않지만(막으면 정상 공종이 무더기로 멈춘다) **화면이 + #: 반드시 말해야 한다.** 안 말하면 조용히 싼 단가가 내역서에 그대로 든다 + #: (2026-09-09 실측: 일위대가가 선 141 공종 중 **70 공종**이 이 자리였다). + unattached: dict[str, list[str]] = field(default_factory=dict) #: 배분율 표인데 일부 몫만 붙은 공종 — 「단가가 일부만 섬」. 값은 붙은 몫(%). partial_ratio: dict[str, Decimal] = field(default_factory=dict) #: 시공능력 공식으로 장비 몫을 세운 공종 — 산출근거를 화면에 그대로 보인다. @@ -354,6 +360,13 @@ def build_unit_prices( build = UnitPriceBuild() build.component_gaps = dict(axis.partial_items) + for unmatched_row in axis.unmatched: + # ⚠ 지역 이름을 조심할 것 — 바로 위 `names` 는 **공종 이름표**다. 같은 이름을 쓰면 + # 그 표가 리스트로 덮여 조립이 통째로 터진다(2026-09-09 실측). + labels = build.unattached.setdefault(unmatched_row.work_item_code, []) + label = " ".join(str(unmatched_row.cell).split()) + if label and label not in labels: + labels.append(label) missing_basis = load_basis_missing() wages = load_operator_wages() _add_labor_titles(build.book, wages) diff --git a/B09_Estimation/B09_Estimation_UnitPrice_View.py b/B09_Estimation/B09_Estimation_UnitPrice_View.py index 14c57ed6..b3cd3790 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice_View.py +++ b/B09_Estimation/B09_Estimation_UnitPrice_View.py @@ -165,6 +165,9 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict: """본표 — 「그것이 무엇으로 이루어졌나」. 줄마다 원천과 파고들기 여부를 함께 낸다.""" title = build.book.title(code) money = build.book.resolve(code) + # 코드에서 공종을 도로 뽑는다 — 「B-FP-09-11-01#갈래」의 갈래는 떼고 본다. + work_item_code = code[2:].split("#")[0] if code.startswith("B-") else "" + unattached = list(build.unattached.get(work_item_code, [])) rows: list[dict] = [] for detail in build.book.details.get(code, []): if detail.percent_of_labor is not None: @@ -255,4 +258,16 @@ def detail_of(build: UnitPriceBuild, code: str) -> dict: # 전정밀 합과의 차이 — 행별 절사 탓에 끝자리가 어긋나는 것은 **정상**이다. "precise_total": _money_text(money.total), "rows": rows, + # ⚠ **표에 있는데 못 붙은 줄** — 이 단가가 일부만으로 섰다는 뜻이다. + # 안 보이면 조용히 싼 단가가 내역서에 그대로 든다. + "unattached": unattached, + "unattached_note": ( + f"⚠ 품셈 표에 있는 {len(unattached)}줄이 아직 안 붙었습니다 — " + f"{', '.join(unattached[:4])}" + + (" 등" if len(unattached) > 4 else "") + + ". 자재·기계 카탈로그가 서면 채워집니다. 그때까지 이 단가는 " + "**붙은 줄만의 값**입니다." + ) + if unattached + else "", }