fix(B06): 화면 실측으로 잡은 사토장 흠 둘 — 지운 뒤 남던 값 · 가짜 경고

⚠ **시험만으로는 안 잡히던 자리임.** 실제 프로젝트에 사토장을 놓고 눈으로 본 뒤 잡음.

① **사토장을 지워도 값이 남음** — `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) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 12:57:34 +09:00
co-authored by Claude Opus 5
parent 2972e4ccce
commit f569cb8498
2 changed files with 56 additions and 4 deletions
+51 -3
View File
@@ -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:
@@ -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)}㎥ 는 못 담음 — 지반 자료가 있는 데까지만 넓힘`);
}
}