fix(B08): 관 줄의 막힌 사유를 둘로 가름 — 「저장하면 풀림」과 「눌러도 안 풀림」

랩탑 [저장] 뒤 실측하니 관 9개 중 6개 값이 서고(59.0m) 3개가 막힘.
그 셋에 「[저장]을 한 번 누르면 섭니다」가 떴는데 눌러도 안 풀리는 자리였음
— cross_sections 에 그 관 자리 측점이 아예 없음(관은 놓였는데 횡단이 안 만들어짐).

- 측점 목록을 함께 받아 「횡단이 있는데 길이만 없음」과
  「관 자리에 횡단이 없음」을 갈라 말함
- ⚠ 관 자리 판정을 아주 좁게(0.05m) — 실측에서 관 439.55 근처에 측점 440.0 만
  있었고, 길이 찾기와 같은 0.5m 로 보면 「횡단이 있다」로 읽혀 거짓 안내가 떴음.
  길이는 B06 이 관이 놓인 측점에만 실으므로 뜻이 다름

실측(5601e828): 값 6줄 59.0m · 막힘 3줄이 정확히 그 셋
  85.05→10.0 · 258.12→11.0 · 534.49→9.0 · 804.30→9.0 · 901.24→10.0 · 984.18→10.0

⚠ npm run build:server-calc 돌려 새 번들로 바꿨음 — 옛 번들은 이웃 측점에도
길이를 실어 같은 관을 두 번 셀 수 있었음(랩탑이 고쳐 push 한 건).

