From 3e729439cb5439e87bf6e8f8a18410c5610527fd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 05:46:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(B08):=20=EC=9E=90=EA=B8=B0=20=EA=B0=90?= =?UTF-8?q?=EC=82=AC=20=E2=80=94=20=EC=95=88=20=EB=B6=80=EB=A5=B4=EB=8D=98?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20=EB=84=B7=20=EC=97=B0=EA=B2=B0=20+=20?= =?UTF-8?q?=ED=91=9C=ED=86=A0=20=EB=91=90=EA=BB=98=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=B9=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ㉘ 자기 감사. B09 에 겨눈 네 축을 내 것에 그대로 겨눔. 안 부르던 검사 — 시험에서만 불리고 있던 것들을 실제 경로에 이음 - verify_no_code_on_materials · verify_bill_flags → build_handoff 결과에 실음 - check_against_plan → 운반 표 옆에 haul_check 로 실음 (계획이 있을 때만) - 바로 위 주석이 「검사는 실제로 부른다」였는데 형제 둘이 놀고 있었음 표토 두께 — 서버·엔진은 받고 있었는데 화면에 넣을 칸이 없었음 - optionalNumberField 신설 (빈 값 = 「안 정함」, 0 과 구별) - null 을 저장에서 버리던 것 수정 (NULLABLE_SETTING_KEYS) — 한 번 넣으면 「안 정함」으로 못 되돌리던 자리 짝 시험 — 규칙은 있는데 시험이 없던 둘 - REFERENCE_MARKS (41건을 지웠던 그 규칙): 기준표는 잡고 「단 위」는 안 잡는 것을 양쪽으로 박음 - attachments_of: 칸이 있을 때만 줄이 서고 부모와 id 가 갈리는 것 시험: 662 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관) · tsc 오류 0. Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 4 ++ B08_Quantity/B08_Quantity_Router_Earthwork.py | 25 ++++++++- B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 2 + B08_Quantity/B08_Quantity_UI_Page.ts | 52 +++++++++++++++++++ ui_template/ui_template_locale_b2.ts | 3 ++ 5 files changed, 85 insertions(+), 1 deletion(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index 94d6e298..35bdea2f 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -775,7 +775,11 @@ def build_handoff( "excluded_row_count": sum(1 for row in work_items if not row["in_bill"]), } # ⚠ 검사는 **실제로 부른다** — 만들어 두고 안 부르면 없는 것과 같다. + # 2026-09-08 ㉘ 자기 감사: 아래 줄 하나만 이어져 있고 형제 둘은 **시험에서만** 불리고 + # 있었다. B09 에서 같은 병(가드 둘이 놀고 있음)을 지적해 놓고 내 쪽도 같았다. result["ratio_math_warnings"] = verify_ratio_math(result) + result["material_code_warnings"] = verify_no_code_on_materials(result) + result["bill_flag_warnings"] = verify_bill_flags(result) return result diff --git a/B08_Quantity/B08_Quantity_Router_Earthwork.py b/B08_Quantity/B08_Quantity_Router_Earthwork.py index 23373193..bcae977f 100644 --- a/B08_Quantity/B08_Quantity_Router_Earthwork.py +++ b/B08_Quantity/B08_Quantity_Router_Earthwork.py @@ -32,6 +32,7 @@ from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table from B08_Quantity.B08_Quantity_Engine_EarthworkTable import StationArea, build_table from B08_Quantity.B08_Quantity_Engine_HaulSummary import build_table as build_haul_table +from B08_Quantity.B08_Quantity_Engine_HaulSummary import check_against_plan from B08_Quantity.B08_Quantity_Engine_Handoff import load_mapping from B08_Quantity.B08_Quantity_Engine_Preparation import build_table as build_preparation_table from B08_Quantity.B08_Quantity_Engine_HaulSummary import summary_input_rows @@ -85,6 +86,17 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse: table["haul"] = haul # 운반계획은 [저장]·[확정]에서 정본에 남는 값이다 — 아직 없으면 빈 표가 정직하다. table["haul_available"] = bool(plan) + # ⚠ 검산을 **실제로 부른다** — 무대·도자·덤프 합이 운반계획 총량과 맞는가(8-7 ㉡). + # 2026-09-08 ㉘ 자기 감사: 만들어 두고 시험에서만 부르고 있었다. 값을 막지는 않고 + # 차이만 실어 화면이 띄우게 한다 — 막으면 계획이 없는 정상 상태에서도 멈춘다. + if plan: + check = check_against_plan(haul, plan) + table["haul_check"] = { + "hauled_total_m3": check.hauled_total_m3, + "plan_total_m3": check.plan_total_m3, + "difference_m3": check.difference_m3, + "by_equipment": check.details, + } table["summary"] = build_summary_table( SummaryInput( @@ -189,6 +201,11 @@ class QuantitySettingsBody(BaseModel): topsoil_thickness_m: float | None = None +#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**. +#: 빈 문자열로 되돌리는 칸(시공법·타설 방식)과 달리 숫자 칸은 되돌릴 값이 `None` 뿐이다. +NULLABLE_SETTING_KEYS = ("topsoil_thickness_m",) + + @router.put("/{project_id}/quantity/settings") async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) -> JSONResponse: """산출 조건을 정본에 남긴다 — [저장]이 부르는 자리. @@ -206,6 +223,12 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - content={"status": "error", "message": "프로젝트 저장 폴더를 찾지 못했습니다."}, ) values = {key: value for key, value in body.model_dump().items() if value is not None} + # ⚠ `None` 을 통째로 버리면 **「안 정함」으로 되돌릴 길이 없다** — 한 번 넣은 값이 + # 영영 남는다(2026-09-08 ㉘ 자기 감사). 시공법·타설 방식은 빈 문자열로 되돌리지만 + # 숫자 칸은 되돌리는 값이 `None` 뿐이라, **화면이 보낸 것**만 골라 살린다. + for key in NULLABLE_SETTING_KEYS: + if key in body.model_fields_set: + values[key] = getattr(body, key) if "concrete_placing_method" in values: method = values["concrete_placing_method"] # 「안 정함」으로 되돌릴 수 있어야 한다 — 빈 값이면 지운다(8-22 ② 와 같은 자리). @@ -225,7 +248,7 @@ async def save_quantity_settings(project_id: UUID, body: QuantitySettingsBody) - _save_quantity, root, values, - ("rock_methods", "material_supply", "concrete_placing_method"), + ("rock_methods", "material_supply", "concrete_placing_method") + NULLABLE_SETTING_KEYS, ) except Exception: logger.exception("B08 설정 저장 실패(쓰기): project_id=%s", project_id) diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index 9825c0e7..be3184cc 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -65,6 +65,8 @@ export interface QuantitySettings { material_supply?: Record; /** 콘크리트 타설 방식. `null`·없음이면 **아직 안 정한 것**이고 화면이 기본값 안내를 띄운다. */ concrete_placing_method?: string | null; + /** 표토 두께(m). `null`·없음이면 **안 정한 것**이라 표토제거 줄이 「근거 없음」으로 선다. */ + topsoil_thickness_m?: number | null; } export interface EarthworkTable { diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index 2e0413c6..2b3099d9 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -84,6 +84,8 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr rock_methods: draft.rock_methods, material_supply: draft.material_supply, concrete_placing_method: draft.concrete_placing_method, + // ⚠ `null` 도 그대로 보낸다 — 「안 정함」으로 되돌릴 길이 있어야 한다(시공법과 같은 규칙). + topsoil_thickness_m: draft.topsoil_thickness_m, }), }, ); @@ -134,6 +136,37 @@ function numberField(label: string, value: number, onInput: (value: number) => v return row; } +/** 비워 둘 수 있는 숫자 칸 — **빈 값은 「안 정함」**이고 0 과 다르다. + * + * ⚠ `numberField` 로 두면 빈 칸이 0 으로 읽혀 「두께 0m」와 「안 정함」이 같아진다. + * 표토제거처럼 **안 정하면 줄이 아예 안 서는** 값은 그 둘을 갈라야 한다. + */ +function optionalNumberField( + label: string, + value: number | null, + step: string, + onInput: (value: number | null) => void, +): HTMLElement { + const row = document.createElement("label"); + row.className = "b08-quantity__field"; + const name = document.createElement("span"); + name.textContent = label; + const input = document.createElement("input"); + input.type = "number"; + input.className = "b08-quantity__input"; + input.min = "0"; + input.step = step; + input.placeholder = L("B08_Quantity_Unset_Placeholder"); + input.value = value === null || value === undefined ? "" : String(value); + // 자동저장은 만들지 않는다 — 입력은 캐시에만 남는다(CLAUDE.md 5장). + input.addEventListener("input", () => { + const text = input.value.trim(); + onInput(text === "" ? null : Number(text)); + }); + row.append(name, input); + return row; +} + /** 타설 방식 표기 — 코드가 아니라 사람이 읽는 이름으로 보인다. */ const PLACING_LABELS: Record = { ready_mixed: "레디믹스트", @@ -182,6 +215,8 @@ interface DraftSettings { rock_methods: Record; // 콘크리트 타설 방식 — `""` 는 「안 정함」이고 저장에서 지워진다. concrete_placing_method: string; + // 표토 두께(m) — `null` 은 「안 정함」. 정해야 표토제거 줄이 선다(품셈 9-15 [주]② 의 T). + topsoil_thickness_m: number | null; // 자재별 관급/사급 — 표 안에서 줄마다 고른 값. material_supply: Record; dirty: boolean; @@ -265,6 +300,22 @@ function buildQuantitySidePanel( } } + // ── 표토 두께 — 정해야 준비공 표토제거 줄이 선다(품셈 9-15 [주]② 의 「T : 표토두께(m)」) ── + // ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.** + // 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리. + panel.append(field(L("B08_Quantity_Side_Topsoil"), "")); + panel.append( + optionalNumberField( + L("B08_Quantity_Side_Topsoil_Label"), + draft.topsoil_thickness_m, + "0.01", + (value) => { + draft.topsoil_thickness_m = value; + draft.dirty = true; + }, + ), + ); + // ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ── panel.append(field(L("B08_Quantity_Side_Placing"), "")); panel.append( @@ -503,6 +554,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { application_ratios_pct: { ...(stored.application_ratios_pct ?? {}) }, rock_methods: { ...((stored.rock_methods ?? {}) as Record) }, concrete_placing_method: (stored.concrete_placing_method as string) ?? "", + topsoil_thickness_m: (stored.topsoil_thickness_m as number | null) ?? null, material_supply: { ...((stored.material_supply ?? {}) as Record) }, dirty: false, }; diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 4f4ce8ee..35671a42 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -628,6 +628,9 @@ export const ui_locales_b2 = { B08_Quantity_Method_Unset: ["안 정함", "Not set"], B08_Quantity_Method_Ripping: ["긁어내기(암절취)", "Ripping"], B08_Quantity_Method_Blasting: ["터뜨리기(발파암)", "Blasting"], + B08_Quantity_Side_Topsoil: ["표토제거", "Topsoil Removal"], + B08_Quantity_Side_Topsoil_Label: ["표토 두께(m)", "Topsoil thickness (m)"], + B08_Quantity_Unset_Placeholder: ["안 정함", "Not set"], B08_Quantity_Side_Placing: ["콘크리트 타설", "Concrete Placing"], B08_Quantity_Side_Placing_Label: ["타설 방식", "Method"], B08_Quantity_Placing_Unset: ["안 정함(기본값 사용)", "Not set (default)"],