사용자 확정(2026-09-07) — 소단은 좌측 별도 폼이 아니라 **다른 옹벽·기슭막이와 같은 자리** (구조물 배치)에서 놓는 것으로 바뀜. 기하는 그대로 두고 **입구만 옮겼음.** · 레지스트리에 **C군(사면안정) 「소단」** 신설. 옵션은 다른 C군 구간형과 같은 꼴 — 길이·기준측점 전·후 + 폭·간격(사면길이)·안쪽 기울기. · ⚠ **소단은 C군 구간형이지만 벽이 아님** — 기슭막이 제원 자리에 얹히지 않게 파이썬·TS 양쪽 벽 필터에서 뺐음. 안 뺐으면 소단을 놓는 순간 「독립 기슭막이」로 그려졌을 것임. · 구조물 목록이 바뀌면 소단 구간을 세션 사본(`berm`)으로 펴고, 달라졌을 때만 재계산함. 계산 계통(브라우저·서버·`USER_TOUCHED_KEYS`·옹벽 의무 판정)은 **한 줄도 안 바꿨음**. · ④에서 만든 좌측 별도 「소단」 패널은 걷어냈음(모듈 삭제). **안쪽 기울기 기본값 2° → 0°** (사용자 재확정). 기울이는 것은 실무이나 법령·교본 근거가 없어 기본으로 넣지 않고 폼에서 받음. 주석·지식DB 방침도 그에 맞춰 고침. **소단측구**(B군 종단배수)도 같은 꼴로 옵션을 붙였음 — 소단 위에 놓이는 시설이라 폭·간격이 소단을 따르고, 기울기만 **사면 쪽**(소단과 반대). 소단이 실제로 서면서 `PENDING_TYPE_IDS` 빈 칸이 풀려 연장 수량이 다른 구조물과 같은 길로 나옴. 자체검증 — 「근거 없는 기본값 금지」 시험에 두 타입 12개 기본값을 근거와 함께 등재. 소단측구 연장 시험을 「빈 칸」에서 「연장 100m」로 뒤집음. 전체 544 passed · 18 skipped. TS 타입 검사·ruff 통과. 화면 확인은 다음 단계. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
5.6 KiB
Python
116 lines
5.6 KiB
Python
"""구조물 정본(C군 사면안정 벽)을 횡단 측점에 얹는다.
|
|
|
|
왜 필요한가(2026-09-06 사용자 확정) — 좌측 「구조물 배치」로 넣은 옹벽·돌쌓기 같은 벽이
|
|
횡단도에도 서고 **절·성토 면적에도 반영**돼야 한다. 지금까지 이 목록은 측점만 심고
|
|
(`B05_Profile_Engine_Sections.resolve_extra_stations`) 기하가 없어 면적이 그대로였다.
|
|
|
|
방법은 **이미 도는 길을 그대로 태우는 것**이다. 독립 기슭막이가 쓰는 `section.revetment`
|
|
제원과 같은 꼴로 얹으면 횡단 기하(`B06_Section_UI_Cross_Revetment`)·설계선 트림·폐회로
|
|
면적(`B06_Section_Structure_Layouts`)·3D 가 손대지 않고 따라온다. 옛 D군 기슭막이가
|
|
관 정본으로 이관되며 사라졌던 `attach_revetments`(2026-08-28)를 C군 벽으로 되살린 것이다.
|
|
|
|
치수는 여기서 정하지 않는다 — 높이·형태만 넘기고 도형은 화면·3D 가 같은 산식으로 그린다.
|
|
|
|
**짝**: `common_util/common_util_structure_walls.ts` — 브라우저는 아직 저장하지 않은
|
|
목록으로 같은 제원을 만들어야 해서 한 벌을 더 둔다. 거울 테스트
|
|
`tmp/tests/test_b06_structure_walls_mirror.py` 가 같은 값이 나오는지 대조한다.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
|
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 정본이 누가거리를 0.01m로 끊어 쓴다.
|
|
_EDGE_TOLERANCE_M = 0.02
|
|
|
|
# 구조물 종류 → 횡단 기하가 아는 **형태** 이름. 형태가 벽 높이 한계·두께를 정하므로
|
|
# (`B06_Section_UI_Cross_Revetment.revetHeightLimit`) 가장 가까운 것으로 잇는다.
|
|
_FORM_BY_TYPE = {
|
|
"masonry_wet": "돌쌓기(찰)",
|
|
"masonry_dry": "돌쌓기(메)",
|
|
"boulder_masonry": "돌쌓기(메)",
|
|
"retaining_wall": "콘크리트",
|
|
"soil_guard": "통나무·목재틀",
|
|
}
|
|
|
|
|
|
# 소단 타입 id — 레지스트리와 한 벌이다(C군이지만 벽이 아니다).
|
|
BERM_TYPE_ID = "berm"
|
|
|
|
|
|
def load_wall_structures(project_root: Path) -> list[dict[str, Any]]:
|
|
"""구조물 정본에서 C군 벽(구간형) 목록을 읽는다. 실패하면 빈 목록."""
|
|
try:
|
|
types = structure_type_map()
|
|
found: list[dict[str, Any]] = []
|
|
for structure in load_structures(str(project_root))[1]:
|
|
definition = types.get(structure.type_id)
|
|
if definition is None or definition.group != "C" or definition.placement != "interval":
|
|
continue
|
|
# 소단은 C군 구간형이지만 **벽이 아니다** — 사면을 계단으로 끊는 시설이라
|
|
# 기슭막이 제원 자리에 얹으면 안 된다(계획서 3-9).
|
|
if structure.type_id == BERM_TYPE_ID:
|
|
continue
|
|
start, end = structure.start_m, structure.end_m
|
|
if start is None or end is None:
|
|
continue
|
|
options = structure.options or {}
|
|
found.append(
|
|
{
|
|
"structure_id": structure.structure_id,
|
|
"type_id": structure.type_id,
|
|
"name": definition.name,
|
|
"start_m": float(min(start, end)),
|
|
"end_m": float(max(start, end)),
|
|
"anchor_m": float(structure.anchor_m()),
|
|
"form": options.get("form") or _FORM_BY_TYPE.get(structure.type_id),
|
|
"height_m": options.get("height_m"),
|
|
# C군 폼에는 설치 측 칸이 없다 — 비워 두면 화면이 **성토가 나는 쪽**으로
|
|
# 세운다(`computeRevetmentLayout`). 사용자가 정하고 싶어지면 그때 칸을 낸다.
|
|
"side": options.get("side"),
|
|
"tiers": options.get("tiers"),
|
|
"lift_m": options.get("lift_m"),
|
|
"shift_m": options.get("shift_m"),
|
|
}
|
|
)
|
|
return found
|
|
except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다
|
|
logger.exception("B06 구조물 벽 정본을 읽지 못했습니다 (없는 것으로 본다)")
|
|
return []
|
|
|
|
|
|
def attach_wall_structures(project_root: Path, cross_sections: list[dict[str, Any]]) -> int:
|
|
"""구간 안 측점의 횡단 dict에 `revetment` 키를 얹는다. 얹은 개수를 돌려준다.
|
|
|
|
이미 관 정본이 얹은 세트(`culvert`·`revetment`)가 있는 측점은 **건드리지 않는다** —
|
|
관 유입·유출 벽과 구조물 벽이 한 자리에 겹치면 어느 쪽 그림인지 읽히지 않는다.
|
|
한 측점에 여러 개가 겹치면 먼저 시작한 것을 쓴다(겹침 정리는 사용자 몫).
|
|
"""
|
|
walls = load_wall_structures(project_root)
|
|
if not walls:
|
|
return 0
|
|
attached = 0
|
|
for section in cross_sections:
|
|
chainage = section.get("chainage_m")
|
|
if not isinstance(chainage, (int, float)):
|
|
continue
|
|
if section.get("culvert") or section.get("revetment"):
|
|
continue
|
|
for spec in walls:
|
|
if (
|
|
spec["start_m"] - _EDGE_TOLERANCE_M
|
|
<= float(chainage)
|
|
<= spec["end_m"] + _EDGE_TOLERANCE_M
|
|
):
|
|
section["revetment"] = spec
|
|
attached += 1
|
|
break
|
|
if attached:
|
|
logger.info("B06 구조물 벽 %d개 측점에 얹음 (구조물 %d건)", attached, len(walls))
|
|
return attached
|