시험 4건 추가(가른 두 문구 · 목록 없으면 종전대로 · ⚠ 옆 측점은 「있음」이 아님).
시험: 701 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 09:40:51 +09:00
co-authored by Claude Opus 5
parent 50c0d234e9
commit 1ad40fa2fb
3 changed files with 43 additions and 4 deletions
+25 -1
View File
@@ -34,10 +34,21 @@ FACILITY_PIPE = "pipe"
#: 연장을 못 찾은 줄의 막힌 갈래 — 「입력하면 풀림」이 아니라 **앞 단계가 내야 하는 값**이다.
BLOCKED_LENGTH_MISSING = "input_missing"
#: ⚠ **둘을 갈라 말한다** — 「[저장]을 누르면 풀리는 것」과 「눌러도 안 풀리는 것」은
#: 사용자가 할 일이 다르다. 뒤엣것에 앞 문구를 쓰면 눌러 보고 안 되어 헤맨다
#: (2026-09-08 실측: 관 9개 중 3개가 **횡단 행 자체가 없는** 자리였다).
NOTE_LENGTH_MISSING = (
"관 연장이 아직 정본에 없습니다 — 횡단설계에서 [저장]을 한 번 누르면 "
"그 측점의 관 길이가 남고 값이 섭니다"
)
NOTE_SECTION_MISSING = (
"그 측점의 횡단 자체가 없습니다 — 관은 놓였는데 횡단이 안 만들어진 자리라 "
"[저장]으로는 안 풀립니다. 횡단설계에서 그 측점이 서야 합니다"
)
#: 관 자리에 횡단이 있는지 볼 때의 허용 오차. **아주 좁게** — 옆 측점을 「있다」로 세면
#: 거짓 안내가 된다. 길이 찾기(0.5m)보다 좁은 것은 뜻이 다르기 때문이다.
SECTION_MATCH_TOLERANCE_M = 0.05
NOTE_KIND_DEFAULT = "관종을 안 정해 기본값({kind})으로 섰습니다 — 정하면 공종이 갈립니다"
NOTE_KIND_UNKNOWN = "{kind}」은(는) 아는 관종이 아니라 공종을 못 골랐습니다"
@@ -79,6 +90,7 @@ def build_rows(
pipe_points: list[dict[str, Any]],
designs: list[dict[str, Any]],
mapping: dict[str, Any] | None = None,
section_chainages: list[float] | None = None,
) -> dict[str, Any]:
"""관 줄 목록. **값이 서는 줄도, 못 서는 줄도** 함께 낸다.
@@ -92,6 +104,9 @@ def build_rows(
diameter_key = str(table.get("diameter_option_key") or "pipe_diameter_mm")
lengths = _length_by_chainage(designs, length_key)
# 횡단이 **있는데 길이가 없는 것**과 **관 자리에 횡단이 없는 것**을 가르기 위한 목록.
# 안 주면 종전대로 「[저장]하면 풀림」 하나로만 말한다.
sections = [float(x) for x in (section_chainages or [])]
rows: list[dict[str, Any]] = []
notes: list[str] = []
@@ -111,8 +126,17 @@ def build_rows(
kind_note = NOTE_KIND_UNKNOWN.format(kind=stored_kind)
length = _nearest(lengths, chainage)
# ⚠ **관이 놓인 그 측점**이 있는지를 본다 — 옆 측점이 있는 것은 소용없다.
# 실측(2026-09-08 `5601e828`): 관 439.55 근처에 측점 440.0 만 있었고, 0.5m 로
# 느슨히 보면 「횡단이 있다」로 읽혀 **「[저장]하면 풀린다」는 거짓 안내**가 떴다.
# 길이는 B06 이 **관이 놓인 측점에만** 싣는다(2026-09-08 이웃 오염을 고친 뒤).
has_section = not sections or any(
abs(x - chainage) <= SECTION_MATCH_TOLERANCE_M for x in sections
)
blocked_kind = None if length else BLOCKED_LENGTH_MISSING
blocked_reason = "" if length else NOTE_LENGTH_MISSING
blocked_reason = ""
if not length:
blocked_reason = NOTE_LENGTH_MISSING if has_section else NOTE_SECTION_MISSING
if code is None:
blocked_kind = blocked_kind or BLOCKED_LENGTH_MISSING
blocked_reason = blocked_reason or kind_note
@@ -93,6 +93,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
for row in designs
if isinstance(row, dict) and (row.get("design") or {}).get("pipe_length_m") is not None
]
# 횡단이 선 측점 목록 — 관 줄이 「길이가 없음」과 「횡단 자체가 없음」을 가르는 데 쓴다.
table["section_chainages"] = [row.get("chainage_m") for row in designs if isinstance(row, dict)]
table["haul"] = haul
# 운반계획은 [저장]·[확정]에서 정본에 남는 값이다 — 아직 없으면 빈 표가 정직하다.
table["haul_available"] = bool(plan)
+16 -3
View File
@@ -164,7 +164,11 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
# 준비공·사방공 — 값이 서는 줄도, 못 내는 줄도 함께 넘긴다(빼면 빠진 줄이 안 보임).
preparation_table=earthwork.get("preparation"),
# 배수관 — 관 정본은 `pipe_points.json`, 연장은 측점 `design.pipe_length_m` 다.
pipe_table=_pipe_table(project_root, earthwork.get("pipe_lengths") or []),
pipe_table=_pipe_table(
project_root,
earthwork.get("pipe_lengths") or [],
earthwork.get("section_chainages") or [],
),
ground_class_set=settings.get("rock_class_set"),
ground_classes=rock_classes(settings),
ground_methods={name: rock_method(settings, name) for name in rock_classes(settings)},
@@ -177,7 +181,11 @@ async def get_handoff(project_id: UUID) -> JSONResponse:
return JSONResponse(content=handoff)
def _pipe_table(project_root: str, pipe_lengths: list[dict[str, Any]]) -> dict[str, Any]:
def _pipe_table(
project_root: str,
pipe_lengths: list[dict[str, Any]],
section_chainages: list[Any] | None = None,
) -> dict[str, Any]:
"""배수관 표 — 정본 셋을 읽어 잇는다. 못 읽으면 **빈 표**(줄이 안 서는 것이 정직하다).
⚠ 관 정본은 `structures.json` 이 아니라 `pipe_points.json` 이다
@@ -206,7 +214,12 @@ def _pipe_table(project_root: str, pipe_lengths: list[dict[str, Any]]) -> dict[s
{"chainage_m": row.get("chainage_m"), "design": {"pipe_length_m": row.get("pipe_length_m")}}
for row in pipe_lengths
]
return build_pipe_rows(points, designs, (load_mapping().pipe or {}))
return build_pipe_rows(
points,
designs,
(load_mapping().pipe or {}),
[float(x) for x in (section_chainages or []) if x is not None],
)
async def _earthwork_tables(project_id: UUID) -> dict[str, Any]: