- 원인: 화면 attach · 서버 carry_facility_attributes 가 거리 제한 없이 가장 가까운 관에 속성을 붙여, 관 수 > 속성 수이거나 관을 이웃 쪽으로 옮기면 BOX · 세월교가 옆 관 측점으로 넘어감 - 고침: 화면 carryIndices ↔ 서버 carry_indices 짝 — 허용 오차 0.05m 안 가장 가까운 원본만 · 한 원본 한 번 · 못 붙인 관은 속성 없음(추천은 기존대로 빈칸에만) · 두 파일 머리에 짝 표기 - 관을 옮기면 속성이 따라감: 지도 끌기(onMove → move) · 되돌리기 · 구조물 폼(follow — 사라진 관 하나 · 새 관 하나일 때) - B06 옛 「관 이동 예약」(culvertmove): 저장 때 읽지도 적용하지도 않음 — flushCulvertOptions · createCulvertOptionWriter 에서 이동 키 제거 - 시험: 거울 시험 test_b05_facility_carry(화면 · 서버 같은 답 · 끌기 · 되돌리기) · 옛 최근접 · 이동 예약 적용 시험을 새 규칙으로 고침 - 확인: ORCA 5180 랩탑_보조 — 세월교(173.09) 8m 앞 165m 에 옵션 없는 관 추가 → 저장 → pipe_points.json 165 = 속성 없음 · 173.09 = ford_bridge · 나머지 11건 전후 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RrQ65VtGZbae2VKgYVMhmc
215 lines
8.3 KiB
Python
215 lines
8.3 KiB
Python
"""계곡 통과 시설 확장 — PipePoint의 시설종류·구간·옵션 (PLAN 2026-08-17 컨테이너 병합 1단계).
|
|
|
|
배관 정본 `pipe_points.json`은 유역 계산의 입력이라 저장소를 옮기지 않고 제자리 확장한다.
|
|
구 파일(`{chainage_m, source}`뿐)은 배관·폭 미지정으로 읽혀야 한다 (하위 호환).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from common_util.common_util_drainage_pipes import (
|
|
PIPE_FACILITY_BOX,
|
|
PIPE_FACILITY_FORD_BRIDGE,
|
|
PIPE_FACILITY_FORD_PAVEMENT,
|
|
PIPE_FACILITY_PIPE,
|
|
PIPE_SOURCE_STREAM,
|
|
PIPE_SOURCE_USER,
|
|
PipePoint,
|
|
carry_facility_attributes,
|
|
parse_pipe_points,
|
|
)
|
|
|
|
|
|
# ── 하위 호환 — 구 파일 형식 ────────────────────────────────────────────────
|
|
|
|
|
|
def test_legacy_dict_defaults_to_pipe_with_no_span():
|
|
points = parse_pipe_points([{"chainage_m": 120.0, "source": "stream"}])
|
|
assert len(points) == 1
|
|
point = points[0]
|
|
assert point.facility == PIPE_FACILITY_PIPE
|
|
assert point.start_m is None and point.end_m is None
|
|
assert point.source == PIPE_SOURCE_STREAM
|
|
|
|
|
|
def test_legacy_bare_number_still_parses():
|
|
points = parse_pipe_points([80.5])
|
|
assert points[0].chainage_m == 80.5
|
|
assert points[0].facility == PIPE_FACILITY_PIPE
|
|
|
|
|
|
def test_legacy_dict_roundtrip_stays_compact():
|
|
"""구 형식 저장분은 확장 필드 없이 그대로 다시 저장된다 (파일 불변)."""
|
|
point = parse_pipe_points([{"chainage_m": 60.0, "source": "user"}])[0]
|
|
assert point.as_dict() == {"chainage_m": 60.0, "source": PIPE_SOURCE_USER}
|
|
|
|
|
|
# ── 시설 종류 ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_facility_roundtrip():
|
|
for facility in (
|
|
PIPE_FACILITY_PIPE,
|
|
PIPE_FACILITY_BOX,
|
|
PIPE_FACILITY_FORD_PAVEMENT,
|
|
PIPE_FACILITY_FORD_BRIDGE,
|
|
):
|
|
data = {"chainage_m": 100.0, "facility": facility}
|
|
parsed = parse_pipe_points([data])[0]
|
|
assert parsed.facility == facility
|
|
assert parse_pipe_points([parsed.as_dict()])[0].facility == facility
|
|
|
|
|
|
def test_unknown_facility_falls_back_to_pipe():
|
|
points = parse_pipe_points([{"chainage_m": 100.0, "facility": "bridge"}])
|
|
assert points[0].facility == PIPE_FACILITY_PIPE
|
|
|
|
|
|
# ── 구간 (기준점 + 시작·종료) ──────────────────────────────────────────────
|
|
|
|
|
|
def test_span_roundtrip():
|
|
data = {"chainage_m": 100.0, "facility": "box_culvert", "start_m": 92.0, "end_m": 111.0}
|
|
point = parse_pipe_points([data])[0]
|
|
assert (point.start_m, point.end_m) == (92.0, 111.0)
|
|
again = parse_pipe_points([point.as_dict()])[0]
|
|
assert (again.start_m, again.end_m) == (92.0, 111.0)
|
|
|
|
|
|
def test_span_is_normalized_to_contain_anchor():
|
|
"""시작·종료가 뒤집혔거나 기준점을 안 품으면 정규화한다 (파서는 관대하게)."""
|
|
flipped = parse_pipe_points([{"chainage_m": 100.0, "start_m": 110.0, "end_m": 90.0}])[0]
|
|
assert (flipped.start_m, flipped.end_m) == (90.0, 110.0)
|
|
|
|
outside = parse_pipe_points([{"chainage_m": 100.0, "start_m": 104.0, "end_m": 112.0}])[0]
|
|
assert outside.start_m == 100.0 and outside.end_m == 112.0
|
|
|
|
|
|
def test_half_span_is_completed_with_anchor():
|
|
"""한쪽만 오면 기준점으로 나머지를 채운다."""
|
|
point = parse_pipe_points([{"chainage_m": 100.0, "end_m": 115.0}])[0]
|
|
assert (point.start_m, point.end_m) == (100.0, 115.0)
|
|
|
|
|
|
def test_non_numeric_span_is_dropped():
|
|
point = parse_pipe_points([{"chainage_m": 100.0, "start_m": "십", "end_m": None}])[0]
|
|
assert point.start_m is None and point.end_m is None
|
|
|
|
|
|
# ── 시설 옵션 (세월교 관 종류/크기/수량 등) ────────────────────────────────
|
|
|
|
|
|
def test_options_roundtrip():
|
|
data = {
|
|
"chainage_m": 100.0,
|
|
"facility": "ford_bridge",
|
|
"options": {"pipe_kind": "흄관", "pipe_diameter_mm": 800, "pipe_count": 3},
|
|
}
|
|
point = parse_pipe_points([data])[0]
|
|
assert point.options == {"pipe_kind": "흄관", "pipe_diameter_mm": 800, "pipe_count": 3}
|
|
again = parse_pipe_points([point.as_dict()])[0]
|
|
assert again.options == point.options
|
|
|
|
|
|
def test_pipe_accessory_options_roundtrip():
|
|
"""배관 부속 키(2026-08-17 재편) — 유형·집수정·기슭막이·돌붙임이 왕복 보존되는지."""
|
|
data = {
|
|
"chainage_m": 240.0,
|
|
"facility": "pipe",
|
|
"start_m": 236.0,
|
|
"end_m": 246.0,
|
|
"options": {
|
|
"flow_type": "계곡부형",
|
|
"pipe_diameter_mm": 1000,
|
|
"catch_basin": "없음",
|
|
"revetment": "있음",
|
|
"revet_length_m": 10.0,
|
|
"revet_front_m": 4.0,
|
|
"stone_pitching": "있음",
|
|
},
|
|
}
|
|
point = parse_pipe_points([data])[0]
|
|
again = parse_pipe_points([point.as_dict()])[0]
|
|
assert again.options == data["options"]
|
|
assert (again.start_m, again.end_m) == (236.0, 246.0)
|
|
|
|
|
|
def test_non_dict_options_are_dropped():
|
|
point = parse_pipe_points([{"chainage_m": 100.0, "options": "D800x3"}])[0]
|
|
assert point.options is None
|
|
|
|
|
|
def test_empty_options_are_omitted_from_dict():
|
|
point = PipePoint(chainage_m=50.0, options={})
|
|
assert "options" not in point.as_dict()
|
|
|
|
|
|
# ── 시설 속성 승계 — 세부유역 계산 왕복에서 종류가 사라지면 안 된다 ────────
|
|
|
|
|
|
def test_carry_attributes_restores_facility_after_rebuild():
|
|
"""계산기는 chainage만 다뤄 재구성 목록이 전부 기본 배관이 된다 — 되붙여야 한다."""
|
|
requested = parse_pipe_points(
|
|
[
|
|
{
|
|
"chainage_m": 100.0,
|
|
"facility": "ford_bridge",
|
|
"start_m": 95.0,
|
|
"end_m": 108.0,
|
|
"options": {"pipe_count": 2},
|
|
},
|
|
{"chainage_m": 200.0},
|
|
]
|
|
)
|
|
rebuilt = [PipePoint(chainage_m=100.0, source="user"), PipePoint(chainage_m=200.0)]
|
|
|
|
carried = carry_facility_attributes(rebuilt, requested)
|
|
|
|
assert carried[0].facility == PIPE_FACILITY_FORD_BRIDGE
|
|
assert (carried[0].start_m, carried[0].end_m) == (95.0, 108.0)
|
|
assert carried[0].options == {"pipe_count": 2}
|
|
assert carried[1].facility == PIPE_FACILITY_PIPE
|
|
|
|
|
|
def test_carry_attributes_only_within_tolerance():
|
|
"""계산기 반올림(0.01m) 차이는 승계 · 허용 오차(0.05m) 밖은 옆 관 속성을 안 가져감."""
|
|
requested = parse_pipe_points([{"chainage_m": 150.0, "facility": "box_culvert"}])
|
|
near = carry_facility_attributes([PipePoint(chainage_m=150.004)], requested)
|
|
far = carry_facility_attributes([PipePoint(chainage_m=150.37)], requested)
|
|
|
|
assert near[0].facility == PIPE_FACILITY_BOX
|
|
assert far[0].facility == PIPE_FACILITY_PIPE and far[0].options is None
|
|
|
|
|
|
def test_carry_attributes_new_pipe_before_box_keeps_box_in_place():
|
|
"""옵션 없는 관을 BOX 앞에 두어도 BOX 속성은 제 측점에 남음 — 관 수 > 속성 원본 수."""
|
|
requested = parse_pipe_points(
|
|
[{"chainage_m": 100.0, "facility": "box_culvert", "options": {"body_width_m": 2.0}}]
|
|
)
|
|
rebuilt = [PipePoint(chainage_m=97.0), PipePoint(chainage_m=100.0)]
|
|
|
|
carried = carry_facility_attributes(rebuilt, requested)
|
|
|
|
assert carried[0].facility == PIPE_FACILITY_PIPE and carried[0].options is None
|
|
assert carried[1].facility == PIPE_FACILITY_BOX
|
|
assert carried[1].options == {"body_width_m": 2.0}
|
|
|
|
|
|
def test_carry_attributes_without_reference_is_noop():
|
|
rebuilt = [PipePoint(chainage_m=100.0)]
|
|
assert carry_facility_attributes(rebuilt, []) is rebuilt
|
|
assert rebuilt[0].facility == PIPE_FACILITY_PIPE
|
|
|
|
|
|
# ── 정렬 유지 ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_points_stay_sorted_by_chainage():
|
|
points = parse_pipe_points(
|
|
[
|
|
{"chainage_m": 300.0, "facility": "ford_pavement"},
|
|
{"chainage_m": 100.0},
|
|
{"chainage_m": 200.0, "facility": "box_culvert"},
|
|
]
|
|
)
|
|
assert [point.chainage_m for point in points] == [100.0, 200.0, 300.0]
|