feat(구조물표): 323-7 새 종류 · 정리 — M02 종류 더하기(본보기 복사 · 일곱 갈래 본보기 그대로) · 손코딩 동작 · 명세 = 본보기 키 · 특수 동작 = 동작 이름 · 정본 목록 = 표 · drawing_views 걷음 · 옛 이름 되짚기 걷음(자리로 가름) · 허용 거리 파이썬 한 곳 · 벽 툴팁 = 종류 이름
- M02 종류별 탭 [종류 더하기] = 본보기(구조물 정본 원본) 복사 — 키 자동 · 이름 「본보기 2」 · 옵션 · 표시 · 그룹 · 배치 · 규칙 · 종류 덧칸 · 반영 그대로 · 종류.{새}.본보기 · 관 지점 종류 · 복사본은 본보기 밖
- 구조물 표 = 본보기 처음값 이어받음(resolveCatalog · resolve_catalog 짝 · 줄 칸 base) · baseType/base_type · specFor(명세) · 덧단 · 길이 전후 풀기 · 옹벽 추가 단 콘크리트
- 검사(화면 = 서버 짝) — 본보기 = 원본 · 같은 군 · 자리꼴 · 정본 · 만든 뒤 고정 · 복사본은 본보기가 숨긴 칸 이어받음 · 서버 덧칸 본보기 받음
- 특수 동작 = 동작 이름 — 바로 안 놓기 · 포장 나누기 = 포장_물넘이_겹침 · 월류 높이 기다림 = 월류단면
- 정본 목록 = 표 — 파이썬 관 지점 종류 = catalog owner · B05 시설 서브폼 = 표 형상 · 안 쓰는 손 목록 걷음
- drawing_views — 스키마에서 빼고 읽을 때 버림 · M02 [저장] 때 빠짐(코드는 마스터 안 고침)
- 이관 = 지난 판 이름 대신 측점 계획 자리(planted)로 가름 · all_type_names 걷음
- 허용 거리 = common_util_structure_station.py 한 곳(SAME_STATION_M · EDGE_TOLERANCE_M · PIPE_MATCH_M) · TS 표는 station.ts 머리
- 시험: test_323_7_new_type(템플릿 · 일곱 갈래 · 그림 · 검사 짝 · 서버 읽기) · 322-0 유령 시험 = 자리 · 옛 도움 파일 동작 · 표 길 · 기대 글 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -18,6 +18,7 @@ import { BOX_SPEC } from "./design_structure_box";
|
||||
import { FORD_SPEC } from "./design_structure_ford";
|
||||
import { FORD_PAVEMENT_SPEC } from "./design_structure_ford_pavement";
|
||||
import { ETC_SPEC, PAVEMENT_SPEC } from "./design_structure_road";
|
||||
import { baseType } from "../../common_util/common_util_structure_catalog";
|
||||
|
||||
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
|
||||
const BASIN = "집수정";
|
||||
@@ -116,3 +117,7 @@ export const STRUCTURE_SPECS: Readonly<Record<string, StructureSpec>> = {
|
||||
pavement_concrete: PAVEMENT_SPEC,
|
||||
etc: ETC_SPEC,
|
||||
};
|
||||
|
||||
/** 그 종류의 명세 — 복사한 종류는 본보기 명세(323-7 · `baseType`). 화면은 이것으로 고름(종류 키 직접 안 씀). */
|
||||
export const specFor = (typeId: string): StructureSpec | undefined =>
|
||||
STRUCTURE_SPECS[baseType(typeId)];
|
||||
|
||||
@@ -471,17 +471,10 @@ export async function fetchRoadInflow(
|
||||
export type PipeSource = "stream" | "spacing" | "user";
|
||||
|
||||
/** 계곡 통과 시설 종류(2026-08-17 컨테이너 병합). 같은 계곡 교차점에서 유량·지형에
|
||||
* 따라 택일한다 — 정의처는 백엔드 `common_util_drainage_pipes`. 교량은 임도용이 아니다. */
|
||||
* 따라 택일한다 — 관 지점 정본 종류 = 구조물 표 정본(`catalogRow().owner` · 323-7) · 이름 = 표 이름 ·
|
||||
* 형상마다 손코딩 폼이 있어 종류 더하기(본보기)에서 뺌. 교량은 임도용이 아니다. */
|
||||
export type PipeFacility = "pipe" | "box_culvert" | "ford_pavement" | "ford_bridge" | "revetment";
|
||||
|
||||
export const PIPE_FACILITY_LABELS: ReadonlyArray<[PipeFacility, string]> = [
|
||||
["pipe", "배수관"],
|
||||
["box_culvert", "BOX암거"],
|
||||
["ford_pavement", "물넘이포장"],
|
||||
["ford_bridge", "세월교"],
|
||||
["revetment", "기슭막이"],
|
||||
];
|
||||
|
||||
export interface DetailPipePoint {
|
||||
chainage_m: number;
|
||||
lonlat: [number, number];
|
||||
|
||||
@@ -79,7 +79,6 @@ export interface StructureType {
|
||||
placement: StructurePlacement;
|
||||
options: StructureOptionField[];
|
||||
style: { color?: string; abbr?: string };
|
||||
drawing_views: string[];
|
||||
/** 다른 정본이 관리하는 타입(배관 = pipe_points.json) — 구조물 목록에 넣지 않는다. */
|
||||
managed_by: string | null;
|
||||
/** 목록에는 두되 **제원·수량을 내는 주인이 다른 화면**인 타입 — 그 화면 이름.
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
StructureInstance,
|
||||
all_type_names,
|
||||
structure_type_map,
|
||||
)
|
||||
from common_util.common_util_drainage_pipes import PIPE_SOURCE_USER, PipePoint
|
||||
from common_util.common_util_structure_station import SAME_STATION_M
|
||||
|
||||
PIPE_STRUCTURE_NAME = "배관"
|
||||
# 구조물 정본이 아니라 관 지점 정본이 관리한다는 표식(레지스트리 `managed_by`).
|
||||
@@ -54,25 +55,22 @@ _PIPE_LABEL = re.compile(r"^(배관|.*관)\s+D\d+", re.UNICODE)
|
||||
_ESCAPE_LABEL = re.compile(r"^대피로(?:\s+([\d.]+)\s*m)?", re.UNICODE)
|
||||
|
||||
|
||||
def _managed_elsewhere_labels() -> set[str]:
|
||||
"""정본이 이미 관리하는 측점 이름 — 구조물로 재이관하면 안 된다(정본 이중화 → "기타" 고스트).
|
||||
def _is_managed_elsewhere(entry: dict[str, Any], planted: Sequence[float]) -> bool:
|
||||
"""정본(관 지점 · 구조물)이 이미 관리하는 측점인가 — 재이관하면 정본 이중화 → 「기타」 유령.
|
||||
|
||||
지금 판 · 지난 판 정의의 **종류 이름 전부**(322-0) — 심는 측점 이름 · 관 지점 시설 이름만
|
||||
보던 옛 규칙은 M02 에서 종류 이름을 바꾸면 옛 이름 측점을 못 알아보고 「기타」로 굳혔다
|
||||
(이름 바꿈 · 심는 규칙 바뀜 둘 다).
|
||||
자리로 가름(323-7 · 지금 관 · 구조물이 심는 측점 계획 자리 `planted`) — 옛 이름 되짚기
|
||||
(지난 판 이름 전부 · 322-0)는 걷음 · 이름을 바꿔도 자리는 그대로라 옛 이름 측점도 알아봄.
|
||||
지금 판 종류 이름 · 관 라벨 꼴(관종 D관경)은 자리를 못 받은 부름(옛 확정분만)을 위해 그대로.
|
||||
"""
|
||||
return all_type_names()
|
||||
|
||||
|
||||
def _is_managed_elsewhere(entry: dict[str, Any]) -> bool:
|
||||
"""다른 정본(pipe_points.json·구조물 A군)이 관리하는 측점인가."""
|
||||
label = str(entry.get("structure", "")).strip()
|
||||
at = float(entry.get("chainage_m", 0.0))
|
||||
return (
|
||||
entry.get("origin") == "pipe"
|
||||
or entry.get("structureType") == PIPE_STRUCTURE_NAME
|
||||
or label == PIPE_STRUCTURE_NAME
|
||||
or label in _managed_elsewhere_labels()
|
||||
or label in {item.name for item in structure_type_map().values()}
|
||||
or bool(_PIPE_LABEL.match(label))
|
||||
or any(abs(at - p) < SAME_STATION_M for p in planted)
|
||||
)
|
||||
|
||||
|
||||
@@ -104,7 +102,9 @@ def _escape_width_of(entry: dict[str, Any]) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def migrate_irregular_stations(entries: Iterable[dict[str, Any]]) -> MigrationPlan:
|
||||
def migrate_irregular_stations(
|
||||
entries: Iterable[dict[str, Any]], planted: Sequence[float] = ()
|
||||
) -> MigrationPlan:
|
||||
"""기존 비정규 측점 목록을 정본별로 나눠 옮긴다(같은 입력이면 같은 결과).
|
||||
|
||||
타입이 다른 정본 소관(`managed_by`)이면 구조물 목록이 아니라 그 정본으로 간다 —
|
||||
@@ -117,7 +117,7 @@ def migrate_irregular_stations(entries: Iterable[dict[str, Any]]) -> MigrationPl
|
||||
seen: set[tuple[str, float]] = set()
|
||||
|
||||
for entry in entries:
|
||||
if _is_managed_elsewhere(entry):
|
||||
if _is_managed_elsewhere(entry, planted):
|
||||
continue
|
||||
|
||||
chainage = float(entry.get("chainage_m", 0.0))
|
||||
|
||||
@@ -43,6 +43,7 @@ from common_util.common_util_drainage_pipes import (
|
||||
read_pipe_points_file,
|
||||
)
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_structure_station import plan_stations
|
||||
from config.config_db import get_db_pool
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import TypesWithVisible, with_visible
|
||||
|
||||
@@ -193,7 +194,14 @@ async def migrate_structures(project_id: UUID, payload: StructureMigrateRequest)
|
||||
return JSONResponse(status_code=404, content=_PROJECT_PATH_MISSING)
|
||||
revision, existing = load_structures(root)
|
||||
migrated_before = load_migrated_legacy(root)
|
||||
plan = migrate_irregular_stations(payload.stations)
|
||||
# 정본이 이미 심는 자리(측점 계획 · 323-7) — 이름이 아니라 자리로 가름(옛 이름 되짚기 걷음)
|
||||
planted = [
|
||||
float(station["chainage_m"])
|
||||
for station in plan_stations(
|
||||
read_pipe_points_file(pipe_points_path_in(Path(root))), existing
|
||||
)
|
||||
]
|
||||
plan = migrate_irregular_stations(payload.stations, planted)
|
||||
# 표식 = "타입@위치". 원천(종단 정본의 비정규 측점)은 원복용으로 그대로 두고,
|
||||
# 옮긴 이력만 남긴다 — 그래야 사용자가 지운 구조물이 재진입 때 되살아나지 않는다
|
||||
# (2026-08-24 사용자: 초기 계산값은 원복용, 사용자 수정 1세트가 최종본).
|
||||
|
||||
@@ -88,8 +88,6 @@ class StructureType(BaseModel):
|
||||
placement: Placement
|
||||
options: list[StructureOptionField] = Field(default_factory=list)
|
||||
style: dict[str, str] = Field(default_factory=dict)
|
||||
# B06 도면 연계용 메타데이터 — 저장만 하고 이번 단계 완료 조건에는 넣지 않는다.
|
||||
drawing_views: list[str] = Field(default_factory=list)
|
||||
# 다른 정본이 관리하는 타입(배관 = pipe_points.json). structures.json에 저장하지 않는다.
|
||||
managed_by: str | None = None
|
||||
# 목록에는 두되 **제원·수량을 내는 주인이 다른 화면**인 타입 — 그 화면 이름을 적는다
|
||||
@@ -116,10 +114,19 @@ def _read_definition(path: str, _mtime: int, _size: int) -> dict[str, Any]:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
#: 정의에 남은 옛 칸 — 아무도 안 읽어 걷음(323-7 · 마스터는 M02 [저장] 때 빠짐 · 읽을 때 버림).
|
||||
RETIRED_TYPE_KEYS = ("drawing_views",)
|
||||
|
||||
|
||||
def without_retired(item: dict[str, Any]) -> dict[str, Any]:
|
||||
"""종류 한 줄에서 옛 칸을 뺀 사본."""
|
||||
return {k: v for k, v in item.items() if k not in RETIRED_TYPE_KEYS}
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def _read_types(path: str, mtime: int, size: int) -> tuple[StructureType, ...]:
|
||||
payload = _read_definition(path, mtime, size)
|
||||
return tuple(StructureType.model_validate(item) for item in payload["types"])
|
||||
return tuple(StructureType.model_validate(without_retired(item)) for item in payload["types"])
|
||||
|
||||
|
||||
def load_definition() -> dict[str, Any]:
|
||||
@@ -156,22 +163,6 @@ def is_station_planting_type(item: StructureType) -> bool:
|
||||
HISTORY_DIRNAME = "구조물정의_이력"
|
||||
|
||||
|
||||
def all_type_names() -> set[str]:
|
||||
"""지금 판 + 지난 판 정의의 종류 이름 전부 — 이름을 바꾼 뒤 옛 이름 측점도 알아봄(322-0)."""
|
||||
names = {item.name for item in load_structure_types()}
|
||||
history = os.path.join(os.path.dirname(DEFINITION_PATH), HISTORY_DIRNAME)
|
||||
for name in os.listdir(history) if os.path.isdir(history) else []:
|
||||
if not (name.startswith("판_") and name.endswith(".json")):
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(history, name), encoding="utf-8") as handle:
|
||||
types = json.load(handle).get("types", [])
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
names |= {str(t["name"]) for t in types if isinstance(t, dict) and t.get("name")}
|
||||
return names
|
||||
|
||||
|
||||
def registry_schema_version() -> int:
|
||||
return int(_read_definition(*_stamp()).get("schema_version", 1))
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ import {
|
||||
type SpecLinks,
|
||||
} from "../A00_Common/design/design_structure_form";
|
||||
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
|
||||
import { catalogRow } from "../common_util/common_util_structure_catalog";
|
||||
import {
|
||||
BOX_SPEC,
|
||||
bodyOf,
|
||||
@@ -496,13 +497,16 @@ export function createFacilityOptionsForm(
|
||||
);
|
||||
|
||||
let current: PipeFacility | null = null;
|
||||
/** 서브폼 = 구조물 표 형상(323-7 · 종류 키 대신 · 관 지점 시설은 형상마다 손코딩 폼 하나). */
|
||||
const shapeOf = (facility: PipeFacility | null) =>
|
||||
(facility && catalogRow(facility)?.shape) || null;
|
||||
|
||||
function syncVisibility(): void {
|
||||
root.hidden = current === null;
|
||||
if (current === null) return;
|
||||
const isPipe = current === "pipe";
|
||||
const isBox = current === "box_culvert";
|
||||
const isBridge = current === "ford_bridge";
|
||||
const isPipe = shapeOf(current) === "pipe_set";
|
||||
const isBox = shapeOf(current) === "box";
|
||||
const isBridge = shapeOf(current) === "ford";
|
||||
pipeForm.root.hidden = !isPipe;
|
||||
boxForm.root.hidden = !isBox;
|
||||
fordForm.root.hidden = !isBridge;
|
||||
@@ -512,8 +516,8 @@ export function createFacilityOptionsForm(
|
||||
wingOutFields.root.hidden = true;
|
||||
(spec ? spec.slot("inlet") : wingInHome).append(wingInFields.slot);
|
||||
(spec ? spec.slot("outlet") : wingOutHome).append(wingOutFields.slot);
|
||||
paveForm.root.hidden = current !== "ford_pavement";
|
||||
const isRevet = current === "revetment";
|
||||
paveForm.root.hidden = shapeOf(current) !== "ford_pavement";
|
||||
const isRevet = shapeOf(current) === "hidden_revet";
|
||||
revetTopRow.hidden = !isRevet;
|
||||
// 설치 측이 한쪽이면 그쪽 칸만 남긴다 — 이름표는 측점이 정한 좌·우다(안 정함 = 양쪽처럼 보임).
|
||||
const side = revetSide.value || "양쪽";
|
||||
@@ -580,10 +584,10 @@ export function createFacilityOptionsForm(
|
||||
},
|
||||
setChainage(chainageM) {
|
||||
chainage = chainageM;
|
||||
if (current === "pipe") pipeForm.refresh();
|
||||
if (shapeOf(current) === "pipe_set") pipeForm.refresh();
|
||||
},
|
||||
recount() {
|
||||
if (current === "pipe") pipeForm.recount();
|
||||
if (shapeOf(current) === "pipe_set") pipeForm.recount();
|
||||
},
|
||||
onInletStructureChange(handler) {
|
||||
inletStructureHandler = handler;
|
||||
@@ -658,28 +662,28 @@ export function createFacilityOptionsForm(
|
||||
},
|
||||
setDesignFlow(designFlow) {
|
||||
fordFlow = designFlow ?? null;
|
||||
if (current === "ford_bridge") fordFlowBox.fill();
|
||||
if (current === "ford_pavement") paveFlowBox.fill();
|
||||
if (shapeOf(current) === "ford") fordFlowBox.fill();
|
||||
if (shapeOf(current) === "ford_pavement") paveFlowBox.fill();
|
||||
syncVisibility();
|
||||
},
|
||||
readOptions() {
|
||||
const options: Record<string, string | number> = {};
|
||||
// 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈).
|
||||
if (current === "pipe") {
|
||||
if (shapeOf(current) === "pipe_set") {
|
||||
Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1)
|
||||
// 자동(규칙)은 고를 때까지 그대로 — 보여 주기만으로 파일이 안 바뀜(54-13)
|
||||
if (inletIsAuto()) {
|
||||
if (inletAuto!.structure === undefined) delete options.inlet_structure;
|
||||
else options.inlet_structure = inletAuto!.structure;
|
||||
}
|
||||
} else if (current === "box_culvert") {
|
||||
} else if (shapeOf(current) === "box") {
|
||||
// 정본은 폭 · 높이 두 칸 — 규격이면 규격 수 · 사용자 입력이면 명세 숫자 칸(PLAN 52-2)
|
||||
Object.assign(options, boxForm.read(), bodyOptions(boxForm.state().body?.[0]));
|
||||
} else if (current === "ford_pavement") {
|
||||
} else if (shapeOf(current) === "ford_pavement") {
|
||||
Object.assign(options, paveForm.read()); // 표 03 명세 값 · 옛 월류 · 경사 · 두께 · 길이는 병합이 이어 붙임(52-3)
|
||||
} else if (current === "ford_bridge") {
|
||||
} else if (shapeOf(current) === "ford") {
|
||||
Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4)
|
||||
} else if (current === "revetment") {
|
||||
} else if (shapeOf(current) === "hidden_revet") {
|
||||
if (revetSide.value) options.side = revetSide.value;
|
||||
// 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고
|
||||
// 조작은 [추가 기슭막이(단)] 행이 한다(2026-08-30 사용자: 중복이라 삭제).
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
RoutePointKind,
|
||||
} from "./B05_Profile_UI_Markers";
|
||||
import type { CirclePoint, RouteLatestResponse, RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { SectionStation } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import {
|
||||
irregularLabel,
|
||||
@@ -226,15 +225,6 @@ export function interpolateIrregularStations(
|
||||
});
|
||||
}
|
||||
|
||||
/** 시설 종류별 표시 이름 — 그래프·3D 라벨용(배관은 관종·관경까지 따로 붙인다). */
|
||||
export const FACILITY_NAMES: Record<PipeFacility, string> = {
|
||||
pipe: "배수관",
|
||||
box_culvert: "BOX암거",
|
||||
ford_pavement: "물넘이포장",
|
||||
ford_bridge: "세월교",
|
||||
revetment: "기슭막이",
|
||||
};
|
||||
|
||||
/* ── 측점 상단측(=측구 방향) 사용자 변경분 세션 보관 ─────────────────────
|
||||
* 3D 램프 클릭으로 바꾼 값. 경로 확정 때 uphill_overrides로 백엔드에 병합한다.
|
||||
* 화면 본체가 700줄에 닿아 읽기·쓰기만 여기로 뺐다(2026-09-04, 동작 불변). */
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
import { shapeFields, shapeKey } from "./B05_Profile_UI_Structures_Define";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { bodyOf, boxOptionsIn } from "../A00_Common/design/design_structure_box";
|
||||
import { withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
||||
import {
|
||||
@@ -43,7 +43,7 @@ export function balloonLines(
|
||||
head: { name: string; position: string },
|
||||
count?: ContainerCount,
|
||||
): string[] {
|
||||
const spec = STRUCTURE_SPECS[structure.type_id];
|
||||
const spec = specFor(structure.type_id);
|
||||
const values = (
|
||||
spec ? specLines(spec, structure, type, count) : groupLines(structure.options ?? {}, type)
|
||||
).filter(Boolean);
|
||||
@@ -158,7 +158,7 @@ export function structureOptionLines(
|
||||
const kept = parts.filter(Boolean);
|
||||
return kept.length ? [`${title}: ${kept.join(SEP)}`] : [];
|
||||
};
|
||||
const spec = STRUCTURE_SPECS[structure.type_id];
|
||||
const spec = specFor(structure.type_id);
|
||||
if (spec) {
|
||||
const names = specNames(structure as StructureInstance, type);
|
||||
const state = formState(spec, structure.type_id, options);
|
||||
|
||||
@@ -186,7 +186,10 @@ export function shapeFields(
|
||||
export interface StructureDefinition {
|
||||
판?: number;
|
||||
구조물군?: Record<string, string>;
|
||||
종류?: Record<string, { 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string> }>;
|
||||
종류?: Record<
|
||||
string,
|
||||
{ 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string>; 본보기?: string }
|
||||
>;
|
||||
그룹?: Record<string, Array<{ id: string; 이름?: string; 형상?: DefinedShape }>>;
|
||||
배치?: Record<string, Record<string, DefinedSpot>>;
|
||||
규칙?: DefinedRule[];
|
||||
|
||||
@@ -23,7 +23,7 @@ import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary";
|
||||
import { SHAPE_FIELDS, shapeFields, shapeKey } from "./B05_Profile_UI_Structures_Define";
|
||||
import { numberInput } from "./B05_Profile_UI_Structures_Fields";
|
||||
import { grid, group as boxOf, labeled, stepper } from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import {
|
||||
GROUP_JOIN,
|
||||
type GroupItems,
|
||||
@@ -117,7 +117,7 @@ export function createGroupBoxes(): GroupBoxes {
|
||||
shapeInputs.clear();
|
||||
current = type;
|
||||
// 명세 종류(PLAN 52 · 배관)는 컨테이너가 잇기 줄을 가짐 — 그룹 상자를 안 세움
|
||||
const groups = type && STRUCTURE_SPECS[type.type_id] ? [] : (type?.groups ?? []);
|
||||
const groups = type && specFor(type.type_id) ? [] : (type?.groups ?? []);
|
||||
const boxes: HTMLElement[] = [];
|
||||
bodies = [];
|
||||
// 「구조물 옵션 보이기」에서 끈 칸 — 시설 폼 밖으로 옮긴 칸도 같은 규칙으로 가림(Hidden 과 같은 식)
|
||||
@@ -200,9 +200,7 @@ export function createGroupBoxes(): GroupBoxes {
|
||||
bodies.push(body);
|
||||
}
|
||||
const stale =
|
||||
type && !STRUCTURE_SPECS[type.type_id]
|
||||
? paintStale(new Set(groups.map((g) => g.id)))
|
||||
: null;
|
||||
type && !specFor(type.type_id) ? paintStale(new Set(groups.map((g) => g.id))) : null;
|
||||
root.replaceChildren(...boxes, ...(stale ? [stale] : []));
|
||||
root.hidden = !type || (!boxes.length && !stale);
|
||||
// 잠금은 계곡 통과 시설 서브폼(`inert`)을 따름 — 레지스트리 칸은 칸마다 disabled 라 그대로
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
summaryTooltip,
|
||||
type SummaryItem,
|
||||
} from "../common_util/common_util_summary_item";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
||||
import { pipeExtraCount } from "../A00_Common/design/design_structure_pipe_calc";
|
||||
import {
|
||||
@@ -103,7 +103,7 @@ export function linkLamps(
|
||||
): LinkLamp[] {
|
||||
const links = readGroupItems(source.group_items);
|
||||
const summary = readSummaryItem(source.summary_item);
|
||||
const spec = STRUCTURE_SPECS[source.type_id];
|
||||
const spec = specFor(source.type_id);
|
||||
if (spec?.containers.length) {
|
||||
const groupNames = Object.fromEntries((type?.groups ?? []).map((g) => [g.id, g.name]));
|
||||
// 배관 흙막이 = 명세 이름 「흙막이」가 정의 옛 이름 「흙막이 추가」보다 앞섬(54-1 · 시설 폼과 같음)
|
||||
|
||||
@@ -10,10 +10,10 @@ import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structu
|
||||
import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
|
||||
/** 바로 추가하지 않는 종류 — 노면공(겹침 확인창). */
|
||||
const FORM_ONLY = new Set(["pavement_concrete"]);
|
||||
/** 월류 높이를 유량 뒤에 받는 종류 — 추가 때 알림 한 줄. */
|
||||
const FORD_KINDS = new Set(["ford_bridge", "ford_pavement"]);
|
||||
/** 바로 추가하지 않는 동작 — 포장 · 물넘이 겹침(확인창 · 323-7 종류 키 대신 동작 이름). */
|
||||
const FORM_ONLY = "포장_물넘이_겹침";
|
||||
/** 월류 높이를 유량 뒤에 받는 동작 — 추가 때 알림 한 줄. */
|
||||
const FORD_KINDS = "월류단면";
|
||||
export const FORD_WAIT_NOTICE = "월류 높이는 배수유역 재계산(설계유량)이 오면 채워집니다.";
|
||||
|
||||
/** 우클릭 한 번으로 [추가]까지 끝낼 수 있는지 — [추가]가 막는 빈 필수 칸이 없어야 함. */
|
||||
@@ -21,7 +21,7 @@ export function addsAtOnce(
|
||||
type: StructureType,
|
||||
optionInputs: ReadonlyArray<{ required: boolean; isEmpty: () => boolean }>,
|
||||
): boolean {
|
||||
if (FORM_ONLY.has(type.type_id)) return false;
|
||||
if (type.behaviors?.includes(FORM_ONLY)) return false;
|
||||
return !optionInputs.some((entry) => entry.required && entry.isEmpty());
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function finishAdd(ctx: {
|
||||
// B05 는 재계산 뒤 배수유역 패널이 새 관 고름을 다시 알려 폼이 열림.
|
||||
await ctx.commit();
|
||||
ctx.pick.selectPipeByChainage(ctx.anchorM);
|
||||
if (FORD_KINDS.has(ctx.type.type_id)) showToast(FORD_WAIT_NOTICE);
|
||||
if (ctx.type.behaviors?.includes(FORD_KINDS)) showToast(FORD_WAIT_NOTICE);
|
||||
return true;
|
||||
}
|
||||
const before = new Set(ctx.structures.map((entry) => entry.structure_id));
|
||||
|
||||
@@ -22,7 +22,7 @@ import { mergeStructureOptions } from "./B05_Profile_UI_Structures_Summary";
|
||||
import { readStructureSpec, specRenderedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
import { asRoad } from "./B05_Profile_UI_Structures_Define";
|
||||
import type { GroupBoxes } from "./B05_Profile_UI_Structures_Groups";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { snapStructureStation } from "../common_util/common_util_structure_station";
|
||||
import {
|
||||
readGroupItems,
|
||||
@@ -117,7 +117,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance
|
||||
const links = readGroupItems(target.group_items);
|
||||
const summary = readSummaryItem(target.summary_item);
|
||||
// 정의 그룹 종류는 종류 단위 잇기 줄이 없음 — 옛 종류 단위 값은 첫 그룹 컨테이너 잇기로 읽음(54-6)
|
||||
const first = type && !STRUCTURE_SPECS[type.type_id] ? type.groups?.[0]?.id : undefined;
|
||||
const first = type && !specFor(type.type_id) ? type.groups?.[0]?.id : undefined;
|
||||
const moved = !!(first && summary && !links[first]);
|
||||
ctx.setSummary(moved ? null : summary);
|
||||
ctx.groupBoxes.set(moved ? { ...links, [first!]: summary! } : links);
|
||||
@@ -320,9 +320,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
);
|
||||
// 기준 측점(마킹 위치) — 비우면 시작 측점. 시작~종료 밖은 서버도 거절한다.
|
||||
// 범위 직접 입력형(노면공 · 87-1) = 기준 칸 없음 → 늘 시점.
|
||||
anchor = STRUCTURE_SPECS[type.type_id]?.byRange
|
||||
? start
|
||||
: (ctx.anchorFields.read(step, false) ?? start);
|
||||
anchor = specFor(type.type_id)?.byRange ? start : (ctx.anchorFields.read(step, false) ?? start);
|
||||
if (anchor < start || anchor > end) {
|
||||
ctx.anchorFields.station.classList.add("is-invalid");
|
||||
return stop(ctx.anchorFields.station, "기준 측점(시작 ~ 끝 안)");
|
||||
@@ -357,7 +355,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis
|
||||
// 포장 구간은 물넘이포장과 겹칠 수 없다 — 통째로 품으면 나누고, 끝에 걸치면 멈춘다
|
||||
// (2026-08-28 사용자 확정). 다른 타입은 원래 구간 한 벌 그대로다.
|
||||
const spans =
|
||||
type.type_id === "pavement_concrete" && start !== null && end !== null
|
||||
type.behaviors?.includes("포장_물넘이_겹침") && start !== null && end !== null
|
||||
? await splitPavementRange(start, end, ctx.pipeFacilities())
|
||||
: [{ start, end }];
|
||||
if (!spans) return;
|
||||
|
||||
@@ -21,14 +21,15 @@ import {
|
||||
} from "./B05_Profile_UI_Structures_Define";
|
||||
import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge";
|
||||
import { stepper } from "./B05_Profile_UI_Drainage_Facility_Fields";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { baseType } from "../common_util/common_util_structure_catalog";
|
||||
import { paintStructureSpec, specOwnedKeys } from "./B05_Profile_UI_Structures_Spec";
|
||||
import { solveSplit, specKeys, type SplitRole } from "../A00_Common/design/design_structure_spec";
|
||||
|
||||
/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로
|
||||
* 그리는 종류(PLAN 52-1 · 52-6b · 52-7) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */
|
||||
export const hidesTypeSummary = (type: StructureType): boolean =>
|
||||
!!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length || type.group === "B";
|
||||
!!specFor(type.type_id)?.containers.length || !!type.groups?.length || type.group === "B";
|
||||
|
||||
/** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일 · 표 09 흙막이 · 옹벽). */
|
||||
const SOLVE_SPLIT = new Set(["guardrail", "soil_guard", "retaining_wall"]);
|
||||
@@ -150,7 +151,7 @@ export function renderOptionFields(
|
||||
"length" | "before_m" | "after_m";
|
||||
const refresh = (normalize: boolean): void => {
|
||||
input.classList.remove("is-invalid");
|
||||
if (SOLVE_SPLIT.has(type!.type_id)) {
|
||||
if (SOLVE_SPLIT.has(baseType(type!.type_id))) {
|
||||
solveInto(option.key); // 타이핑 중에도 — change 의 실시간 반영이 푼 값을 읽게
|
||||
ctx.syncRangeDisplay();
|
||||
} else ctx.syncSplitInputs(source, normalize);
|
||||
@@ -273,7 +274,7 @@ export function paintStale(
|
||||
shapeKeys: string[] = [],
|
||||
): void {
|
||||
// 명세 종류(52) — 명세가 뺀 옛 키(retired)는 지난 정의 값 · 선택 컨테이너 키(머리)는 앎
|
||||
const spec = type ? STRUCTURE_SPECS[type.type_id] : undefined;
|
||||
const spec = type ? specFor(type.type_id) : undefined;
|
||||
const own = spec ? specKeys(spec) : { keys: [], prefixes: [] };
|
||||
const known = [
|
||||
...shapeKeys,
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
import type { DesignSelection, ResolvedSelection } from "../A00_Common/design/design_selection";
|
||||
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { catalogRow } from "../common_util/common_util_structure_catalog";
|
||||
|
||||
/** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다.
|
||||
@@ -235,7 +235,7 @@ export function readBeforeOption(inputs: OptionEntry[]): number {
|
||||
|
||||
/** 시작 · 종료 측점만 받는 명세 종류인가(노면공 · 87-1) — 기준 측점 칸 없음 · 길이 출력. */
|
||||
export const isRangeOnly = (type: { type_id: string } | null): boolean =>
|
||||
!!type && !!STRUCTURE_SPECS[type.type_id]?.byRange;
|
||||
!!type && !!specFor(type.type_id)?.byRange;
|
||||
|
||||
/** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이
|
||||
* 있으면 시작·종료 입력 칸을 없애고 기준 측점 + 길이로 범위를 계산해 보여만
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
type SpecFormHooks,
|
||||
type SpecLinks,
|
||||
} from "../A00_Common/design/design_structure_form";
|
||||
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../A00_Common/design/design_structure_pipe";
|
||||
import { ADDON_SPECS } from "../A00_Common/design/design_structure_slope_tier";
|
||||
import { baseType } from "../common_util/common_util_structure_catalog";
|
||||
import { specKeys, type StructureSpec } from "../A00_Common/design/design_structure_spec";
|
||||
import { hiddenOptionKeys, optionDefaults } from "./B05_Profile_UI_Structures_Fields";
|
||||
import type { WideningBox } from "./B05_Profile_UI_Structures_Widening";
|
||||
@@ -27,7 +28,7 @@ const widenings = new WeakMap<HTMLElement, WideningBox>();
|
||||
|
||||
/** 일반 구조물 명세 — 계곡 통과 시설(관 지점 정본)은 시설 폼 몫이라 뺌 · 정의 그룹 종류 덧붙임(178-3 추가 단)도. */
|
||||
export const structureSpecOf = (type: StructureType | null): StructureSpec | null =>
|
||||
(type && !type.managed_by && (STRUCTURE_SPECS[type.type_id] ?? ADDON_SPECS[type.type_id])) ||
|
||||
(type && !type.managed_by && (specFor(type.type_id) ?? ADDON_SPECS[baseType(type.type_id)])) ||
|
||||
null;
|
||||
|
||||
/** 옵션 칸 짝 폼을 세움 — 값이 바뀌면 `onChange`(패널 즉시 반영). */
|
||||
|
||||
@@ -31,6 +31,7 @@ from typing import Any
|
||||
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
|
||||
from common_util.common_util_drainage_pipes import parse_pipe_points
|
||||
from common_util.common_util_structure_catalog import catalog_row
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
from config.config_system import (
|
||||
DRAINAGE_CACHE_DIRNAME,
|
||||
DRAINAGE_EDITS_DIRNAME,
|
||||
@@ -39,8 +40,8 @@ from config.config_system import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 관 지점과 횡단 측점을 같은 자리로 볼 허용 오차(m). 정본이 누가거리를 0.01m로 끊어 쓴다.
|
||||
_CHAINAGE_TOLERANCE_M = 0.02
|
||||
# 관 지점과 횡단 측점을 같은 자리로 볼 허용 오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_CHAINAGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
# 세트 종류 -> 횡단 dict 키. 그림이 다른 시설끼리 소비처가 섞이지 않게 나눠 둔다.
|
||||
_SECTION_KEYS = {"ford": "ford", "box": "box", "ford_pavement": "ford_pavement"}
|
||||
|
||||
@@ -24,12 +24,13 @@ 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
|
||||
from common_util.common_util_structure_catalog import catalog_row
|
||||
from common_util.common_util_structure_catalog import base_type, catalog_row
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 정본이 누가거리를 0.01m로 끊어 쓴다.
|
||||
_EDGE_TOLERANCE_M = 0.02
|
||||
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def _wall_form(type_id: str) -> str | None:
|
||||
@@ -58,7 +59,7 @@ def tier_extras(type_id: str, options: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"form": "콘크리트"
|
||||
if type_id == "retaining_wall"
|
||||
if base_type(type_id) == "retaining_wall" # 본보기 포함(323-7)
|
||||
else (options.get(f"tier_{n}_form") or None),
|
||||
"height_m": _num(options.get(f"tier_{n}_height")),
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ from typing import Any
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets
|
||||
from common_util.common_util_structure_catalog import catalog_row
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_EDGE_TOLERANCE_M = 0.02
|
||||
_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
TiltRange = tuple[float, float, float | None]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from common_util.common_util_cross_berm import (
|
||||
BermSpec,
|
||||
)
|
||||
from common_util.common_util_route_profile import design_elevation_from_longitudinal
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
from config.config_system import STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -125,8 +126,8 @@ def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
# 범위 끝 허용오차(m) — 브라우저 짝(`common_util_structure_pavement` · `_culvert_sets`)과 같은 값.
|
||||
_PAVED_EDGE_TOLERANCE_M = 0.02
|
||||
# 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool:
|
||||
|
||||
@@ -93,6 +93,8 @@ export interface RevetmentLayout {
|
||||
chainageM: number;
|
||||
/** 벽 구조물 id — 그린 벽 신원표(323-5). */
|
||||
structureId: string | null;
|
||||
/** 종류 이름(옹벽 · 흙막이 · 돌쌓기 …) — 툴팁 글(323-7 · 「독립 기슭막이」로 뜨던 것). */
|
||||
name: string;
|
||||
/** 사용자가 요청한 단 수 — 실제로 선 단 수(`walls.length`)와 다르면 자리가 부족한 것이다. */
|
||||
requestedTiers: number;
|
||||
/** 1단 벽 이음선 상단점(성토 설계선 트림 경계). */
|
||||
@@ -319,6 +321,7 @@ export function computeRevetmentLayout(
|
||||
heightM: pureHeight,
|
||||
chainageM: Number(section.chainage_m) || 0,
|
||||
structureId: spec.structure_id ?? null,
|
||||
name: spec.name || "벽",
|
||||
requestedTiers,
|
||||
top: jt,
|
||||
walls,
|
||||
@@ -352,7 +355,7 @@ export function appendRevetmentOverlay(
|
||||
segment.points,
|
||||
x,
|
||||
y,
|
||||
`독립 기슭막이 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m` +
|
||||
`${layout.name} 성토부선 — 사면길이 ${segment.lengthM.toFixed(2)}m` +
|
||||
(segment.overLimit ? " (법정 5m 이상 — 단 추가 검토)" : " (법정 5m 이내)") +
|
||||
` · 성토 물매 1:${(segment.ratio ?? 1.2).toFixed(2)}`,
|
||||
);
|
||||
@@ -366,7 +369,7 @@ export function appendRevetmentOverlay(
|
||||
layout.walls.forEach((wall, index) => {
|
||||
const keyId = index === 0 ? "own" : `own-extra${index - 1}`;
|
||||
const tooltip =
|
||||
`독립 기슭막이 ${wall.form ?? ""} H=${(wall.height + REVET_EMBED_DEPTH_M).toFixed(1)}m` +
|
||||
`${layout.name} ${wall.form ?? ""} H=${(wall.height + REVET_EMBED_DEPTH_M).toFixed(1)}m` +
|
||||
`(상단 = 성토선 접점, 전면 1:${wall.lean}(품셈 13-4-4 [주]⑪ 표준경사))` +
|
||||
(wall.floatGapM > 0.01
|
||||
? ` · ⚠ 바닥 원지반 이격 ${wall.floatGapM.toFixed(2)}m — 하부 지지 별도`
|
||||
|
||||
@@ -26,6 +26,7 @@ from B05_Profile.B05_Profile_Structures_Schema import load_definition, load_stru
|
||||
from B06_Section.B06_Section_Engine_Culvert import _culvert_set, pipe_points_file
|
||||
from common_util.common_util_drainage_pipes import PIPE_FACILITY_PIPE, parse_pipe_points
|
||||
from common_util.common_util_node_bundle import run_bundle_json
|
||||
from common_util.common_util_structure_station import PIPE_MATCH_M # 같은 자리 폭 한 곳(323-7)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,8 +34,6 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
_OPTIONS_BUNDLE = ROOT / "config" / "b07_note_node" / "B07_DesignDetail_Note_Options_Node.js"
|
||||
_OPTIONS_NPM = "build:b07-note"
|
||||
|
||||
# TS `PIPE_MATCH_M`(`design_selection`) — 관 · 구조물 자리와 측점이 같은 자리로 보는 폭
|
||||
PIPE_MATCH_M = 0.51
|
||||
_LAYER_ID = "기본"
|
||||
_FONT = 2.0
|
||||
_LINE = 3.0
|
||||
|
||||
@@ -111,7 +111,8 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
|
||||
continue
|
||||
if row["shape"] == "none" and row["station"] != "none":
|
||||
out.append(f"「{row['name']}」 형상이 「없음」이면 횡단도도 「안 만듦」")
|
||||
kept = _hidden_in(before or doc, t["type_id"]) # 앞 판 없음 = 새로 숨긴 것 없음
|
||||
# 앞 판 없음 = 새로 숨긴 것 없음 · 복사한 종류 = 본보기가 앞 판에서 숨긴 칸도 그대로(323-7)
|
||||
kept = [k for tid in (t["type_id"], row["base"]) for k in _hidden_in(before or doc, tid)]
|
||||
hidden = [key for key in _hidden_in(doc, t["type_id"]) if key not in kept]
|
||||
# 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
|
||||
keys = {
|
||||
@@ -144,10 +145,36 @@ def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]
|
||||
return out
|
||||
|
||||
|
||||
def _template_errors(doc: dict[str, Any]) -> list[str]:
|
||||
"""본보기(복사한 종류 · 323-7) — 원본(구조물 정본 · 본보기 아닌 종류) · 같은 군 · 자리꼴 ·
|
||||
정본(TS 짝)."""
|
||||
out: list[str] = []
|
||||
kinds = doc.get("종류") or {}
|
||||
now = {t["type_id"]: t for t in doc.get("types") or []}
|
||||
for tid, row in kinds.items():
|
||||
src = (row or {}).get("본보기") if isinstance(row, dict) else None
|
||||
if src is None or tid not in now:
|
||||
continue
|
||||
t, base = now[tid], now.get(src)
|
||||
name = t.get("name") or tid
|
||||
if base is None or src == tid or (kinds.get(src) or {}).get("본보기"):
|
||||
out.append(f"「{name}」 본보기 「{src}」 — 원본 종류여야 함")
|
||||
continue
|
||||
same = all(
|
||||
t.get(k) == base.get(k) for k in ("group", "placement", "managed_by")
|
||||
) and not base.get("managed_by")
|
||||
if not same:
|
||||
out.append(
|
||||
f"「{name}」 본보기 「{base.get('name') or src}」 와 군 · 자리꼴 · 정본이 같아야 함"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
|
||||
"""앞 판과 비교 — 만든 뒤 고정."""
|
||||
out: list[str] = []
|
||||
now = {t["type_id"]: t for t in doc.get("types") or []}
|
||||
base_of = lambda d, tid: ((d.get("종류") or {}).get(tid) or {}).get("본보기") # noqa: E731
|
||||
for old in before.get("types") or []:
|
||||
tid = old["type_id"]
|
||||
name = old.get("name") or tid
|
||||
@@ -159,6 +186,7 @@ def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
|
||||
("자리꼴", old.get("placement"), t.get("placement")),
|
||||
("정본", old.get("managed_by"), t.get("managed_by")),
|
||||
("군 글자", old.get("group"), t.get("group")),
|
||||
("본보기", base_of(before, tid), base_of(doc, tid)),
|
||||
)
|
||||
for what, a, b in fixed:
|
||||
if a != b:
|
||||
@@ -180,4 +208,5 @@ def catalog_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[s
|
||||
shape = _shape_errors(doc, known)
|
||||
if shape:
|
||||
return shape
|
||||
return _row_errors(doc, before) + _fixed_errors(doc, before) if before else []
|
||||
templates = _template_errors(doc)
|
||||
return templates + (_row_errors(doc, before) + _fixed_errors(doc, before) if before else [])
|
||||
|
||||
@@ -29,12 +29,13 @@ import {
|
||||
import { buildOptionsBlock } from "./M02_SO_Def_Options";
|
||||
import { buildRulesBlock } from "./M02_SO_Def_Rules";
|
||||
import { buildSpecBlock } from "./M02_SO_Def_Spec";
|
||||
import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe";
|
||||
import { specFor } from "../../A00_Common/design/design_structure_pipe";
|
||||
import { catalogProblems, type CheckDef } from "./M02_SO_Def_Check";
|
||||
import { buildParts } from "./M02_SO_Def_Parts";
|
||||
import { differs, setReflect, tidy } from "./M02_SO_Def_Reflect";
|
||||
import { buildReflectBlock, buildTable } from "./M02_SO_Def_Table";
|
||||
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { addFromTemplate, templates } from "./M02_SO_Def_Add";
|
||||
import { buildGroupBlock } from "./M02_SO_Groups";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
@@ -116,6 +117,21 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
const d = shown();
|
||||
const edit = canEdit();
|
||||
const parts: HTMLElement[] = [];
|
||||
// 종류 더하기(323-7) — 본보기 고름 + 단추 · 새 종류를 골라 둠(이름 · 반영은 오른쪽에서 고침)
|
||||
if (edit) {
|
||||
const pairs = templates(d);
|
||||
let pick = pairs.find(([key]) => key === current)?.[0] ?? pairs[0]?.[0] ?? "";
|
||||
const box = pickBox(pairs, pick, (v) => (pick = v), { edit, attr: "data-sod-add-from" });
|
||||
box.title = to("AddKindNote");
|
||||
const add = smallButton(to("AddKind"), () => {
|
||||
const key = addFromTemplate(d, pick);
|
||||
if (!key) return;
|
||||
current = key;
|
||||
changed();
|
||||
});
|
||||
add.setAttribute("data-sod-add", "");
|
||||
parts.push(el("div", { className: "m02-sod__family", children: [box, add] }));
|
||||
}
|
||||
const families = Object.keys(d.구조물군);
|
||||
families.forEach((g, gi) => {
|
||||
parts.push(
|
||||
@@ -194,7 +210,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void {
|
||||
const edit = canEdit();
|
||||
const extra = (): NonNullable<StructDef["종류"][string]> => (d.종류[type.type_id] ??= {});
|
||||
// 이름만 고침(PLAN 52-11 · 52-12) — 명세 종류 = 명세 나무 · 나머지 = 정의 그룹 나무
|
||||
const spec = STRUCTURE_SPECS[type.type_id];
|
||||
const spec = specFor(type.type_id);
|
||||
detail.replaceChildren(
|
||||
el("div", {
|
||||
className: "m02-sod__kindhead",
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Add.ts
|
||||
* 종류 더하기(PLAN 323-7) — 본보기(구조물 정본 원본 종류)를 그대로 복사한 새 종류.
|
||||
*
|
||||
* 키 자동(`{본보기}_xxxxxx` · 만든 뒤 안 바뀜) · 이름 「{본보기} 2」(겹치면 3 …) · 군 · 자리꼴 · 옵션 · 표시 · 그룹 ·
|
||||
* 배치 · 규칙 · 종류 덧칸(동작 · 이름 표) · 반영 그대로 · `종류.{새}.본보기 = 원본` — 구조물 표가 본보기 처음값을
|
||||
* 이어받고(`resolveCatalog` · 일곱 갈래 = 목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D · 자리꼴), 손코딩 동작 ·
|
||||
* 명세도 본보기 키로 고름(`baseType`). 관 지점 정본 종류는 본보기에서 뺌(관 시설 폼 · 정본 목록이 따로).
|
||||
* ========================================================================== */
|
||||
|
||||
import { clone, newKey, type StructDef } from "./M02_SO_Def_Model";
|
||||
|
||||
/** 본보기로 고를 수 있는 종류 — 구조물 정본 · 원본(본보기 아님) · [키, 이름] (정의 차례). */
|
||||
export const templates = (def: StructDef): Array<[string, string]> =>
|
||||
def.types
|
||||
.filter((t) => !t.managed_by && !def.종류[t.type_id]?.본보기)
|
||||
.map((t) => [t.type_id, t.name] as [string, string]);
|
||||
|
||||
/** 본보기를 복사해 그 뒤에 새 종류를 끼움 — 새 키(못 하면 null). */
|
||||
export function addFromTemplate(def: StructDef, sourceId: string): string | null {
|
||||
const index = def.types.findIndex((t) => t.type_id === sourceId);
|
||||
const src = def.types[index];
|
||||
if (!src || src.managed_by || def.종류[sourceId]?.본보기) return null;
|
||||
const key = newKey(
|
||||
`${sourceId}_`,
|
||||
def.types.map((t) => t.type_id),
|
||||
);
|
||||
const names = new Set(def.types.map((t) => t.name));
|
||||
let n = 2;
|
||||
while (names.has(`${src.name} ${n}`)) n += 1;
|
||||
const copy = clone(src);
|
||||
copy.type_id = key;
|
||||
copy.name = `${src.name} ${n}`;
|
||||
delete copy.drawing_views; // 안 읽는 칸(323-7)
|
||||
def.types.splice(index + 1, 0, copy);
|
||||
def.종류[key] = { ...clone(def.종류[sourceId] ?? {}), 본보기: sourceId };
|
||||
for (const table of [def.표시, def.그룹, def.배치] as Array<Record<string, unknown>>)
|
||||
if (table[sourceId] !== undefined) table[key] = clone(table[sourceId]);
|
||||
def.규칙.push(
|
||||
...def.규칙.filter((r) => r.종류 === sourceId).map((r) => ({ ...clone(r), 종류: key })),
|
||||
);
|
||||
const reflect = (def as { 반영?: Record<string, unknown> }).반영;
|
||||
if (reflect?.[sourceId] !== undefined) reflect[key] = clone(reflect[sourceId]);
|
||||
return key;
|
||||
}
|
||||
@@ -126,7 +126,8 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
||||
if (!row) continue;
|
||||
if (row.shape === "none" && row.station !== "none")
|
||||
out.push(`「${row.name}」 형상이 「없음」이면 횡단도도 「안 만듦」`);
|
||||
const kept = hiddenIn(before ?? doc, t.type_id); // 앞 판 없음 = 새로 숨긴 것 없음
|
||||
// 앞 판 없음 = 새로 숨긴 것 없음 · 복사한 종류 = 본보기가 앞 판에서 숨긴 칸도 그대로(323-7)
|
||||
const kept = [t.type_id, row.base].flatMap((id) => hiddenIn(before ?? doc, id));
|
||||
const hidden = hiddenIn(doc, t.type_id).filter((key) => !kept.includes(key));
|
||||
// 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
|
||||
const keys = new Set(
|
||||
@@ -161,9 +162,35 @@ function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
|
||||
}
|
||||
|
||||
/** 앞 판과 비교 — 만든 뒤 고정. */
|
||||
/** 본보기(복사한 종류 · 323-7) — 원본(구조물 정본 · 본보기 아닌 종류) · 같은 군 · 자리꼴 · 정본(파이썬 짝). */
|
||||
function templateErrors(doc: CheckDef): string[] {
|
||||
const out: string[] = [];
|
||||
const kinds = doc.종류 ?? {};
|
||||
const now = new Map((doc.types ?? []).map((t) => [t.type_id, t]));
|
||||
for (const [tid, row] of Object.entries(kinds)) {
|
||||
const src = row?.본보기;
|
||||
const t = now.get(tid);
|
||||
if (src == null || !t) continue;
|
||||
const base = now.get(src);
|
||||
const name = t.name || tid;
|
||||
if (!base || src === tid || kinds[src]?.본보기) {
|
||||
out.push(`「${name}」 본보기 「${src}」 — 원본 종류여야 함`);
|
||||
continue;
|
||||
}
|
||||
const same =
|
||||
(["group", "placement", "managed_by"] as const).every(
|
||||
(k) => (t[k] ?? null) === (base[k] ?? null),
|
||||
) && !base.managed_by;
|
||||
if (!same)
|
||||
out.push(`「${name}」 본보기 「${base.name || src}」 와 군 · 자리꼴 · 정본이 같아야 함`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
|
||||
const out: string[] = [];
|
||||
const now = new Map((doc.types ?? []).map((t) => [t.type_id, t]));
|
||||
const baseOf = (d: CheckDef, tid: string) => d.종류?.[tid]?.본보기 ?? null;
|
||||
for (const old of before.types ?? []) {
|
||||
const t = now.get(old.type_id);
|
||||
const name = old.name ?? old.type_id;
|
||||
@@ -175,6 +202,7 @@ function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
|
||||
["자리꼴", old.placement, t.placement],
|
||||
["정본", old.managed_by ?? null, t.managed_by ?? null],
|
||||
["군 글자", old.group, t.group],
|
||||
["본보기", baseOf(before, old.type_id), baseOf(doc, old.type_id)],
|
||||
];
|
||||
for (const [what, a, b] of fixed) if (a !== b) out.push(`「${name}」 ${what} — 만든 뒤 고정`);
|
||||
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
|
||||
@@ -195,5 +223,9 @@ export function catalogProblems(doc: CheckDef, before: CheckDef | null): string[
|
||||
const known = new Set((doc.types ?? []).map((t) => t.type_id));
|
||||
const shape = shapeErrors(doc, known);
|
||||
if (shape.length) return shape;
|
||||
return before ? [...rowErrors(doc, before), ...fixedErrors(doc, before)] : [];
|
||||
const templates = templateErrors(doc);
|
||||
return [
|
||||
...templates,
|
||||
...(before ? [...rowErrors(doc, before), ...fixedErrors(doc, before)] : []),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -58,7 +58,11 @@ export interface StructDef {
|
||||
동작: Record<string, string>;
|
||||
types: TypeDef[];
|
||||
/** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */
|
||||
종류: Record<string, { 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string> }>;
|
||||
/** 본보기 = 복사한 종류의 원본 키(323-7 · 만든 뒤 고정 · 처음값 · 손코딩 동작을 이어받음). */
|
||||
종류: Record<
|
||||
string,
|
||||
{ 기준측점?: boolean; 동작?: string[]; 이름?: Record<string, string>; 본보기?: string }
|
||||
>;
|
||||
표시: Record<string, Record<string, false>>;
|
||||
그룹: Record<string, GroupDef[]>;
|
||||
배치: Record<string, Record<string, Spot>>;
|
||||
|
||||
@@ -66,7 +66,9 @@ def _type_errors(doc: dict[str, Any]) -> tuple[list[str], dict[str, dict[str, An
|
||||
groups = {}
|
||||
for row in doc.get("types") if isinstance(doc.get("types"), list) else []:
|
||||
try:
|
||||
t = schema.StructureType.model_validate(row)
|
||||
t = schema.StructureType.model_validate(
|
||||
schema.without_retired(row) if isinstance(row, dict) else row
|
||||
)
|
||||
except ValidationError as e:
|
||||
errors.append(
|
||||
f"종류 「{row.get('type_id') if isinstance(row, dict) else row}」 꼴 — {e}"
|
||||
@@ -111,8 +113,11 @@ def _kind_errors(doc: dict[str, Any], keys: dict[str, dict[str, Any]]) -> list[s
|
||||
continue
|
||||
if name == "이름" and _names_ok(value):
|
||||
continue
|
||||
if name == "본보기" and isinstance(value, str) and value in keys:
|
||||
continue # 복사한 종류의 원본(323-7) — 같은 군 등은 `_template_errors`
|
||||
errors.append(
|
||||
f"종류 덧칸 「{tid}.{name}」 이 틀림(기준측점 참거짓 · 동작 = 동작 이름들 · 이름 = 글 묶음)"
|
||||
f"종류 덧칸 「{tid}.{name}」 이 틀림(기준측점 참거짓 · 동작 = 동작 이름들 · "
|
||||
"이름 = 글 묶음 · 본보기 = 있는 종류 키)"
|
||||
)
|
||||
return errors
|
||||
|
||||
@@ -221,6 +226,13 @@ def save(doc: Any, version: Any) -> dict[str, Any]:
|
||||
"""정의 통째로 고쳐 씀(시스템 관리자) — 돌려주는 것 = 새 정의."""
|
||||
if not isinstance(doc, dict):
|
||||
raise store.StoreError(400, "정의는 묶음이어야 함")
|
||||
# 옛 칸(drawing_views)은 [저장] 때 빠짐(323-7 · 아무도 안 읽음)
|
||||
types = doc.get("types")
|
||||
if isinstance(types, list):
|
||||
doc = {
|
||||
**doc,
|
||||
"types": [schema.without_retired(t) if isinstance(t, dict) else t for t in types],
|
||||
}
|
||||
return _commit(lambda _: dict(doc), version)
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ const TEXT = {
|
||||
GroupName: ["그룹 이름", "Group name"],
|
||||
GroupNew: ["새 그룹 {value}", "New group {value}"],
|
||||
GroupUp: ["위로", "Up"],
|
||||
AddKind: ["종류 더하기", "Add type"],
|
||||
AddKindNote: [
|
||||
"본보기 종류를 그대로 복사한 새 종류(이름 「본보기 2」) — 옵션 · 그룹 · 반영 · 그리는 법이 같음 · 이름 · 반영은 고쳐서 [저장]",
|
||||
"New type copied from the template (name “template 2”) — same options, groups, reflect and drawing; edit name / reflect, then [Save]",
|
||||
],
|
||||
GroupDown: ["아래로", "Down"],
|
||||
GroupDel: ["지우기", "Delete"],
|
||||
GroupDelAsk: [
|
||||
|
||||
@@ -66,13 +66,13 @@ PIPE_FACILITY_FORD_BRIDGE = "ford_bridge" # 세월교
|
||||
# 독립 기슭막이(2026-08-28 사용자) — 배관 없이 성토 사면에 세우는 벽. 배관 세트 경로를
|
||||
# 그대로 태우되 관을 숨긴다(hidden_pipe). 수량은 관 정보를 빼고 벽만 센다.
|
||||
PIPE_FACILITY_REVET = "revetment" # 독립 기슭막이(관 숨김)
|
||||
_KNOWN_FACILITIES = (
|
||||
PIPE_FACILITY_PIPE,
|
||||
PIPE_FACILITY_BOX,
|
||||
PIPE_FACILITY_FORD_PAVEMENT,
|
||||
PIPE_FACILITY_FORD_BRIDGE,
|
||||
PIPE_FACILITY_REVET,
|
||||
)
|
||||
|
||||
|
||||
def _known_facility(facility: str) -> bool:
|
||||
"""관 지점 정본 종류인가 = 구조물 표 정본(323-7 · 옛 손 목록 걷음 · 모르는 종류 = 배관)."""
|
||||
from common_util.common_util_structure_catalog import catalog_row
|
||||
|
||||
return (catalog_row(facility) or {}).get("owner") == "pipe_points"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -355,7 +355,7 @@ def parse_pipe_points(values: Any) -> list[PipePoint]:
|
||||
PipePoint(
|
||||
chainage_m=float(chainage),
|
||||
source=source if source in _KNOWN_SOURCES else PIPE_SOURCE_USER,
|
||||
facility=facility if facility in _KNOWN_FACILITIES else PIPE_FACILITY_PIPE,
|
||||
facility=facility if _known_facility(facility) else PIPE_FACILITY_PIPE,
|
||||
start_m=start,
|
||||
end_m=end,
|
||||
x=float(raw_x) if isinstance(raw_x, (int, float)) else None,
|
||||
|
||||
@@ -41,7 +41,8 @@ def resolve_catalog(
|
||||
rows: dict[str, dict[str, Any]] = {}
|
||||
for order, item in enumerate(types):
|
||||
type_id = item["type_id"]
|
||||
base = start.get(type_id) or {}
|
||||
root = ((definition.get("종류") or {}).get(type_id) or {}).get("본보기") or type_id
|
||||
base = start.get(root) or {} # 복사한 종류 = 본보기 처음값(323-7)
|
||||
mine = reflect.get(type_id) or {}
|
||||
|
||||
def part(branch: str, base=base, mine=mine) -> str:
|
||||
@@ -72,6 +73,7 @@ def resolve_catalog(
|
||||
"shapeOpts": {**(base.get("형상칸") or {}), **(mine.get("형상칸") or {})},
|
||||
"model3d": part("3D"),
|
||||
"color": (item.get("style") or {}).get("color") or None,
|
||||
"base": root,
|
||||
}
|
||||
return rows
|
||||
|
||||
@@ -94,6 +96,12 @@ def catalog_row(type_id: str) -> dict[str, Any] | None:
|
||||
return catalog().get(type_id)
|
||||
|
||||
|
||||
def base_type(type_id: str) -> str:
|
||||
"""손코딩 동작 · 명세를 고를 종류 키 = 본보기(복사한 종류 · 323-7) — TS `baseType` 짝."""
|
||||
row = catalog_row(type_id)
|
||||
return row["base"] if row else type_id
|
||||
|
||||
|
||||
def type_ids_with(**match: str) -> set[str]:
|
||||
"""표에서 칸 값이 맞는 종류들 — 예 `type_ids_with(shape="pavement")`."""
|
||||
return {key for key, row in catalog().items() if all(row[k] == v for k, v in match.items())}
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface CatalogRow {
|
||||
/** 3D 부품 — none · pipe · box · ford · chute · wall · surface. */
|
||||
model3d: string;
|
||||
color: string | null;
|
||||
/** 본보기 종류 키(323-7 · M02 종류 더하기) — 처음값 · 손코딩 동작을 이 종류에서 이어받음 · 원본이면 자기. */
|
||||
base: string;
|
||||
}
|
||||
|
||||
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
|
||||
@@ -57,6 +59,8 @@ export interface CatalogDefinition {
|
||||
}>;
|
||||
반영?: Record<string, Reflect>;
|
||||
부품?: Record<string, boolean>;
|
||||
/** 종류 덧칸 — 본보기(복사한 종류의 원본 키 · 323-7). */
|
||||
종류?: Record<string, { 본보기?: string }>;
|
||||
}
|
||||
|
||||
interface Parts {
|
||||
@@ -91,7 +95,8 @@ export function resolveCatalog(
|
||||
const rows = new Map<string, CatalogRow>();
|
||||
types.forEach((item, typeOrder) => {
|
||||
const id = item.type_id;
|
||||
const base = start[id] ?? {};
|
||||
const root = definition?.종류?.[id]?.본보기 ?? id; // 복사한 종류 = 본보기 처음값(323-7)
|
||||
const base = start[root] ?? {};
|
||||
const mine = definition?.반영?.[id] ?? {};
|
||||
const part = (branch: Branch): string => {
|
||||
const value = mine[branch] ?? base[branch] ?? OFF[branch];
|
||||
@@ -122,6 +127,7 @@ export function resolveCatalog(
|
||||
shapeOpts: { ...(base.형상칸 ?? {}), ...(mine.형상칸 ?? {}) },
|
||||
model3d: part("3D"),
|
||||
color: ("style" in item && item.style?.color) || null,
|
||||
base: root,
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
@@ -147,6 +153,9 @@ export function useCatalogDefinition(definition: CatalogDefinition | null): void
|
||||
/** 종류 한 줄 — 표에 없는 종류면 undefined. */
|
||||
export const catalogRow = (typeId: string): CatalogRow | undefined => current.get(typeId);
|
||||
|
||||
/** 손코딩 동작 · 명세를 고를 종류 키 = 본보기(복사한 종류 · 323-7) · 원본이면 자기 · 파이썬 `base_type` 짝. */
|
||||
export const baseType = (typeId: string): string => current.get(typeId)?.base ?? typeId;
|
||||
|
||||
/** 고르기 목록에 보일 종류인가 — B05 = both · B06 = both + b06(표에 없는 종류는 보임). */
|
||||
export const listPick =
|
||||
(page: "b05" | "b06") =>
|
||||
|
||||
@@ -13,10 +13,12 @@ import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_structure_station import EDGE_TOLERANCE_M
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — C군 벽(`common_util_structure_walls`)과 같은 값.
|
||||
_EDGE_TOLERANCE_M = 0.02
|
||||
# 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 같은 자리 거리 한 곳(323-7)
|
||||
_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M
|
||||
|
||||
|
||||
def _num(value: Any) -> float | None:
|
||||
|
||||
@@ -72,8 +72,15 @@ def snap_candidates(
|
||||
|
||||
# ── 측점 계획 한 벌(322-3 · TS `planStations` 짝 · 거울 = test_322_3_plan_mirror.py) ──
|
||||
|
||||
#: 같은 측점으로 볼 거리(m) — TS `SAME_STATION_M` 짝
|
||||
# ── 같은 자리 거리 한 곳(323-7 · 파이썬은 여기서 가져다 씀 · TS 짝은 이름이 같은 상수) ──
|
||||
#: 같은 측점으로 볼 거리(m) — 측점 계획 · 반올림 차이만 · TS `SAME_STATION_M` 짝
|
||||
SAME_STATION_M = 0.005
|
||||
#: 구간 끝 · 관 세트가 걸치는 측점을 같은 자리로 볼 폭(m) — 정본이 누가거리를 0.01 m 로 끊어 씀 ·
|
||||
#: TS `EDGE_TOLERANCE_M`(벽 · 포장 · 맹암거) · `CHAINAGE_TOLERANCE_M`(관 세트) 짝
|
||||
EDGE_TOLERANCE_M = 0.02
|
||||
#: 관 · 구조물 자리와 측점 · 고름을 같은 자리로 볼 폭(m) — 정수 m 기준 측점 · 옛 소수 자료 · TS
|
||||
#: `design_selection.PIPE_MATCH_M` 짝
|
||||
PIPE_MATCH_M = 0.51
|
||||
|
||||
|
||||
def _empty(value: object) -> bool:
|
||||
|
||||
@@ -27,6 +27,11 @@ import { cardTitle, catalogRow, shapeNeeds } from "./common_util_structure_catal
|
||||
|
||||
const EPS = 1e-6;
|
||||
|
||||
/* 같은 자리 거리(323-7 · 뜻마다 하나 · 파이썬은 `common_util_structure_station.py` 한 곳에서 가져다 씀):
|
||||
* SAME_STATION_M 0.005 = 같은 측점(반올림 차이) — 여기
|
||||
* EDGE_TOLERANCE_M · CHAINAGE_TOLERANCE_M 0.02 = 구간 끝 · 관 세트가 걸치는 측점(정본 0.01 m 끊음) — 벽 ·
|
||||
* 포장 · 맹암거 · 관 세트 모듈(거울 시험이 tsc ESM 으로 따로 엮어 각자 둠 · 값은 파이썬 `EDGE_TOLERANCE_M`)
|
||||
* PIPE_MATCH_M 0.51 = 관 · 구조물 자리와 측점 · 고름(정수 m · 옛 소수) — `design_selection` */
|
||||
/** 같은 측점으로 볼 거리(m) — 서버 측점 만들기의 격자 붙임 폭(`Engine_Sections_Core`)과 같음. */
|
||||
export const SAME_STATION_M = 0.005;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
// `.js` = 짝 거울 시험이 tsc → Node ESM 으로 이 파일만 엮음(확장자 있어야 찾음 · vite 는 .ts 로 풂)
|
||||
import { catalogRow } from "./common_util_structure_catalog.js";
|
||||
import { baseType, catalogRow } from "./common_util_structure_catalog.js";
|
||||
|
||||
/** 구조물 정본 한 건(필요한 칸만). `structures.json` 과 같은 이름을 쓴다. */
|
||||
export interface WallStructureInput {
|
||||
@@ -89,7 +89,11 @@ export function tierExtras(typeId: string, options: Record<string, unknown>): Wa
|
||||
),
|
||||
].sort((a, b) => a - b);
|
||||
return ns.map((n) => ({
|
||||
form: typeId === "retaining_wall" ? "콘크리트" : (options[`tier_${n}_form`] as string) || null,
|
||||
// 옹벽(본보기 포함 · 323-7) 추가 단 = 콘크리트
|
||||
form:
|
||||
baseType(typeId) === "retaining_wall"
|
||||
? "콘크리트"
|
||||
: (options[`tier_${n}_form`] as string) || null,
|
||||
height_m: num(options[`tier_${n}_height`]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -49,7 +49,17 @@ const { addsAtOnce, finishAdd } = loadTs(
|
||||
const { loadForm } = loadTs(path.join(B05, "B05_Profile_UI_Structures_Panel_Commit.ts"));
|
||||
|
||||
const entry = (required, value) => ({ required, isEmpty: () => value === "" });
|
||||
const type = (type_id, managed_by = null) => ({ type_id, group: "C", managed_by });
|
||||
// 동작 = 정의 종류 덧칸(앱의 `withDefinition` 과 같음 · 323-7 종류 키 대신 동작 이름)
|
||||
const DEFINED = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const behaviors = (typeId) => DEFINED.종류?.[typeId]?.동작 ?? [];
|
||||
const type = (type_id, managed_by = null) => ({
|
||||
type_id,
|
||||
group: "C",
|
||||
managed_by,
|
||||
behaviors: behaviors(type_id),
|
||||
});
|
||||
|
||||
/** 목록 알림 → 페이지가 임시 id 를 붙여 패널 배열 안을 갈아 채우는 것(`withLocalIds` · `setStructures`). */
|
||||
function fakeCommit(structures, adds) {
|
||||
|
||||
@@ -43,7 +43,16 @@ const { fordHeightM } = loadTs(
|
||||
path.join(ROOT, "A00_Common", "design", "design_structure_ford_calc.ts"),
|
||||
);
|
||||
|
||||
const type = (type_id) => ({ type_id, group: "A", managed_by: "pipe_points" });
|
||||
// 동작 = 정의 종류 덧칸(앱의 `withDefinition` 과 같음 · 323-7 종류 키 대신 동작 이름)
|
||||
const DEFINED = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const type = (type_id) => ({
|
||||
type_id,
|
||||
group: "A",
|
||||
managed_by: "pipe_points",
|
||||
behaviors: DEFINED.종류?.[type_id]?.동작 ?? [],
|
||||
});
|
||||
|
||||
/** 우클릭 → 바로 추가 — 관 지점 정본에 한 번 넘기고 그 측점 관을 고름 · 알림 한 줄. */
|
||||
async function rightClick(typeId) {
|
||||
@@ -116,7 +125,14 @@ function respond(points, basins, pending) {
|
||||
once: {
|
||||
fordBridge: addsAtOnce(type("ford_bridge"), []),
|
||||
fordPavement: addsAtOnce(type("ford_pavement"), []),
|
||||
pavement: addsAtOnce({ type_id: "pavement_concrete", managed_by: null }, []),
|
||||
pavement: addsAtOnce(
|
||||
{
|
||||
type_id: "pavement_concrete",
|
||||
managed_by: null,
|
||||
behaviors: DEFINED.종류?.pavement_concrete?.동작 ?? [],
|
||||
},
|
||||
[],
|
||||
),
|
||||
},
|
||||
bridge: await rightClick("ford_bridge"),
|
||||
paveRc: await rightClick("ford_pavement"),
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
@@ -18,6 +19,7 @@ function loadTs(file) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG; // 구조물 표(본보기 · 323-7)
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/* PLAN 323-7 종류 더하기 헬퍼 — 진짜 `M02_SO_Def_Add.ts`(본보기 복사) · 구조물 표 · 측점 계획 · 형상 얹기 · 벽 단 ·
|
||||
* 명세 고르기 · M02 검사를 그 자리에서 트랜스파일해 Node 로 돌림(마스터는 읽기만 · 사본에서 더함) · 결과 = JSON.
|
||||
* 짝 = `test_323_7_new_type.py`(파이썬 검사 · 표 · 벽 제원이 같은 사본을 읽음).
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const cache = new Map();
|
||||
function load(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
if (full.endsWith(".json")) return JSON.parse(fs.readFileSync(full, "utf8"));
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const need = (spec) => {
|
||||
if (spec.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} });
|
||||
const base = path.join(path.dirname(full), spec);
|
||||
const hit = [`${base}.ts`, base.replace(/\.js$/, ".ts"), base].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
);
|
||||
if (!hit) throw new Error(`모름: ${spec} (${full})`);
|
||||
return load(hit);
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, need);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const C = (name) => path.join(ROOT, "common_util", `${name}.ts`);
|
||||
const M = (name) => path.join(ROOT, "M02_MasterTemplete", "StructOptions", `${name}.ts`);
|
||||
const master = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
);
|
||||
const def = JSON.parse(JSON.stringify(master));
|
||||
const add = load(M("M02_SO_Def_Add"));
|
||||
const catalog = load(C("common_util_structure_catalog"));
|
||||
const station = load(C("common_util_structure_station"));
|
||||
const attach = load(C("common_util_structure_attach"));
|
||||
const walls = load(C("common_util_structure_walls"));
|
||||
const pipeSpecs = load(path.join(ROOT, "A00_Common", "design", "design_structure_pipe.ts"));
|
||||
const check = load(M("M02_SO_Def_Check"));
|
||||
|
||||
const out = {};
|
||||
out.templates = add.templates(def).map(([key]) => key);
|
||||
out.pipeRefused = add.addFromTemplate(def, "pipe");
|
||||
const wall = add.addFromTemplate(def, "retaining_wall");
|
||||
const pave = add.addFromTemplate(def, "pavement_concrete");
|
||||
out.keys = [wall, pave];
|
||||
const types = def.types.map((t) => ({ managed_by: null, enabled: true, ...t }));
|
||||
catalog.useCatalogDefinition({ ...def, types });
|
||||
const BRANCHES = [
|
||||
"list",
|
||||
"station",
|
||||
"title",
|
||||
"shape",
|
||||
"shapeOpts",
|
||||
"model3d",
|
||||
"placement",
|
||||
"owner",
|
||||
];
|
||||
const pick = (row) => Object.fromEntries(BRANCHES.map((k) => [k, row[k]]));
|
||||
out.rows = {
|
||||
wall: [pick(catalog.catalogRow("retaining_wall")), pick(catalog.catalogRow(wall))],
|
||||
pave: [pick(catalog.catalogRow("pavement_concrete")), pick(catalog.catalogRow(pave))],
|
||||
};
|
||||
out.base = [catalog.baseType(wall), catalog.baseType("retaining_wall")];
|
||||
out.names = [
|
||||
types.find((t) => t.type_id === wall).name,
|
||||
types.find((t) => t.type_id === pave).name,
|
||||
];
|
||||
const structure = {
|
||||
structure_id: "n1",
|
||||
type_id: wall,
|
||||
placement: "interval",
|
||||
start_m: 40,
|
||||
chainage_m: 50,
|
||||
end_m: 60,
|
||||
options: { height_m: 2, tier_1_height: 1 },
|
||||
};
|
||||
out.plan = station.planStations([], [structure], types).map((s) => [s.chainage_m, s.title]);
|
||||
const sections = [40, 50, 60].map((at) => ({ chainage_m: at }));
|
||||
attach.attachShapes(sections, [], [structure], types);
|
||||
out.attached = sections.map((s) => [s.revetment?.type_id ?? null, s.revetment?.name ?? null]);
|
||||
out.tier = walls.tierExtras(wall, { tier_1_height: 1 });
|
||||
out.spec =
|
||||
!!pipeSpecs.specFor(pave) && pipeSpecs.specFor(pave) === pipeSpecs.specFor("pavement_concrete");
|
||||
out.problems = check.catalogProblems(def, master);
|
||||
// 본보기 바꿈 · 복사본의 복사 = 거절(만든 뒤 고정 · 원본만)
|
||||
const bad = JSON.parse(JSON.stringify(def));
|
||||
bad.종류[wall].본보기 = "soil_guard";
|
||||
out.badBase = check.catalogProblems(bad, def).length > 0;
|
||||
const chain = JSON.parse(JSON.stringify(def));
|
||||
chain.종류[pave].본보기 = wall;
|
||||
out.chain = check.catalogProblems(chain, null).length > 0;
|
||||
out.def = def;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
@@ -19,6 +20,7 @@ function loadTs(file) {
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG; // 구조물 표(본보기 · 323-7)
|
||||
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
|
||||
return require(request);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 정의는 진짜 `구조물정의.json` 을 읽어 메모리 사본만 고침(파일 쓰기 없음). */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1)
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
@@ -16,9 +17,11 @@ const load = (...parts) => {
|
||||
}).outputText;
|
||||
const box = (cache[file] = { exports: {} });
|
||||
const near = (id) =>
|
||||
id.startsWith(".")
|
||||
? load(path.relative(ROOT, path.resolve(path.dirname(file), `${id}.ts`)))
|
||||
: require(id);
|
||||
CATALOG.isCatalogRequest(id) // 구조물 표(본보기 · 323-7)
|
||||
? CATALOG
|
||||
: id.startsWith(".")
|
||||
? load(path.relative(ROOT, path.resolve(path.dirname(file), `${id}.ts`)))
|
||||
: require(id);
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
@@ -128,7 +128,8 @@ def test_횡단_형상_단_수_단마다_형태_높이(out):
|
||||
|
||||
def test_배선():
|
||||
spec = _read("B05_Profile", "B05_Profile_UI_Structures_Spec.ts")
|
||||
assert "STRUCTURE_SPECS[type.type_id] ?? ADDON_SPECS[type.type_id]" in spec
|
||||
# 명세 · 덧붙이 명세 = 본보기 키로(323-7 · 복사한 종류도 같은 상자)
|
||||
assert "specFor(type.type_id) ?? ADDON_SPECS[baseType(type.type_id)]" in spec
|
||||
walls = _read("common_util", "common_util_structure_walls.ts")
|
||||
assert "파이썬 `tier_extras` 짝" in walls
|
||||
engine = _read("B06_Section", "B06_Section_Engine_Structures_Wall.py")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""PLAN 322-0 ② — 「기타」 유령 길 막기: 종류 이름을 바꾼 뒤에도 옛 이름 측점을 구조물로 되옮기지 않음.
|
||||
"""PLAN 322-0 ② → 323-7 — 「기타」 유령 길 막기: 종류 이름을 바꾼 뒤에도 옛 이름 측점을 안 되옮김.
|
||||
|
||||
정의 사본(임시 폴더 · 마스터 안 고침)에서 종류 이름을 바꾸고 앞 판을 `구조물정의_이력/판_N.json` 으로 둠 →
|
||||
옛 이름 · 새 이름 측점 모두 이관 0건. 옛 꼴 자유 글(기성막이 · 대피로 폭 · 임의 이름)은 종전대로 옮김.
|
||||
정의 사본(임시 폴더 · 마스터 안 고침)에서 종류 이름을 바꿈 → 지금 관 · 구조물이 심는 자리(측점
|
||||
계획)의 측점은 옛 이름 · 새 이름 모두 이관 0건(323-7 · 자리로 가름 · 지난 판 이름 되짚기 걷음).
|
||||
옛 꼴 자유 글(기성막이 · 대피로 폭 · 임의 이름)은 심는 자리가 아니면 종전대로 옮김.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -37,23 +38,25 @@ def renamed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict:
|
||||
return old
|
||||
|
||||
|
||||
def _moved(label: str) -> int:
|
||||
plan = migrate_irregular_stations([{"chainage_m": 120.0, "structure": label}])
|
||||
def _moved(label: str, planted: tuple[float, ...] = ()) -> int:
|
||||
plan = migrate_irregular_stations([{"chainage_m": 120.0, "structure": label}], planted)
|
||||
return len(plan.structures) + len(plan.pipe_facilities)
|
||||
|
||||
|
||||
def test_old_and_new_names_are_not_migrated(renamed: dict) -> None:
|
||||
def test_old_and_new_names_at_planted_spots_are_not_migrated(renamed: dict) -> None:
|
||||
# 323-7 — 자리로 가름(지금 관 · 구조물이 심는 자리) · 이름을 바꿔도 자리는 그대로
|
||||
assert schema.structure_type_map()["box_culvert"].name == "박스암거" # 사본을 읽음
|
||||
for type_id, name in RENAMED.items():
|
||||
old_name = next(t["name"] for t in renamed["types"] if t["type_id"] == type_id)
|
||||
assert _moved(old_name) == 0, old_name
|
||||
assert _moved(name) == 0, name
|
||||
assert _moved(old_name, (120.0,)) == 0, old_name
|
||||
assert _moved(name, (120.0,)) == 0, name
|
||||
assert _moved(name) == 0, name # 지금 판 이름은 자리 없이도
|
||||
|
||||
|
||||
def test_every_type_name_of_any_version_is_blocked(renamed: dict) -> None:
|
||||
names = {t["name"] for t in renamed["types"]} | set(RENAMED.values())
|
||||
assert names <= schema.all_type_names()
|
||||
assert [name for name in sorted(names) if _moved(name)] == []
|
||||
def test_history_names_are_not_read(renamed: dict) -> None:
|
||||
# 옛 이름 되짚기 걷음(323-7) — 자리를 못 받은 옛 이름 = 옛 자유 글처럼 「기타」
|
||||
assert not hasattr(schema, "all_type_names")
|
||||
assert _moved("노출형 횡단수로") == 1
|
||||
|
||||
|
||||
def test_legacy_free_text_still_migrates(renamed: dict) -> None:
|
||||
@@ -62,12 +65,4 @@ def test_legacy_free_text_still_migrates(renamed: dict) -> None:
|
||||
assert [s.type_id for s in plan.structures] == ["refuge"]
|
||||
plan = migrate_irregular_stations([{"chainage_m": 15.0, "structure": "돌망태 보강"}])
|
||||
assert [(s.type_id, s.options["name"]) for s in plan.structures] == [("etc", "돌망태 보강")]
|
||||
|
||||
|
||||
def test_master_history_names_are_read() -> None:
|
||||
"""실제 마스터 이력(판_1 ~)의 이름도 모음 — 이력 파일은 읽기만."""
|
||||
history = MASTER.parent / schema.HISTORY_DIRNAME
|
||||
names = set()
|
||||
for path in history.glob("판_*.json"):
|
||||
names |= {t["name"] for t in json.loads(path.read_text(encoding="utf-8"))["types"]}
|
||||
assert names <= schema.all_type_names()
|
||||
assert _moved("돌망태 보강", (120.004,)) == 0 # 심는 자리면 글이 무엇이든 안 옮김
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""PLAN 323-7 종류 더하기 — 본보기 복사 한 종류가 일곱 갈래(목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D · 자리꼴)
|
||||
를 본보기 그대로 그림 · 손코딩 동작 · 명세는 본보기 키로(`baseType`) · 검사는 화면 = 서버.
|
||||
|
||||
마스터는 읽기만 — 사본(헬퍼가 더한 정의)을 임시 파일로 두고 서버 쪽 읽기를 그리로 돌림.
|
||||
TS 쪽 = `helper_323_7_new_type.cjs`(Node).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile import B05_Profile_Structures_Schema as schema
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
from B06_Section.B06_Section_Engine_Structures_Wall import wall_specs_from
|
||||
from common_util import common_util_structure_catalog as catalog
|
||||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parent.parent
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts() -> dict:
|
||||
run = subprocess.run(
|
||||
["node", str(HERE / "helper_323_7_new_type.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def copied(ts: dict, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict:
|
||||
path = tmp_path / "구조물정의.json"
|
||||
path.write_text(json.dumps(ts["def"], ensure_ascii=False), encoding="utf-8")
|
||||
monkeypatch.setattr(schema, "DEFINITION_PATH", str(path))
|
||||
monkeypatch.setattr(catalog, "DEFINITION_PATH", str(path), raising=False)
|
||||
return ts["def"]
|
||||
|
||||
|
||||
def test_only_structure_originals_are_templates(ts: dict) -> None:
|
||||
assert "pipe" not in ts["templates"] and "retaining_wall" in ts["templates"]
|
||||
assert ts["pipeRefused"] is None
|
||||
|
||||
|
||||
def test_seven_branches_follow_template(ts: dict) -> None:
|
||||
for name, (source, copy) in ts["rows"].items():
|
||||
assert copy == source, name
|
||||
assert ts["base"] == ["retaining_wall", "retaining_wall"]
|
||||
assert ts["names"] == ["옹벽 2", "콘크리트 포장 2"]
|
||||
|
||||
|
||||
def test_new_type_draws_like_template(ts: dict) -> None:
|
||||
wall = ts["keys"][0]
|
||||
assert ts["plan"] == [[40, "옹벽 2"], [50, "옹벽 2"], [60, "옹벽 2"]]
|
||||
assert ts["attached"] == [[wall, "옹벽 2"]] * 3
|
||||
assert ts["tier"] == [
|
||||
{"form": "콘크리트", "height_m": 1}
|
||||
] # 옹벽 추가 단 = 콘크리트(본보기 동작)
|
||||
assert ts["spec"] is True # 포장 명세 = 본보기 명세
|
||||
|
||||
|
||||
def test_check_screen_equals_server(ts: dict, copied: dict) -> None:
|
||||
master = json.loads(Path(definition.schema.DEFINITION_PATH).read_text(encoding="utf-8"))
|
||||
assert ts["problems"] == []
|
||||
real = json.loads(
|
||||
(ROOT / "resources" / "master_template" / "구조물정의.json").read_text("utf-8")
|
||||
)
|
||||
assert definition.check(copied, real) == []
|
||||
assert ts["badBase"] is True and ts["chain"] is True
|
||||
bad = json.loads(json.dumps(copied))
|
||||
bad["종류"][ts["keys"][0]]["본보기"] = "soil_guard"
|
||||
assert any("본보기" in e for e in definition.check(bad, copied))
|
||||
assert master["types"] # 사본을 읽음
|
||||
|
||||
|
||||
def test_server_reads_copy_like_template(ts: dict, copied: dict) -> None:
|
||||
wall = ts["keys"][0]
|
||||
rows = catalog.resolve_catalog(copied)
|
||||
keep = ("list", "station", "title", "shape", "shapeOpts", "model3d", "placement", "owner")
|
||||
assert {k: rows[wall][k] for k in keep} == {k: rows["retaining_wall"][k] for k in keep}
|
||||
assert rows[wall]["base"] == "retaining_wall"
|
||||
item = StructureInstance.model_validate(
|
||||
{
|
||||
"structure_id": "n1",
|
||||
"type_id": wall,
|
||||
"placement": "interval",
|
||||
"start_m": 40,
|
||||
"chainage_m": 50,
|
||||
"end_m": 60,
|
||||
"options": {"height_m": 2, "tier_1_height": 1},
|
||||
}
|
||||
)
|
||||
(spec,) = wall_specs_from([item])
|
||||
assert spec["name"] == "옹벽 2" and spec["extras"][0]["form"] == "콘크리트"
|
||||
@@ -110,9 +110,18 @@ def test_폼이_적는_칸이_모두_목록에_있다(merged) -> None:
|
||||
"""폼에 칸을 늘리고 목록에 안 넣으면 여기서 잡힘."""
|
||||
source = FORM_TS.read_text(encoding="utf-8")
|
||||
body = source[source.index("readOptions() {") :]
|
||||
branches = re.split(r'current === "(\w+)"', body)[1:]
|
||||
assert branches[::2] == ["pipe", "box_culvert", "ford_pavement", "ford_bridge", "revetment"]
|
||||
for facility, block in zip(branches[::2], branches[1::2]):
|
||||
# 폼 갈래 = 구조물 표 형상(323-7 · 종류 키 대신) → 관 지점 종류
|
||||
branches = re.split(r'shapeOf\(current\) === "(\w+)"', body)[1:]
|
||||
kinds = {
|
||||
"pipe_set": "pipe",
|
||||
"box": "box_culvert",
|
||||
"ford_pavement": "ford_pavement",
|
||||
"ford": "ford_bridge",
|
||||
"hidden_revet": "revetment",
|
||||
}
|
||||
assert branches[::2] == list(kinds)
|
||||
for shape, block in zip(branches[::2], branches[1::2]):
|
||||
facility = kinds[shape]
|
||||
written = set(re.findall(r'putNumber\(options, "(\w+)"', block))
|
||||
written |= set(re.findall(r"options\.(\w+) =", block))
|
||||
missing = written - set(merged["keys"][facility])
|
||||
|
||||
@@ -92,8 +92,11 @@ def test_화면_고침을_그대로_받아_판_더하기_1_지난_판_길(copy)
|
||||
assert res.status_code == 200, res.text
|
||||
got = res.json()
|
||||
assert got["판"] == doc["판"] + 1
|
||||
for key in ("구조물군", "종류", "배치", "표시", "그룹", "규칙", "types"):
|
||||
for key in ("구조물군", "종류", "배치", "표시", "그룹", "규칙"):
|
||||
assert got[key] == sent[key], key
|
||||
# 옛 칸(drawing_views)은 [저장] 때 빠짐(323-7 · 아무도 안 읽음) — 나머지는 그대로
|
||||
retired = schema.without_retired
|
||||
assert got["types"] == [retired(t) for t in sent["types"]]
|
||||
assert list(got["구조물군"]) == list(sent["구조물군"]) # 순서 그대로
|
||||
rows = admin.get(f"{URL}/history").json()
|
||||
assert [r["판"] for r in rows] == [doc["판"]] and len(rows[0]["저장일"]) == 16
|
||||
|
||||
Reference in New Issue
Block a user