From f50c55a8e24746e90c6d68ee1ebabbc964445a6c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 06:34:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(B09):=20=ED=91=9C=EC=9D=98=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=EB=A7=8C=20=EB=B6=99=EC=9D=80=20=EB=8B=A8=EA=B0=80?= =?UTF-8?q?=EA=B0=80=20=EC=A1=B0=EC=9A=A9=ED=9E=88=20=EC=84=9C=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=20=E2=80=94=20=EB=B3=B8=ED=91=9C=EA=B0=80=20=EB=A7=90?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일위대가가 선 141 공종 중 **70 공종**이 「품셈 표에 있는데 못 붙은 줄」을 갖고 있었음. 막지 않는 것은 맞으나(막으면 정상 공종이 무더기로 멈춤) 화면이 말하지 않아 **조용히 싼 단가**가 내역서에 그대로 들 수 있었음. 실례 — 초류종자살포(FP-05-24-01, 수량 17,873.80㎡) 품셈 5-24-1 은 자재 5(종자·비료·피복제·침식안정제·색소) + 장비 3(종자살포기·트럭· 물탱크) + 인력 2 인데, 우리 단가는 **인력 둘만**으로 233.4원/㎡ 였음. - `UnitPriceBuild.unattached` 신설 — 공종별로 못 붙은 줄 이름을 들고 있음 - 본표(`detail_of`)가 `unattached`·`unattached_note` 를 함께 냄 - 일위대가 상세 화면이 그 문구를 굵게 띄움. 다 붙은 단가는 아무 말도 안 함 (늘 경고가 뜨면 아무도 안 읽음) - 품셈이 자간을 벌려 적은 이름(「종 자」)은 눌러서 보임 ⚠ 지역 이름 충돌 하나 잡음 — `names` 가 공종 이름표라 같은 이름을 쓰니 조립이 통째로 터졌음. 주석으로 못 박음. 검증: pytest 282 통과(신규 3) · tsc 통과 Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_UI_Page.ts | 14 ++++++++++++++ B09_Estimation/B09_Estimation_UnitPrice.py | 13 +++++++++++++ B09_Estimation/B09_Estimation_UnitPrice_View.py | 15 +++++++++++++++ 3 files changed, 42 insertions(+) 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 "", }