From f569cb84985e301c1a95b0e413fccf802653f28c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 12:57:34 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=ED=99=94=EB=A9=B4=20=EC=8B=A4?= =?UTF-8?q?=EC=B8=A1=EC=9C=BC=EB=A1=9C=20=EC=9E=A1=EC=9D=80=20=EC=82=AC?= =?UTF-8?q?=ED=86=A0=EC=9E=A5=20=ED=9D=A0=20=EB=91=98=20=E2=80=94=20?= =?UTF-8?q?=EC=A7=80=EC=9A=B4=20=EB=92=A4=20=EB=82=A8=EB=8D=98=20=EA=B0=92?= =?UTF-8?q?=20=C2=B7=20=EA=B0=80=EC=A7=9C=20=EA=B2=BD=EA=B3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚠ **시험만으로는 안 잡히던 자리임.** 실제 프로젝트에 사토장을 놓고 눈으로 본 뒤 잡음. ① **사토장을 지워도 값이 남음** — `enforce_spoil_fills` 가 **얹기만** 해서, 구조물을 지운 뒤에도 저장분의 `spoil_fill_*` 가 그대로 남아 횡단도에 계속 그려지고 면적표에도 섰음. ⇒ 사토장이 덮지 않는 측점에 **값이 남아 있으면** 사토장 없이 다시 계산해 지움. 노선 성토도 그때 원래 값으로 돌아옴(사토장 몫을 빼 두었던 것이 복구됨). ⚠ **칸이 있는 것과 값이 있는 것은 다름** — 설계 결과는 사토장이 없어도 `0.0` 을 늘 실으므로 「값이 남은」 측점만 되돌림(`_has_spoil_value`) ② **말풍선에 늘 뜨던 가짜 경고** — 「⚠ 0.0㎥ 는 못 담음」. 폭을 이분법으로 찾아 400 − 399.9937 = 0.0063 이 남는 것이라, 표시 자릿수에서 안 보이는 몫은 경고 안 함 실측(프로젝트 fa76c162) — 지운 뒤 사토장 0 · 용량 null · 노선 성토가 정확히 「지우기 전 + 옮겨 갔던 몫」으로 복구됨(100m 1.7563+4.9217=6.678 …). Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Engine_SpoilFill.py | 54 +++++++++++++++++-- B06_Section/B06_Section_UI_Cross_SpoilFill.ts | 6 ++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index 5437b3a9..f4e6bde5 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -65,6 +65,17 @@ def _spans(sections: list[dict[str, Any]], start_m: float, end_m: float) -> list return spans +def _has_spoil_value(design: dict[str, Any]) -> bool: + """그 측점에 **사토장 값이 실제로 남아 있나**. 칸이 0 으로 있는 것은 값이 아니다.""" + for key in ("spoil_fill_area_m2", "spoil_fill_width_m", "spoil_fill_capacity_m3"): + try: + if float(design.get(key) or 0.0) > 0: + return True + except (TypeError, ValueError): + continue + return bool(design.get("spoil_fill_structure_id")) + + def _side_of(design: dict[str, Any], option: Any) -> str | None: """쌓는 쪽 — 「좌·우」면 그대로, 「자동」이면 그 측점의 **성토 쪽**.""" word = str(option or "").strip() @@ -169,10 +180,8 @@ def enforce_spoil_fills( try: _revision, structures = load_structures(str(project_root)) except Exception: # noqa: BLE001 — 정본이 없으면 사토장도 없다 - return 0 + structures = [] sites = spoil_sites(structures) - if not sites: - return 0 def recompute(section, side, width_m, slope_ratio_n, longitudinal_data, standard_spec): design = section.get("design") @@ -206,6 +215,45 @@ def enforce_spoil_fills( return None changed = 0 + # ⚠⚠ **지운 사토장이 그림·수량에 남던 자리**(2026-09-09 화면 실측으로 잡음). + # `enforce_spoil_fills` 는 **얹기만** 했으므로 구조물을 지워도 저장분의 `spoil_fill_*` + # 가 그대로 남아 **횡단도에 계속 그려지고 면적표에도 섰다.** + # ⇒ 사토장이 덮지 않는 측점에 값이 남아 있으면 **사토장 없이 다시 계산**해 지운다. + # (노선 성토도 그때 원래 값으로 돌아온다 — 사토장 몫을 빼 두었기 때문이다.) + covered: set[float] = set() + for site in sites: + for section in _sections_in(cross_sections, site["start_m"], site["end_m"]): + covered.add(float(section["chainage_m"])) + for section in cross_sections: + design = section.get("design") + if not isinstance(design, dict): + continue + chainage = section.get("chainage_m") + if chainage is None or float(chainage) in covered: + continue + # ⚠ **칸이 있는 것과 값이 있는 것은 다르다** — 설계 결과는 사토장이 없어도 + # `spoil_fill_area_m2: 0.0` 을 늘 싣는다. 「값이 남아 있는」 측점만 되돌린다. + if not _has_spoil_value(design): + continue + side = _side_of(design, None) + plain = recompute(section, side, 0.0, None, longitudinal, standard) if side else None + if plain is None: + # 다시 계산할 수 없으면 **적어도 칸은 지운다** — 값이 남아 그려지는 것보다 낫다. + for key in [k for k in design if str(k).startswith("spoil_fill_")]: + design.pop(key, None) + # 노선 성토는 사토장 몫을 뺀 값이라, 다시 계산 못 하면 그 몫을 되돌려 준다. + moved = float(design.get("spoil_fill_replaced_fill_m2") or 0.0) + if moved > 0: + design["fill_area_m2"] = round(float(design.get("fill_area_m2") or 0.0) + moved, 4) + changed += 1 + continue + for key in ("status", "pavement_suggested", *USER_TOUCHED_KEYS): + if design.get(key) is not None: + plain[key] = design[key] + section["design"] = plain + changed += 1 + if not sites: + return changed for site in sites: sections = _sections_in(cross_sections, site["start_m"], site["end_m"]) if not sections: diff --git a/B06_Section/B06_Section_UI_Cross_SpoilFill.ts b/B06_Section/B06_Section_UI_Cross_SpoilFill.ts index ae88da48..c62db5d8 100644 --- a/B06_Section/B06_Section_UI_Cross_SpoilFill.ts +++ b/B06_Section/B06_Section_UI_Cross_SpoilFill.ts @@ -36,7 +36,11 @@ export function spoilFillTooltip(design: SpoilFillDrawing): string { const placed = Number(design.spoil_fill_placed_m3 ?? 0); lines.push(`구간 용량 ${capacity.toFixed(1)}㎥ 중 ${placed.toFixed(1)}㎥ 담김`); const unplaced = Number(design.spoil_fill_unplaced_m3 ?? 0); - if (unplaced > 0) { + // ⚠ **적어 보이는 0 을 경고로 띄우지 않는다**(2026-09-09 화면 실측). 폭을 이분법으로 + // 찾으므로 용량과 담긴 양이 소수점 아래에서 조금 남는다(400 − 399.9937 = 0.0063). + // 그것을 「못 담음」으로 띄우면 **늘 경고가 뜬 채**가 되어 진짜 경고가 안 보인다. + // 표시 자릿수(0.1㎥)에서 보이지 않는 몫은 없는 것으로 본다. + if (unplaced >= 0.05) { lines.push(`⚠ ${unplaced.toFixed(1)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`); } }