랩탑 창이 측점 design 에 pipe_length_m 을 넣어 줘서 마지막 조각이 채워짐.
B08 은 잇기만 하고 길이를 짓지 않음.
값이 어디서 오나
관 자체(있나·어디·관경·관종) → pipe_points.json (레지스트리 managed_by)
관 연장(m) → 측점 design.pipe_length_m
(B06 이 서버 Node 로 m 단위 올림까지 끝낸 값)
관종 → 공종코드 → work_item_mapping 의 pipe.kind_codes
파형강관 FP-12-11-03 · 흄관 FP-12-11-02 · VR관 FP-12-11-01
⚠ facility 가 pipe 인 점만 배관 — pipe_points.json 은 계곡 통과 시설 전부의
정본이라 BOX암거·물넘이·세월교가 같은 파일에 있음. 관경 유무로 가르면
관경 미지정 관을 놓침. 실측: 5601e828 11점 중 관 9 · 세월교 2
⚠ 관종 기본값(파형강관)은 2026-08-17 사용자 확정값이라 근거가 있으나
조용히 쓰지 않고 「기본값으로 섰습니다」를 알림에 실음
⚠ 안 붙인 것 둘 — 터파기·되메우기(관 부설과 각각 오면 같은 굴착을 두 번 셈,
B09 ㉡ 가드 자리) · 유출입부 기슭막이(관 옵션이 정본이고 구조물에서 빠졌음)
연장이 없는 관은 0 으로 때우지 않고 blocked_reason 과 함께 감
「횡단설계에서 [저장]을 한 번 누르면 그 측점의 관 길이가 남고 값이 섭니다」
시험 10건 — 세월교 걸러짐 · 관경 없어도 관임 · 연장 없으면 안 섬 ·
관종 셋이 갈림 · 기본값을 알림 · 모르는 관종은 못 고름 ·
⚠ 좁게: 옆 측점 길이를 물어 오지 않음 · 실제 자료 정본 대조
시험: 697 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
6.6 KiB
Python
149 lines
6.6 KiB
Python
"""배수관 물량 — **정본 셋을 잇기만 한다** (2026-09-08 두 창 합의).
|
|
|
|
값이 어디서 오나
|
|
관 자체(있나·어디·관경·관종) → `pipe_points.json` (레지스트리 `pipe` 타입이
|
|
`managed_by: pipe_points`)
|
|
관 연장(m) → 측점 `design.pipe_length_m`
|
|
(B06 횡단이 **서버 Node 로** 계산해 m 단위 올림까지
|
|
끝낸 값을 정본에 남긴다 — 계산이 두 벌이 아니다)
|
|
관종 → 공종코드 → `work_item_mapping` 의 `pipe.kind_codes`
|
|
|
|
⚠ **여기서 길이를 짓지 않는다.** 앞서 「도로폭 = 관 길이」처럼 잡을 뻔했는데 그것이 곧
|
|
임의 수치다. 연장이 없는 관은 **줄을 세우되 막힌 사유와 함께** 보낸다.
|
|
|
|
⚠ **`facility` 가 `pipe` 인 점만 배관이다.**
|
|
`pipe_points.json` 은 **계곡 통과 시설 전부의 정본**이라 BOX암거·물넘이·세월교·독립
|
|
기슭막이가 같은 파일에 있다. `pipe_diameter_mm` 유무로 가르면 **관경 미지정 관을 놓친다**
|
|
(실측: `5601e828` 11점 중 2점이 `facility: ford_bridge`).
|
|
|
|
⚠ **유출·유입부 기슭막이는 여기서 세지 않는다.**
|
|
관 옵션(`outlet_revet_*`)이 정본이고 구조물 목록에서는 빠졌다(2026-08-28 이관).
|
|
구조물 쪽으로 또 세면 이중계상이다.
|
|
|
|
⚠ **터파기·되메우기를 관 줄에 붙이지 않는다.**
|
|
관 부설과 굴착이 각각 오면 **같은 굴착을 두 번** 센다(B09 ㉡ 가드와 같은 자리).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
#: 배관으로 보는 `facility` 값. 그 밖(BOX암거·물넘이·세월교·독립 기슭막이)은 관이 아니다.
|
|
FACILITY_PIPE = "pipe"
|
|
|
|
#: 연장을 못 찾은 줄의 막힌 갈래 — 「입력하면 풀림」이 아니라 **앞 단계가 내야 하는 값**이다.
|
|
BLOCKED_LENGTH_MISSING = "input_missing"
|
|
|
|
NOTE_LENGTH_MISSING = (
|
|
"관 연장이 아직 정본에 없습니다 — 횡단설계에서 [저장]을 한 번 누르면 "
|
|
"그 측점의 관 길이가 남고 값이 섭니다"
|
|
)
|
|
NOTE_KIND_DEFAULT = "관종을 안 정해 기본값({kind})으로 섰습니다 — 정하면 공종이 갈립니다"
|
|
NOTE_KIND_UNKNOWN = "「{kind}」은(는) 아는 관종이 아니라 공종을 못 골랐습니다"
|
|
|
|
|
|
def _length_by_chainage(designs: list[dict[str, Any]], key: str) -> dict[float, float]:
|
|
"""측점별 관 길이. **없는 측점은 담지 않는다** — 0 으로 채우면 「없음」과 구별이 안 된다."""
|
|
found: dict[float, float] = {}
|
|
for row in designs or []:
|
|
design = row.get("design") if isinstance(row, dict) else None
|
|
if not isinstance(design, dict):
|
|
continue
|
|
value = design.get(key)
|
|
if value is None:
|
|
continue
|
|
try:
|
|
length = float(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if length > 0:
|
|
found[round(float(row.get("chainage_m") or 0.0), 3)] = length
|
|
return found
|
|
|
|
|
|
def _nearest(lengths: dict[float, float], chainage: float, tolerance: float = 0.5) -> float | None:
|
|
"""관 측점과 단면 측점이 소수점에서 어긋날 수 있어 **가까운 것**을 본다.
|
|
|
|
⚠ 좁게 본다(기본 0.5m) — 넓히면 옆 측점의 길이를 물어 와 조용히 틀린다.
|
|
"""
|
|
if not lengths:
|
|
return None
|
|
key = round(chainage, 3)
|
|
if key in lengths:
|
|
return lengths[key]
|
|
best = min(lengths, key=lambda x: abs(x - chainage))
|
|
return lengths[best] if abs(best - chainage) <= tolerance else None
|
|
|
|
|
|
def build_rows(
|
|
pipe_points: list[dict[str, Any]],
|
|
designs: list[dict[str, Any]],
|
|
mapping: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""관 줄 목록. **값이 서는 줄도, 못 서는 줄도** 함께 낸다.
|
|
|
|
`pipe_points` 는 `PipePoint.model_dump()` 또는 같은 모양의 딕셔너리 목록이다.
|
|
"""
|
|
table = mapping or {}
|
|
kind_codes: dict[str, str] = table.get("kind_codes") or {}
|
|
kind_key = str(table.get("kind_option_key") or "pipe_kind")
|
|
default_kind = str(table.get("default_kind") or "")
|
|
length_key = str(table.get("length_key") or "pipe_length_m")
|
|
diameter_key = str(table.get("diameter_option_key") or "pipe_diameter_mm")
|
|
|
|
lengths = _length_by_chainage(designs, length_key)
|
|
rows: list[dict[str, Any]] = []
|
|
notes: list[str] = []
|
|
|
|
for point in pipe_points or []:
|
|
if str(point.get("facility") or FACILITY_PIPE) != FACILITY_PIPE:
|
|
continue # 배관이 아닌 시설 — 그쪽 줄은 그쪽이 센다
|
|
options = point.get("options") or {}
|
|
chainage = float(point.get("chainage_m") or 0.0)
|
|
|
|
stored_kind = str(options.get(kind_key) or "").strip()
|
|
kind = stored_kind or default_kind
|
|
code = kind_codes.get(kind)
|
|
kind_note = ""
|
|
if not stored_kind and default_kind:
|
|
kind_note = NOTE_KIND_DEFAULT.format(kind=default_kind)
|
|
elif stored_kind and code is None:
|
|
kind_note = NOTE_KIND_UNKNOWN.format(kind=stored_kind)
|
|
|
|
length = _nearest(lengths, chainage)
|
|
blocked_kind = None if length else BLOCKED_LENGTH_MISSING
|
|
blocked_reason = "" if length else NOTE_LENGTH_MISSING
|
|
if code is None:
|
|
blocked_kind = blocked_kind or BLOCKED_LENGTH_MISSING
|
|
blocked_reason = blocked_reason or kind_note
|
|
|
|
rows.append(
|
|
{
|
|
"chainage_m": chainage,
|
|
"work_item_code": code,
|
|
"kind": kind,
|
|
"kind_from_default": not stored_kind,
|
|
# 갈래는 **저장 원본값**만 보낸다 — 「…㎜ 이하」 구간 나누기는 원문을 읽는 쪽 몫.
|
|
"variant_axis": diameter_key,
|
|
"variant_value": options.get(diameter_key),
|
|
"unit": str(table.get("unit") or "m"),
|
|
"quantity": float(length or 0.0),
|
|
"blocked_kind": blocked_kind,
|
|
"blocked_reason": blocked_reason or kind_note,
|
|
"in_bill": bool(length and code),
|
|
}
|
|
)
|
|
if kind_note and kind_note not in notes:
|
|
notes.append(kind_note)
|
|
|
|
missing = sum(1 for row in rows if not row["in_bill"])
|
|
if missing:
|
|
notes.append(f"관 {len(rows)}개 중 {missing}개가 아직 값이 안 섭니다")
|
|
return {
|
|
"rows": rows,
|
|
"notes": notes,
|
|
"pipe_count": len(rows),
|
|
"ready_count": sum(1 for row in rows if row["in_bill"]),
|
|
"length_total_m": round(sum(row["quantity"] for row in rows if row["in_bill"]), 3),
|
|
}
|