From c59f36eccd0c415667491532b75ddcd92bfdf4b5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 19:47:43 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=ED=91=9C=EB=B3=B8=EC=9D=B4=20?= =?UTF-8?q?=EC=96=87=EC=9D=80=20=EB=85=B8=EC=9E=84=EC=97=90=20=EC=8B=A0?= =?UTF-8?q?=EB=A2=B0=EB=8F=84=20=ED=91=9C=EC=8B=9C=20+=20=EC=B8=B5?= =?UTF-8?q?=EB=94=B0=EA=B8=B0=20=EC=82=AC=EC=A0=84=20=EB=B0=91=EC=88=98=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLAN 9-6 · 8-32 두 잔손질. ① 미공표 직종 신뢰도 플래그 (9-6) 지식DB 가 「단가 채택 시 플래그 유지 필요」(노임단가_적용 §2-3) · 「해당 단가 사용 시 경고 표시」(원가_입력변수_사전)로 두었는데 노임 층에 없었음. - load_labor_reliability() — 원문 기호 `*`(조사현장 5개 미만)·`**`(미조사) 그대로. 정상 공표 직종은 아예 안 담음(빈 값과 「정상」을 구별할 일이 없게). - PriceTitle.reliability — 노임 제목 전부 + 조종원 줄에도 같이 붙임. - build_summary 의 labor_reliability → 화면 힌트 줄로 경고. ⚠ 금액을 막지 않음 — 값은 그대로 서고 「조사현장이 적다」는 사실만 나름. ⚠ 내역에 실제로 실린 것만 경고 — 카탈로그 40직종을 다 띄우면 쓰지도 않는 직종이 화면을 채움. 실측 둘: 1037 벌목부 · 1050 일반기계운전사(지식DB 예시 그대로). ② 층따기 사전 밑수 (8-32) 원가_출력변수_사전 B2-9 가 「㎡·m」였음. 품셈 9-18 은 ㎥ 당이고, 우리도 확정 2차 ① 로 면적이 정본이되 길이를 곱해 ㎥ 로 내보내고 있음. 두 축을 한 칸에 적어 ㎡ 로 내도 되는 것처럼 읽히던 자리 — 밑수를 ㎥ 로 고치고 까닭을 비고에 적음. 코드는 이미 ㎥ 로 내보내 수치 변화 없음. 시험 여섯 신설 · 전체 1182 통과 · 실패 0. Co-Authored-By: Claude Opus 5 (1M context) --- B09_Estimation/B09_Estimation_UI_Page.ts | 13 +++ .../B09_Estimation_UnitPrice_View.py | 7 ++ .../tester/test_b09_labor_reliability.py | 94 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 resources/tester/test_b09_labor_reliability.py diff --git a/B09_Estimation/B09_Estimation_UI_Page.ts b/B09_Estimation/B09_Estimation_UI_Page.ts index e82e33f0..20c89fa0 100644 --- a/B09_Estimation/B09_Estimation_UI_Page.ts +++ b/B09_Estimation/B09_Estimation_UI_Page.ts @@ -82,6 +82,8 @@ interface UnitPriceListDto { unit_prices: number; machine_hourly: number; notes: string[]; + // 표본이 얇은 노임 — 금액은 서 있고 「조사현장이 적다」는 사실만 알린다. + labor_reliability: Array<{ code: string; name: string; flag: string; why: string }>; }; rows: UnitPriceRow[]; } @@ -175,6 +177,8 @@ function injectStyles(): void { } .b09-panel__actions { display: flex; gap: var(--space-xs, 4px); margin-top: var(--space-sm, 8px); } .b09-hint { font-size: var(--font-size-xs, 12px); color: var(--color-text-secondary); } +/* 표본이 얇은 노임 — 막는 것이 아니라 눈에 띄기만 하면 된다. */ +.b09-hint--warn { color: var(--color-warning-text, #8a5a00); } /* ⚠ min-width: 0 이 빠지면 **표가 넓은 만큼 이 칸이 통째로 밀려 나간다**(2026-09-08 실측: 창 620 에서 1,190px 넘침). flex 자식의 기본 최소폭이 auto 라 안쪽 표의 최소폭 @@ -849,6 +853,15 @@ export async function renderB09Estimation(root: HTMLElement): Promise { line.textContent = note; body.append(line); } + // ⚠ 표본이 얇은 노임(`*` 조사현장 5개 미만 · `**` 미조사)이 내역에 실렸으면 알린다. + // 금액을 막지 않는다 — 값은 그대로 서고 **무엇이 얇은지**만 말한다 + // (지식DB `노임단가_적용 §2-3` 「단가 채택 시 플래그 유지 필요」). + for (const item of unitPriceList.summary.labor_reliability ?? []) { + const line = document.createElement("div"); + line.className = "b09-hint b09-hint--warn"; + line.textContent = `⚠ ${item.name}(${item.code}) ${item.flag} — ${item.why}`; + body.append(line); + } body.append( buildUnitPriceList(unitPriceList, selectedUnitPrice, (code) => { void openUnitPrice(code); diff --git a/B09_Estimation/B09_Estimation_UnitPrice_View.py b/B09_Estimation/B09_Estimation_UnitPrice_View.py index 200999ec..e541b4a8 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice_View.py +++ b/B09_Estimation/B09_Estimation_UnitPrice_View.py @@ -125,6 +125,13 @@ def build_summary(build: UnitPriceBuild) -> dict: "skipped_work_items": len(build.skipped), "incomplete_machines": len(build.incomplete_machines), "kinds": kinds, + # ⚠ 표본이 얇은 노임이 내역에 실렸으면 **화면이 말해야 한다** — 금액은 그대로 서고 + # 「이 단가는 조사현장이 적다」만 알린다(지식DB `노임단가_적용 §2-3` + # 「단가 채택 시 플래그 유지 필요」, 2026-09-09에 이음). + "labor_reliability": [ + {"code": code, "name": name, "flag": flag, "why": why} + for code, (name, flag, why) in sorted(build.labor_reliability.items()) + ], # ⚠ 지금 상태를 화면에 그대로 알린다 (PLAN 9-6 미결). "notes": _status_notes(), } diff --git a/resources/tester/test_b09_labor_reliability.py b/resources/tester/test_b09_labor_reliability.py new file mode 100644 index 00000000..72d1ab0f --- /dev/null +++ b/resources/tester/test_b09_labor_reliability.py @@ -0,0 +1,94 @@ +"""표본이 얇은 노임에 신뢰도 표시 (2026-09-09 · PLAN 9-6). + +⚠⚠ **금액을 막지 않는다.** 단가는 그대로 서고 「조사현장이 적다」는 사실만 나른다 — + 지식DB `노임단가_적용 §2-3` 이 「프로그램에서 단가 채택 시 **플래그 유지 필요**」로, + `원가_입력변수_사전` 이 「해당 단가 사용 시 **경고 표시**」로 두었는데 노임 층에 + 그 표시가 없었다. + +⚠ 겨누는 것 다섯 + ① 원문 기호 그대로 — `*`(조사현장 5개 미만) · `**`(미조사). 우리가 낱말을 지어내지 않음 + ② 정상 공표 직종은 **아예 안 담김** (빈 값과 「정상」을 구별할 일이 없게) + ③ 노임 제목에 그 기호가 실림 — 조종원도 노임이라 같이 붙음 + ④ ⚠ **내역에 실제로 실린 것만** 경고 — 카탈로그 40직종을 다 띄우면 화면이 묻힘 + ⑤ 화면 요약에 사람 말로 실려 나감 +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B09_Estimation.B09_Estimation_MachineOperating import ( # noqa: E402 + LABOR_RELIABILITY_LABEL, + load_labor_reliability, +) +from B09_Estimation.B09_Estimation_PriceBook import PriceKind # noqa: E402 +from B09_Estimation.B09_Estimation_UnitPrice import cached_build # noqa: E402 +from B09_Estimation.B09_Estimation_UnitPrice_View import build_summary # noqa: E402 + + +def test_원문_기호를_그대로_쓴다() -> None: + """⚠ 「주의」·「낮음」 같은 우리 낱말로 바꾸면 원문과 못 맞춘다.""" + flags = set(load_labor_reliability().values()) + assert flags <= {"*", "**"}, flags + assert set(LABOR_RELIABILITY_LABEL) == {"*", "**"} + + +def test_정상_직종은_담기지_않는다() -> None: + """⚠ 빈 값을 담으면 「정상」과 「모름」이 구별되지 않는다 — 있는 것만 담는다.""" + table = load_labor_reliability() + assert all(v.strip() for v in table.values()) + # 지식DB 가 「원문 40직종 해당」이라 적어 둔 자리다. + assert len(table) == 40 + + +def test_노임_제목에_기호가_실린다() -> None: + build = cached_build() + flagged = load_labor_reliability() + checked = 0 + for code, flag in flagged.items(): + title = build.book.titles.get(code) + if title is None: + continue + assert title.kind is PriceKind.LABOR + assert title.reliability == flag, code + checked += 1 + assert checked > 0, "노임 제목이 하나도 안 섰다" + + +def test_정상_직종_제목에는_빈_값이다() -> None: + build = cached_build() + flagged = load_labor_reliability() + normals = [ + title + for code, title in build.book.titles.items() + if title.kind is PriceKind.LABOR and code not in flagged + ] + assert normals, "정상 공표 직종 제목이 하나도 없다" + assert all(title.reliability == "" for title in normals) + + +def test_내역에_실린_것만_경고한다() -> None: + """⚠ 카탈로그 40직종을 다 띄우면 **쓰지도 않는 직종**이 화면을 채운다.""" + build = cached_build() + used = build.labor_reliability + assert 0 < len(used) < len(load_labor_reliability()) + # 실제로 일위대가에 붙은 줄만 들어 있다. + attached = {row.ref_code for rows in build.book.details.values() for row in rows} + assert set(used) <= attached + + +def test_화면_요약에_사람_말로_나간다() -> None: + """⚠ 기호만 보내면 사용자가 `*` 가 무슨 뜻인지 모른다 — 뜻을 함께 보낸다.""" + summary = build_summary(cached_build()) + rows = summary["labor_reliability"] + assert rows, "표본이 얇은 노임이 하나도 안 실렸다" + for row in rows: + assert set(row) == {"code", "name", "flag", "why"} + assert row["flag"] in {"*", "**"} + assert row["why"] == LABOR_RELIABILITY_LABEL[row["flag"]] + # 지식DB 가 예로 든 임도 빈발 직종 — 실제로 우리 내역에 실린다. + assert any(row["code"] == "1050" for row in rows), "일반기계운전사(1050)가 안 잡힌다"