diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index a31006619..daf7bb4cc 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -14,6 +14,7 @@ import { readState, writeState } from "../A00_Common/b_page_state"; import { noteSnapshotFailed } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item"; +import { useCatalogDefinition } from "../common_util/common_util_structure_catalog"; import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend"; import { type DefinedRule, @@ -249,6 +250,7 @@ export function fetchStructureTypes(includeDisabled = false): Promise { const defined = await definition; typesVersion = defined?.판 ?? null; + useCatalogDefinition({ types: payload.types, ...defined }); // 구조물 표(322-1 · 반영 · 부품) return withDefinition(payload.types, defined); }) .catch((error) => { diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index 7f8987fae..ae637bc1b 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -29,8 +29,6 @@ from B05_Profile.B05_Profile_Structures_Schema import ( structure_type_map, ) from common_util.common_util_drainage_pipes import ( - PIPE_FACILITY_PIPE, - PIPE_FACILITY_REVET, PipePoint, load_pipe_points_file, parse_pipe_points, @@ -43,14 +41,13 @@ from common_util.common_util_route_geometry import ( planned_route_initial_path, planned_route_working_path, ) +from common_util.common_util_structure_catalog import card_title, catalog, catalog_row from common_util.common_util_surface_sampler import build_surface_sampler from config.config_system import ( DRAINAGE_CACHE_DIRNAME, DRAINAGE_EDITS_DIRNAME, DRAINAGE_PIPE_POINTS_FILENAME, FOREST_ROAD_PROFILE_CRITERIA, - PIPE_DEFAULT_DIAMETER_MM, - PIPE_DEFAULT_TYPE, ) logger = logging.getLogger(__name__) @@ -207,14 +204,10 @@ def _load_pipe_points(project_root: Path, polyline: list[list[float]]) -> list[P def pipe_station_label(pipe: PipePoint, types: dict[str, Any]) -> str | None: - """관 지점 측점 라벨 — 배관 = 「관종 D관경」 · 그 밖(물넘이포장 포함 · 286-1) = 종류 이름.""" - if pipe.facility == PIPE_FACILITY_PIPE: - values = pipe.options or {} - kind = str(values.get("pipe_kind") or PIPE_DEFAULT_TYPE) - diameter = int(float(values.get("pipe_diameter_mm") or PIPE_DEFAULT_DIAMETER_MM)) - return f"{kind} D{diameter}" - definition = types.get(pipe.facility) - return definition.name if definition is not None else None + """관 지점 측점 라벨 = 표 카드 이름(322-1 · 배관 = 「관종 D관경」 · 빈칸 = 정의 기본값).""" + if pipe.facility not in types: + return None + return card_title(pipe.facility, pipe.options) def relabel_pipe_stations(project_root: Path, cross_sections: list[dict[str, Any]]) -> None: @@ -236,7 +229,7 @@ def relabel_pipe_stations(project_root: Path, cross_sections: list[dict[str, Any except (OSError, ValueError, AttributeError): return types = structure_type_map() - pipe_names = {definition.name for definition in types.values() if definition.managed_by} + pipe_names = {row["name"] for row in catalog().values() if row["owner"] == "pipe_points"} for section in cross_sections: label = section.get("structure") if not isinstance(label, str) or not ( @@ -277,10 +270,12 @@ def resolve_extra_stations( label = pipe_station_label(pipe, types) if label is None: continue - # 독립 기슭막이는 구간(시작~종료)이라 **시작·기준·종료** 세 측점을 심는다 — - # 길수록 횡단도가 여러 장 나온다(구 D군 구간형과 같은 규칙). 나머지 관 시설은 - # 기준 한 곳(관이 노선을 가로지르는 점형). - if pipe.facility == PIPE_FACILITY_REVET: + # 표 횡단도(322-1) — span3(독립 기슭막이) = **시작·기준·종료** 세 측점(길수록 횡단도가 + # 여러 장) · anchor(그 밖 관 시설) = 기준 한 곳(관이 노선을 가로지르는 점형) · none = 없음. + station = (catalog_row(pipe.facility) or {}).get("station", "anchor") + if station == "none": + continue + if station == "span3": marks = (pipe.start_m, pipe.chainage_m, pipe.end_m) else: marks = (pipe.chainage_m,) @@ -300,7 +295,7 @@ def resolve_extra_stations( continue if not is_station_planting_type(definition): continue - if definition.placement == "interval": + if catalog_row(structure.type_id)["station"] == "span3": marks = (structure.start_m, structure.chainage_m, structure.end_m) else: marks = (structure.anchor_m(),) diff --git a/B05_Profile/B05_Profile_Structures_Schema.py b/B05_Profile/B05_Profile_Structures_Schema.py index ff1460d09..8f4e42d95 100644 --- a/B05_Profile/B05_Profile_Structures_Schema.py +++ b/B05_Profile/B05_Profile_Structures_Schema.py @@ -141,20 +141,15 @@ def structure_type_map() -> dict[str, StructureType]: def is_station_planting_type(item: StructureType) -> bool: - """서버가 종단 정본에 **측점으로 심는** 타입인가 (단일 규칙, 2026-08-28). + """서버가 종단 정본에 **측점으로 심는** 타입인가 — 구조물 표 횡단도 부품이 있음(322-1). - - A군(횡단배수·계곡 통과 시설): 그 자리에 횡단도가 필요하다. - - D군 구간형(기슭막이 등): 구간 시작·기준·종료에 횡단도가 필요하다 — 길이가 길면 - 그만큼 여러 장이 나온다(사용자 확정). - - C군 구간형(사면안정 — 옹벽·흙막이·소단 등): D군 구간형과 같이 시작·기준·종료. - 안 심으면 측점 격자 사이에 놓인 벽이 횡단도에 안 얹힌다. - - 측점 생성(`resolve_extra_stations`)과 구조물 재이관 차단 - (`B05_Profile_Structures_Migration`)이 같은 규칙을 봐야 "기타" 고스트가 안 생긴다. + anchor(기준 한 장 · 횡단배수 · 관 지점 시설) · span3(시작 · 기준 · 끝 · 벽 · 소단 · 기슭막이 + — 길이가 길면 그만큼 여러 장). 옛 군 글자 규칙(A군 · C · D군 구간형)은 표 처음값으로 옮김. """ - if item.group == "A": - return True - return item.group in ("C", "D") and item.placement == "interval" + from common_util.common_util_structure_catalog import catalog_row + + row = catalog_row(item.type_id) + return row is not None and row["station"] != "none" def station_planting_labels() -> set[str]: diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts index 2a6b0c3fc..f0845d294 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures.ts @@ -54,6 +54,7 @@ import { computeRevetmentLayout } from "../B06_Section/B06_Section_UI_Cross_Reve import { withoutGuardedFill } from "../B06_Section/B06_Section_UI_Cross_Body_Guard"; import { chuteBands, fordPavementChute } from "./B05_Profile_UI_Corridor_Structures_Chute"; import { isChuteSegment } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch"; +import { builds, culvertBody } from "./B05_Profile_UI_Corridor_Structures_Pick"; /** 스윕 프레임 하나 — 노선 위 한 지점의 중심 XY·좌향 단위벡터와 종단 표고 오프셋. */ export interface StructureFrame { @@ -276,17 +277,25 @@ export function buildCorridorStructures( ): CorridorStructure[] { const solids: CorridorStructure[] = []; for (const section of crossSections) { - const layout = culvertLayoutOf(section); + // 몸체 고르기 = 그림 자료 열쇠의 주인 종류 표 3D 부품(322-1) + const layout = culvertBody(section) ? culvertLayoutOf(section) : null; // 흙막이가 선 쪽은 횡단과 같은 자리 함수로 — 측벽 걷고 관 · 바닥판이 흙막이까지(89-1) - const fordLayout = withoutGuardedFill(fordLayoutOf(section), layout); + const fordLayout = builds("ford_bridge", "ford") + ? withoutGuardedFill(fordLayoutOf(section), layout) + : null; // 독립 기슭막이(옛 D군 경로) — 측점에 배관/숨김 기슭막이 세트가 붙으면 그쪽(위 // culvertLayoutOf)이 그린다. 아직 이관 안 된 정본만 이 옛 경로로 스윕한다 // (2026-08-28 이관 이중그리기 방지 — 횡단도 가드와 같은 규칙). - const revetLayout = ownWallsDrawAt(section, culvertOwnerFor(section, crossSections)) - ? computeRevetmentLayout(section, section.design?.revet_adjust?.own) + const revetLayout = + ownWallsDrawAt(section, culvertOwnerFor(section, crossSections)) && + builds(section.revetment?.type_id, "wall") + ? computeRevetmentLayout(section, section.design?.revet_adjust?.own) + : null; + const paveChute = builds("ford_pavement", "chute") + ? fordPavementChute(section, crossSections) : null; - const paveChute = fordPavementChute(section, crossSections); - if (!layout && !fordLayout && !section.box && !revetLayout && !paveChute) continue; + const box = builds("box_culvert", "box") ? section.box : undefined; + if (!layout && !fordLayout && !box && !revetLayout && !paveChute) continue; const chainage = section.chainage_m; const stationFrame: StructureFrame = { cx: section.center_x, @@ -447,7 +456,7 @@ export function buildCorridorStructures( } }; - if (section.box) { + if (box) { solids.push(...boxSolids(section, stationFrame, boxAdjustOf(section))); } paveChute?.bands.forEach((band) => diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Pick.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Pick.ts new file mode 100644 index 000000000..2135085c0 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Pick.ts @@ -0,0 +1,21 @@ +/* ============================================================================= + * B05_Profile_UI_Corridor_Structures_Pick.ts + * 3D 몸체 고르기(322-1) — 그림 자료 열쇠(`culvert` · `box` · `ford` · `ford_pavement` · `revetment`)의 주인 + * 종류가 구조물 표 3D 부품으로 그 몸체를 세우는가. 지금 처음값 = 지금 결과 그대로(표가 끈 것만 빠짐). + * 쓰는 곳 = `B05_Profile_UI_Corridor_Structures.buildCorridorStructures`. + * ========================================================================== */ + +import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; +import { catalogRow } from "../common_util/common_util_structure_catalog"; + +/** 그림 자료의 주인 종류가 이 3D 몸체를 세우는가 — 표에 없는 종류면 세움. */ +export const builds = (typeId: string | null | undefined, body: string): boolean => + (catalogRow(typeId ?? "")?.model3d ?? body) === body; + +/** `culvert` 자리 주인 = 박스 · 세월교 흙막이(body) · 독립 기슭막이(관 숨김) · 배관. */ +export function culvertBody(section: CrossSection): boolean { + const spec = section.culvert; + if (!spec) return false; + if (spec.body) return builds(spec.body === "box" ? "box_culvert" : "ford_bridge", spec.body); + return spec.hidden_pipe ? builds("revetment", "wall") : builds("pipe", "pipe"); +} diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index e4158b73b..e9080f96d 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -36,6 +36,7 @@ import { FACILITY_NAMES } from "./B05_Profile_UI_Page_Helpers"; import { followedStation } from "./B05_Profile_UI_Profile_Alignment"; import { followSnapped } from "./B05_Profile_UI_Structures_Snap"; import { plantsStation } from "./B05_Profile_UI_Profile_Structures"; +import { listPick } from "../common_util/common_util_structure_catalog"; import { pipeStationMarks, SAME_STATION_M, @@ -352,7 +353,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { fetchStructureTypes(true), fetchStructures(deps.projectId), ]); - deps.panel().structures.setTypes(allTypes, (type) => type.enabled); + deps.panel().structures.setTypes(allTypes, listPick("b05")); // 표 목록 both(322-1) deps.profilePanel().setStructureTypes(allTypes); // A군 판정에 쓸 타입 정보 — 횡단배수만 그래프 세로선·틸팅 대상이다. structureTypeMap = new Map( diff --git a/B05_Profile/B05_Profile_UI_Structures_Define.ts b/B05_Profile/B05_Profile_UI_Structures_Define.ts index b3ef3f123..c964d5623 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define.ts @@ -216,7 +216,7 @@ export function withDefinition< }); } -/** 대피로(표 07 · PLAN 52-7) — 옛 대피 종류는 고르기 목록에서 빼고 대피로 + 상세로 읽음. +/** 대피로(표 07 · PLAN 52-7) — 옛 대피 종류는 대피로 + 상세로 읽음(고르기 목록 숨김 = 구조물 표 처음값 hidden · 322-1). * 옛 기록은 고치기 전까지 옛 종류 그대로(도면 · 집계 그대로) · 고치면 대피로로 저장 · 옛 칸 값은 남음. */ export const ROAD_TYPE = "evac_road"; export const ROAD_DETAILS: Readonly> = { @@ -225,9 +225,6 @@ export const ROAD_DETAILS: Readonly> = { turnaround: "차돌림곳", }; -/** 고르기 목록에 낼 종류인지 — 옛 대피 종류만 뺌. */ -export const pickable = (type: { type_id: string }): boolean => !ROAD_DETAILS[type.type_id]; - /** 옛 대피 종류 기록 → 대피로 + 상세(옛 값 그대로 · 상세만 채움). 다른 기록은 그대로. */ export function asRoad }>( record: T, diff --git a/B05_Profile/B05_Profile_UI_Structures_Marks.ts b/B05_Profile/B05_Profile_UI_Structures_Marks.ts index 42f64a4a4..acf44b1c5 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Marks.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Marks.ts @@ -24,6 +24,7 @@ import { etcName, numberDuplicateNames } from "./B05_Profile_UI_Structures_List" import { balloonLines, type ContainerCount } from "./B05_Profile_UI_Structures_Balloon"; import { stationLabel as formatStation } from "../common_util/common_util_station"; import { frameOf } from "../common_util/common_util_view_window"; +import { catalogRow } from "../common_util/common_util_structure_catalog"; /** 마크를 잡았다고 볼 여유(px) — 시작점에서 이만큼 **벗어나기 전에는** 이동으로 * 보지 않고 시각적으로도 움직이지 않는다. 예전 10px 누적 방식은 손떨림이 쌓여 @@ -174,7 +175,12 @@ export function buildStructureLane(options: StructureMarksOptions): HTMLElement // 놓였는지 안 보였다). 예전에는 고른 동안만 폈는데, 측구·맹암거처럼 길게 이어지는 // 시설은 「어디부터 어디까지인가」가 곧 그 시설의 내용이라 늘 보여야 한다. // 고른 것은 진하게, 나머지는 옅게 — 레인 높이(42px)는 그대로다. - if (structure.placement === "interval") { + // 띠 = 표 자리꼴 구간(322-1) · 관 지점 시설(물넘이포장 · 기슭막이)은 알약만(지금 그대로) + const listed = catalogRow(structure.type_id); + if ( + (listed?.placement ?? structure.placement) === "interval" && + listed?.owner !== "pipe_points" + ) { const band = document.createElement("div"); band.className = `b05-structure__band${selected ? " is-selected" : ""}`; const startPx = options.x(structure.start_m ?? 0); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 223e6b6fb..55dbc07e3 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -43,7 +43,7 @@ import { } from "./B05_Profile_UI_Structures_Panel_Commit"; import { renderStructureList } from "./B05_Profile_UI_Structures_List"; import { addsAtOnce, finishAdd } from "./B05_Profile_UI_Structures_Panel_AddAt"; -import { pickable, showsAnchor } from "./B05_Profile_UI_Structures_Define"; +import { showsAnchor } from "./B05_Profile_UI_Structures_Define"; import { hiddenOptionKeys } from "./B05_Profile_UI_Structures_Define_Form"; import { stationLabel as formatStation } from "../common_util/common_util_station"; import { PIPE_MATCH_M, resolveSelection } from "../A00_Common/design/design_selection"; @@ -352,7 +352,7 @@ export function createStructuresSection( * 초기·리셋 상태를 "안 고름"으로 드러낸다. */ function syncTypeOptions(keepTypeId?: string): void { const group = groupSelect.value; - const candidates = group ? types.filter((t) => t.group === group && pickable(t)) : []; + const candidates = group ? types.filter((t) => t.group === group) : []; typeSelect.replaceChildren(new Option("— 선택 —", ""), ...typeOptions(candidates, canPick)); typeSelect.value = keepTypeId && candidates.some((type) => type.type_id === keepTypeId) ? keepTypeId : ""; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index 71c974d11..73856b16f 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -21,7 +21,7 @@ import { 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 { pickable } from "./B05_Profile_UI_Structures_Define"; +import { catalogRow } from "../common_util/common_util_structure_catalog"; /** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다. * 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */ @@ -36,22 +36,6 @@ export const GROUP_LABELS: Record = { 호환: "기타", }; -/** 새로 놓을 때 고르기 목록(군 드롭다운 · 우클릭 메뉴)에서 뺄 군 — 종단배수·계류사방(표 07). - * 옛 값 · 도면 · 집계는 그대로이고 데이터도 지우지 않는다. */ -export const HIDDEN_PICK_GROUPS: ReadonlySet = new Set(["B", "D"]); - -/** 고르기 목록에서 뺄 종류 — 사면안정은 흙막이 · 옹벽 · 소단만(54-31). 옛 값은 그대로. */ -export const HIDDEN_PICK_TYPES: ReadonlySet = new Set([ - "masonry_wet", - "masonry_dry", - "boulder_masonry", -]); - -/** 새로 놓을 때 고르기 목록에서 가릴 종류인가 — 뺄 군이거나 뺄 종류. */ -export function isPickHidden(type: { group: string; type_id: string }): boolean { - return HIDDEN_PICK_GROUPS.has(type.group) || HIDDEN_PICK_TYPES.has(type.type_id); -} - /** 고르기 목록 항목 — 가릴 항목은 가림(옛 항목을 열 때는 값으로 그대로 고를 수 있음). */ export function pickOption(text: string, value: string, hidden: boolean): HTMLOptionElement { const option = new Option(text, value); @@ -60,9 +44,10 @@ export function pickOption(text: string, value: string, hidden: boolean): HTMLOp return option; } -/** 고르기 목록에서 가릴 종류인가 — 꺼진(pick) 종류 · 뺄 군 · 뺄 종류(드롭다운 · 메뉴 공용). */ +/** 고르기 목록에서 가릴 종류인가 — 페이지 거르기(`listPick` · 구조물 표 목록 · 322-1)가 고름. 옛 군 · 종류 + * 숨김(종단배수 · 계류사방 · 돌쌓기 셋 · 옛 대피 셋)은 표 처음값 hidden(드롭다운 · 메뉴 공용). */ const pickHidden = (type: StructureType, pick: (type: StructureType) => boolean): boolean => - !pick(type) || isPickHidden(type); + !pick(type); /** 고르기 목록 한 군 — 가린 군 · 종류도 담음(옛 항목을 열 때 값으로 고름). */ export interface PickGroup { @@ -73,20 +58,25 @@ export interface PickGroup { } /** 군 → 종류 고르기 목록 한 벌 — 패널 드롭다운과 우클릭 메뉴(종단 · 배수유역도 · 3D · B06)가 - * 같은 자료를 봄(129-1). 군 이름은 정의(48) 먼저 · 옛 대피 종류는 뺌 · 꺼진(pick) 종류 · 뺄 군 · - * 뺄 종류는 가림 · 보일 종류가 없는 군도 가림. */ + * 같은 자료를 봄(129-1). 군 이름은 정의(48) 먼저 · 군 차례 = 정의 구조물군 차례 · 군 안 = 종류 차례 · + * 페이지 거르기(pick)가 안 고른 종류는 가림 · 보일 종류가 없는 군도 가림. */ export function pickGroups( types: ReadonlyArray, pick: (type: StructureType) => boolean, ): PickGroup[] { const groups = new Map(types.map((t) => [t.group, t.group_name] as const)); - return [...groups].map(([group, name]) => { - const members = types - .filter((t) => t.group === group && pickable(t)) - .map((type) => ({ type, hidden: pickHidden(type, pick) })); - const hidden = members.every((member) => member.hidden); - return { group, label: name ?? GROUP_LABELS[group] ?? group, hidden, types: members }; - }); + const rank = (group: string): number => + catalogRow(types.find((t) => t.group === group)?.type_id ?? "")?.groupOrder ?? + Number.MAX_SAFE_INTEGER; + return [...groups] + .sort(([a], [b]) => rank(a) - rank(b)) + .map(([group, name]) => { + const members = types + .filter((t) => t.group === group) + .map((type) => ({ type, hidden: pickHidden(type, pick) })); + const hidden = members.every((member) => member.hidden); + return { group, label: name ?? GROUP_LABELS[group] ?? group, hidden, types: members }; + }); } /** 군 드롭다운 항목들 — `pickGroups` 그대로. */ diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index ffea678bb..a7477cb60 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -29,14 +29,8 @@ from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Structures_Schema import structure_type_map -from common_util.common_util_drainage_pipes import ( - PIPE_FACILITY_BOX, - PIPE_FACILITY_FORD_BRIDGE, - PIPE_FACILITY_FORD_PAVEMENT, - PIPE_FACILITY_PIPE, - PIPE_FACILITY_REVET, - parse_pipe_points, -) +from common_util.common_util_drainage_pipes import parse_pipe_points +from common_util.common_util_structure_catalog import catalog_row from config.config_system import ( DRAINAGE_CACHE_DIRNAME, DRAINAGE_EDITS_DIRNAME, @@ -612,18 +606,20 @@ def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]: return {} sets: dict[float, dict[str, Any]] = {} for point in parse_pipe_points(document.get("points")): - if point.facility == PIPE_FACILITY_PIPE: + # 그릴 세트 = 구조물 표 형상(322-1 · TS `buildCulvertSets` 짝) + shape = (catalog_row(point.facility) or {}).get("shape") + if shape == "pipe_set": if (point.options or {}).get("pipe_diameter_mm") in (None, ""): continue # 관경 안 고름 = 관 없음(54-39) spec = _culvert_set(point.options) - elif point.facility == PIPE_FACILITY_FORD_BRIDGE: + elif shape == "ford": spec = _ford_set(point.options) - elif point.facility == PIPE_FACILITY_BOX: + elif shape == "box": spec = _box_set(point.options) - elif point.facility == PIPE_FACILITY_FORD_PAVEMENT: + elif shape == "ford_pavement": # 구조물이 아니라 파인 노면이다 — 스펙 모양도 소비처도 다르다(2026-08-28). spec = _ford_pavement_set(point.options) - elif point.facility == PIPE_FACILITY_REVET: + elif shape == "hidden_revet": # 독립 기슭막이 — 배관 세트 모양이되 관을 숨긴다. `culvert` 키로 얹혀 배관 # 경로(컴퓨트·렌더·패널·연동·경사·3D·수량)를 그대로 탄다. spec = _revet_set(point.options) diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index 5ee2cd263..7fd1cfffc 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -28,14 +28,13 @@ from B06_Section.B06_Section_Engine_Design import ( stored_shoulder_extend, stored_tilt, ) +from common_util.common_util_structure_catalog import catalog_row from common_util.common_util_structure_face_role import structure_face_role #: 폭을 좁혀 가는 이분법 반복 수. TS 짝(`solveSpoilWidthM`)과 같은 값이다. _SOLVE_STEPS = 24 #: 상한을 재려고 한 번 크게 넣어 보는 폭(m). 실제로는 지반 샘플에서 잘린다. _MAX_PROBE_WIDTH_M = 1000.0 -#: 사토장 종류 이름 — 등록부 `spoil_bank`(현행 명칭 유용토운반작업장). -SPOIL_TYPE_ID = "spoil_bank" #: 「자동(성토 쪽)」 — 등록부 `side` 의 기본 선택지. C군 구조물과 같은 낱말이다. _SIDE_AUTO = "자동(성토 쪽)" _SIDE_WORDS = {"좌": "left", "우": "right"} @@ -108,7 +107,7 @@ def spoil_sites(structures: list[Any]) -> list[dict[str, Any]]: type_id = getattr(item, "type_id", None) or ( item.get("type_id") if isinstance(item, dict) else None ) - if str(type_id) != SPOIL_TYPE_ID: + if (catalog_row(str(type_id)) or {}).get("shape") != "spoil": # 표 형상(322-1) continue options = getattr(item, "options", None) if options is None and isinstance(item, dict): diff --git a/B06_Section/B06_Section_Engine_Structures_Wall.py b/B06_Section/B06_Section_Engine_Structures_Wall.py index 90649e6fc..2b7087c15 100644 --- a/B06_Section/B06_Section_Engine_Structures_Wall.py +++ b/B06_Section/B06_Section_Engine_Structures_Wall.py @@ -24,21 +24,21 @@ 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 logger = logging.getLogger(__name__) # 구간 경계 측점을 구간 안으로 볼 허용 오차(m) — 정본이 누가거리를 0.01m로 끊어 쓴다. _EDGE_TOLERANCE_M = 0.02 -# 구조물 종류 → 횡단 기하가 아는 **형태** 이름. 형태는 벽 안 무늬만 정하므로 -# 가장 가까운 것으로 잇는다. -_FORM_BY_TYPE = { - "masonry_wet": "돌쌓기(찰)", - "masonry_dry": "돌쌓기(메)", - "boulder_masonry": "돌쌓기(메)", - "retaining_wall": "콘크리트", - "soil_guard": "통나무·목재틀", -} + +def _wall_form(type_id: str) -> str | None: + """종류 → 횡단 기하가 아는 **형태** 이름 = 구조물 표 형상칸.form(322-1 · TS `formOf` 짝). + + 형태는 벽 안 무늬만 정하므로 가장 가까운 것으로 잇는다(폼에서 형태를 비웠을 때). + """ + form = ((catalog_row(type_id) or {}).get("shapeOpts") or {}).get("form") + return form if isinstance(form, str) else None # 좌측 폼 「추가 단」 상자 저장 키 머리(178-3) — `tier_{n}_form` · `tier_{n}_height`. @@ -71,17 +71,16 @@ BERM_TYPE_ID = "berm" def load_wall_structures(project_root: Path) -> list[dict[str, Any]]: - """구조물 정본에서 C군 벽(구간형) 목록을 읽는다. 실패하면 빈 목록.""" + """구조물 정본에서 벽(표 형상 wall · 322-1) 목록을 읽는다. 실패하면 빈 목록. + + 소단은 형상 berm — 사면을 계단으로 끊는 시설이라 벽 제원 자리에 안 얹음(계획서 3-9). + """ 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: + if definition is None or (catalog_row(structure.type_id) or {}).get("shape") != "wall": continue start, end = structure.start_m, structure.end_m if start is None or end is None: @@ -96,7 +95,7 @@ def load_wall_structures(project_root: Path) -> list[dict[str, Any]]: "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), + "form": options.get("form") or _wall_form(structure.type_id), "height_m": options.get("height_m"), # C군 폼에는 설치 측 칸이 없다 — 비워 두면 화면이 **성토가 나는 쪽**으로 # 세운다(`computeRevetmentLayout`). 사용자가 정하고 싶어지면 그때 칸을 낸다. diff --git a/B06_Section/B06_Section_Engine_Tilt.py b/B06_Section/B06_Section_Engine_Tilt.py index 3fa30b969..ef57abf16 100644 --- a/B06_Section/B06_Section_Engine_Tilt.py +++ b/B06_Section/B06_Section_Engine_Tilt.py @@ -17,10 +17,10 @@ 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 logger = logging.getLogger(__name__) -UNDERDRAIN_TYPE_ID = "cross_underdrain" _EDGE_TOLERANCE_M = 0.02 TiltRange = tuple[float, float, float | None] @@ -44,7 +44,7 @@ def load_underdrains(project_root: Path) -> list[dict[str, Any]]: return [] found: list[dict[str, Any]] = [] for structure in structures: - if structure.type_id != UNDERDRAIN_TYPE_ID: + if (catalog_row(structure.type_id) or {}).get("shape") != "underdrain": continue options = structure.options or {} height = _number(options.get("culvert_height_m")) diff --git a/B06_Section/B06_Section_Section_Drafts.ts b/B06_Section/B06_Section_Section_Drafts.ts index 6f43f57e4..c72eb4631 100644 --- a/B06_Section/B06_Section_Section_Drafts.ts +++ b/B06_Section/B06_Section_Section_Drafts.ts @@ -31,6 +31,7 @@ import { SAME_STATION_M, structureStationMarks, } from "../common_util/common_util_structure_station"; +import { cardTitle } from "../common_util/common_util_structure_catalog"; import { previewStructureSections, type CrossSection, @@ -126,22 +127,16 @@ function decorate( ): void { const registry = registryDefaults(types); const byId = new Map(types.map((type) => [type.type_id, type])); - const pipeLabel = (pipe: Pipes[number]): string | null => { - const facility = pipe.facility ?? "pipe"; - if (facility !== "pipe") return byId.get(facility)?.name ?? facility; - const values = { ...registry.pipe, ...pipe.options }; - return `${values.pipe_kind} D${Math.trunc(Number(values.pipe_diameter_mm))}`; - }; + // 카드 이름 = 표 카드 이름(322-1 · 배관 = 관종 D관경 · 빈칸 = 그 종류 정의 기본값 · 그 밖 = 종류 이름) const labels: Array<[number, string]> = [ ...pipes.flatMap((pipe) => { - const label = pipeLabel(pipe); - return label - ? pipeStationMarks(pipe).map((m): [number, string] => [m.chainage_m, label]) - : []; + const facility = pipe.facility ?? "pipe"; + const label = cardTitle(facility, pipe.options, registry[facility]); + return pipeStationMarks(pipe).map((m): [number, string] => [m.chainage_m, label]); }), ...structures.flatMap((structure) => { const type = byId.get(structure.type_id); - const label = type?.name ?? structure.type_id; + const label = cardTitle(structure.type_id, structure.options); return structureStationMarks(structure, type).map((m): [number, string] => [ m.chainage_m, label, diff --git a/B06_Section/B06_Section_Section_Store.ts b/B06_Section/B06_Section_Section_Store.ts index 6ec26a7ce..0767293a2 100644 --- a/B06_Section/B06_Section_Section_Store.ts +++ b/B06_Section/B06_Section_Section_Store.ts @@ -40,6 +40,7 @@ import { type PavementStructureInput, } from "@util/common_util_structure_pavement"; import { setGeneralStations } from "../common_util/common_util_structure_station"; +import { cardTitle, catalogRow } from "../common_util/common_util_structure_catalog"; import { attachUnderdrainSpecs, underdrainSpecsFrom, @@ -85,9 +86,11 @@ async function withDraftWalls( detail.cross_sections as unknown as Array>, underdrainSpecsFrom(drafts as unknown as UnderdrainInput[]), ); - const types = await fetchStructureTypes().catch(() => []); + // 표 형상(322-1) — 「사용 중」 거르기 없음(꺼진 종류도 그 그림대로 · 쇄석 포장층) + const types = await fetchStructureTypes(true).catch(() => []); + const shaped = (shape: string) => types.filter((t) => catalogRow(t.type_id)?.shape === shape); // 노면공(88) — 초안 범위로 포장 제원을 다시 얹음(짝 `common_util_structure_pavement`) - const paveTypes = new Set(types.filter((type) => type.group === "G").map((type) => type.type_id)); + const paveTypes = new Set(shaped("pavement").map((type) => type.type_id)); if (paveTypes.size) { for (const section of detail.cross_sections) delete section.pavement; attachPavementSpecs( @@ -95,15 +98,8 @@ async function withDraftWalls( pavementSpecsFrom(drafts as unknown as PavementStructureInput[], paveTypes), ); } - const names = new Map( - types - // 소단은 C군 구간형이지만 **벽이 아니다** — 사면을 계단으로 끊는 시설이라 - // 기슭막이 제원 자리에 얹으면 안 된다(계획서 3-9). - .filter( - (type) => type.group === "C" && type.placement === "interval" && type.type_id !== "berm", - ) - .map((type) => [type.type_id, type.name] as const), - ); + // 벽 = 표 형상 wall(소단은 형상 berm — 사면을 계단으로 끊는 시설이라 벽 자리에 안 얹음 · 계획서 3-9) + const names = new Map(shaped("wall").map((type) => [type.type_id, type.name] as const)); if (!names.size) return detail; for (const section of detail.cross_sections) { if (section.revetment) delete (section as { revetment?: unknown }).revetment; @@ -115,16 +111,17 @@ async function withDraftWalls( return withStructureAreas(detail); } -/** 관 지점 시설 → 횡단 자리 키(파이썬 `_SECTION_KEYS` · 배관 · 독립 기슭막이 = culvert). */ +/** 관 지점 시설 → 횡단 자리 키 = 표 형상(파이썬 `_SECTION_KEYS` · 배관 세트 · 독립 기슭막이 = culvert). */ const FACILITY_KEYS = ["culvert", "ford", "box", "ford_pavement"] as const; +const SHAPE_KEY: Record = { + box: "box", + ford: "ford", + ford_pavement: "ford_pavement", +}; +const shapeOf = (facility: string | null | undefined): string => + catalogRow(facility ?? "pipe")?.shape ?? "pipe_set"; const facilityKey = (facility: string): (typeof FACILITY_KEYS)[number] => - facility === "ford_bridge" - ? "ford" - : facility === "box_culvert" - ? "box" - : facility === "ford_pavement" - ? "ford_pavement" - : "culvert"; + SHAPE_KEY[shapeOf(facility)] ?? "culvert"; /** * 저장 전에 **종류를 바꾼 관**(배관 ↔ BOX암거 ↔ 세월교 ↔ 물넘이포장 · 54-4)을 상세에 얹는다. @@ -141,13 +138,14 @@ async function withDraftPipeKinds(detail: SectionDetailResponse, projectId: stri for (const section of sections) { const spec = section.ford_pavement as Record | undefined; const kept = (pipe: (typeof pipes)[number]) => - pipe.facility === "ford_pavement" && !!spec && near(spec, section, pipe.chainage_m); + shapeOf(pipe.facility) === "ford_pavement" && !!spec && near(spec, section, pipe.chainage_m); if (spec && !pipes.some(kept)) delete section.ford_pavement; } /** 그 관 자리에 얹힌 시설 키가 이 종류와 맞는가 — 얹힌 것이 없으면(새 관 · 옮긴 관) 대상 밖. */ const fits = (pipe: (typeof pipes)[number], key: string, spec: Record) => key === facilityKey(pipe.facility ?? "pipe") && - (key !== "culvert" || (spec.hidden_pipe === true) === (pipe.facility === "revetment")); + (key !== "culvert" || + (spec.hidden_pipe === true) === (shapeOf(pipe.facility) === "hidden_revet")); const stale = pipes.filter((pipe) => { const here = sections.flatMap((section) => FACILITY_KEYS.flatMap((key) => { @@ -156,18 +154,18 @@ async function withDraftPipeKinds(detail: SectionDetailResponse, projectId: stri }), ); // 얹힌 것이 없는 관(새 관 · 옮긴 관)은 미리 뜬 횡단이 맡음 — 물넘이포장만 걸친 기존 측점까지 여기서(314) - if (!here.length) return pipe.facility === "ford_pavement"; + if (!here.length) return shapeOf(pipe.facility) === "ford_pavement"; return !here.some(([key, spec]) => fits(pipe, key, spec)); }); if (!stale.length) return; - const types = await fetchStructureTypes().catch(() => []); + const types = await fetchStructureTypes(true).catch(() => []); const registry = registryDefaults(types); for (const section of sections) { for (const key of FACILITY_KEYS) { const spec = section[key] as Record | undefined; if (spec && stale.some((pipe) => near(spec, section, pipe.chainage_m))) delete section[key]; } - // 카드 제목 구조물 이름 — 서버 `resolve_extra_stations` 라벨 규칙(배관 = 관종 D관경 · 그 밖 = 종류 이름) + // 카드 제목 구조물 이름 = 표 카드 이름(서버 `pipe_station_label` 짝 · 배관 = 관종 D관경 · 그 밖 = 종류 이름) // 라벨 자리 = 이름 있는 측점 · 관 자리 그 측점 const pipe = stale.find((entry) => { const gap = Math.abs(entry.chainage_m - Number(section.chainage_m)); @@ -175,10 +173,7 @@ async function withDraftPipeKinds(detail: SectionDetailResponse, projectId: stri }); if (!pipe) continue; const facility = pipe.facility ?? "pipe"; - const values = { ...registry.pipe, ...pipe.options }; - if (facility === "pipe") - section.structure = `${values.pipe_kind} D${Math.trunc(Number(values.pipe_diameter_mm))}`; - else section.structure = types.find((type) => type.type_id === facility)?.name ?? facility; + section.structure = cardTitle(facility, pipe.options, registry[facility]); } attachCulvertSets(sections, buildCulvertSets(stale, registry)); } diff --git a/B06_Section/B06_Section_UI_Cross_Revetment.ts b/B06_Section/B06_Section_UI_Cross_Revetment.ts index db6536233..14a9abba0 100644 --- a/B06_Section/B06_Section_UI_Cross_Revetment.ts +++ b/B06_Section/B06_Section_UI_Cross_Revetment.ts @@ -25,6 +25,7 @@ import { WALL_MIN_PURE_HEIGHT_M, } from "./B06_Section_UI_Cross_Culvert_Const"; import { outlineLeanOf } from "./B06_Section_UI_Cross_Lean"; +import { catalogRow } from "../common_util/common_util_structure_catalog"; import type { RevetMaterial } from "./B06_Section_UI_Cross_Culvert_Const"; import { fillWallBaseWidth, groundInterpolator } from "./B06_Section_UI_Cross_Culvert_Solve"; import { buildExtrasAt } from "./B06_Section_UI_Cross_Culvert_Extra"; @@ -277,7 +278,8 @@ export function computeRevetmentLayout( : { walls: [], segments: [], appliedAdjusts: [], addable: false }; const walls: WallLayout[] = [tier1, ...extras.walls]; - if (spec.type_id === "retaining_wall") for (const wall of walls) wall.retaining = true; + // 물빼기 파이프 단면 = 표 형상칸.weep(322-1 · 옹벽) + if (catalogRow(spec.type_id)?.shapeOpts.weep) for (const wall of walls) wall.retaining = true; const jt = tier1.topJoint; // 노견 → (물매를 탄 노견 연장) → 벽 이음선 상단점. 연장 구간은 노면 물매 그대로 가고, // 꺾임점에서 성토 물매 1:1.2로 벽 상단에 닿는다(2026-08-29 사용자 확정). diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 9acae7a03..379f7f71f 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -60,6 +60,7 @@ import { } from "./B06_Section_UI_Adjust_Dock"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec"; +import { listPick } from "../common_util/common_util_structure_catalog"; const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요."; const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다."; @@ -379,7 +380,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc fetchStructures(projectId), fetchDetailPipePoints(projectId), ]); - section.setTypes(types); + section.setTypes(types, listPick("b06")); // 표 목록 both + b06(322-1) markTypes = types; // 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙). structures = readPendingStructures(projectId) ?? stored.structures; diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index 3250b8525..9fc75d028 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -9,6 +9,7 @@ import { PIPE_TYPES } from "@config/config_frontend"; import { PIPE_DISPLAY_NAME } from "../B05_Profile/B05_Profile_UI_IrregularStations"; +import { pipeTitleKinds } from "../common_util/common_util_structure_catalog"; import type { CrossDesignChange } from "./B06_Section_UI_Cross_Design"; import type { DesignProfile, @@ -62,9 +63,12 @@ export interface YScaleOptions { * * 저장 라벨은 그대로 두어야 한다 — 재진입 이관(`B05_Profile_Structures_Migration`)이 라벨 * 문자열로 구조물 종류를 되짚는다. 그래서 바꾸는 것은 화면에 찍는 순간뿐이다. + * 관종 = 설정 관종 + 표 카드이름 pipe 종류의 정의 관종(322-1 · 흄관 · VR관도 「배수관」). */ export function structureDisplayName(label: string): string { - return PIPE_TYPES.some((kind) => label.startsWith(`${kind} D`)) ? PIPE_DISPLAY_NAME : label; + return [...PIPE_TYPES, ...pipeTitleKinds()].some((kind) => label.startsWith(`${kind} D`)) + ? PIPE_DISPLAY_NAME + : label; } export function validElevation( diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index fab2266fb..08235c5ad 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -15,6 +15,7 @@ import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures"; import type { IrregularStation } from "../B05_Profile/B05_Profile_UI_IrregularStations"; import { mountStructureMenu } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; import { pickGroups } from "../B05_Profile/B05_Profile_UI_Structures_Panel_Types"; +import { listPick } from "../common_util/common_util_structure_catalog"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; /** 종단 그래프에서 구조물을 넣고 빼는 길 — 페이지가 물려 준다. 없으면 메뉴를 안 붙인다. */ @@ -128,7 +129,7 @@ export function mountSectionStructureMenu( else input.edit.removeStructure(station.id); }, onAddPipe: (chainageM) => input.edit.addPipe(chainageM), - structureGroups: () => pickGroups(input.types, () => true), // B06 패널 거르기 = 전부(129-1) + structureGroups: () => pickGroups(input.types, listPick("b06")), // B06 패널 거르기와 같음(129-1 · 322-1) onAddStructureType: (chainageM, typeId) => input.edit.addStructureType(chainageM, typeId), }); } diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index ba1c749a7..09aecc0ec 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -11,8 +11,12 @@ * * 값을 새로 만들지 않는다 — **정본(관 지점 옵션) + 레지스트리 기본값**만 조합한다. * 상수 사본을 두면 B05 폼과 갈라지므로 기본값은 레지스트리에서만 꺼낸다. + * 시설마다 그릴 세트 = 구조물 표 형상(322-1 · `common_util_structure_catalog`). * ========================================================================== */ +// `.js` = 짝 거울 시험이 tsc → Node ESM 으로 이 파일만 엮음(확장자 있어야 찾음 · vite 는 .ts 로 풂) +import { catalogRow } from "./common_util_structure_catalog.js"; + /** 관 지점 정본 1건 — `pipe_points.json` 의 한 줄(브라우저는 API 로 같은 것을 받는다). */ export interface CulvertPipePoint { chainage_m: number; @@ -129,12 +133,8 @@ export function followPipeHeights( } } -/** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */ +/** 시설 종류 기본값 — 파이썬 `common_util_drainage_pipes.PIPE_FACILITY_PIPE` 와 같은 문자열. */ const FACILITY_PIPE = "pipe"; -const FACILITY_BOX = "box_culvert"; -const FACILITY_FORD_PAVEMENT = "ford_pavement"; -const FACILITY_FORD_BRIDGE = "ford_bridge"; -const FACILITY_REVET = "revetment"; /** 숫자 옵션 하나를 정리한다. 문자열 저장분(관경 "1000")도 받는다 — 파이썬 `_number`. */ function num(value: unknown, fallback: number | null): number | null { @@ -566,14 +566,15 @@ export function buildCulvertSets( const chainage = num(point.chainage_m, null); if (chainage === null) continue; const values = (point.options ?? {}) as Record; - const facility = point.facility || FACILITY_PIPE; + // 그릴 세트 = 표 형상(322-1 · 모르는 시설 = 배관 세트) + const shape = catalogRow(point.facility || FACILITY_PIPE)?.shape ?? "pipe_set"; let spec: CulvertSetSpec; - if (facility === FACILITY_FORD_BRIDGE) spec = fordSet(values, registry); - else if (facility === FACILITY_BOX) spec = boxSet(values, registry); - else if (facility === FACILITY_FORD_PAVEMENT) spec = fordPavementSet(values, registry); - else if (facility === FACILITY_REVET) spec = revetSet(values); - else if (text(values.pipe_diameter_mm) === null) - continue; // 관경 안 고름 = 관 없음(54-39) + if (shape === "ford") spec = fordSet(values, registry); + else if (shape === "box") spec = boxSet(values, registry); + else if (shape === "ford_pavement") spec = fordPavementSet(values, registry); + else if (shape === "hidden_revet") spec = revetSet(values); + else if (shape !== "pipe_set" || text(values.pipe_diameter_mm) === null) + continue; // 형상 없음 · 관경 안 고름 = 관 없음(54-39) else spec = culvertSet(values, registry); sets.set(round(chainage, 2), spec); } diff --git a/common_util/common_util_structure_catalog.py b/common_util/common_util_structure_catalog.py new file mode 100644 index 000000000..289156c93 --- /dev/null +++ b/common_util/common_util_structure_catalog.py @@ -0,0 +1,126 @@ +"""구조물 표(catalog · PLAN 322-1) — 종류마다 「무엇을 할지」 한 줄. + +한 줄 = 정의 `반영` ⊕ 부품 상자 처음값 ⊕ 부품 끔. 종류 키 · 군 글자 · 자리꼴 · managed_by 로 +가리던 자리(측점 · 카드 이름 · 형상)가 이 한 줄을 읽음 · 그리는 함수는 그대로. +부품 상자 = `common_util_structure_parts.json`(TS 와 같은 파일) · 줄 열쇠도 TS 와 같은 이름. +TS 짝 = `common_util_structure_catalog.ts` +거울 시험 = `resources/tester/test_322_1_catalog_mirror.py`. +""" + +from __future__ import annotations + +import json +import math +from functools import lru_cache +from pathlib import Path +from typing import Any + +PARTS_PATH = Path(__file__).with_name("common_util_structure_parts.json") + +# 갈래 → 부품을 끈 값(`부품.{갈래}.{id} == false`) +_OFF = {"횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none"} + + +@lru_cache(maxsize=1) +def load_parts() -> dict[str, Any]: + with open(PARTS_PATH, encoding="utf-8") as handle: + return json.load(handle) + + +def resolve_catalog( + definition: dict[str, Any] | None, parts: dict[str, Any] | None = None +) -> dict[str, dict[str, Any]]: + """정의 + 부품 상자 → 종류마다 한 줄(정의 types 차례 · 정의가 없으면 처음값 차례).""" + start = (parts or load_parts())["처음값"] + definition = definition or {} + groups = definition.get("구조물군") or {} + group_keys = list(groups) + off = {key for key, on in (definition.get("부품") or {}).items() if on is False} + reflect = definition.get("반영") or {} + types = definition.get("types") or [{"type_id": key} for key in start] + rows: dict[str, dict[str, Any]] = {} + for order, item in enumerate(types): + type_id = item["type_id"] + base = start.get(type_id) or {} + mine = reflect.get(type_id) or {} + + def part(branch: str, base=base, mine=mine) -> str: + value = mine.get(branch, base.get(branch, _OFF[branch])) + return _OFF[branch] if f"{branch}.{value}" in off else value + + group = item.get("group") or "" + # 반영이 없으면 옛 짝 — 처음값 hidden 은 숨김 · 그 밖은 정의 enabled 가 가름(꺼짐 = B06 만) + if "목록" in mine: + listed = mine["목록"] + elif base.get("목록") == "hidden": + listed = "hidden" + else: + listed = "b06" if item.get("enabled") is False else "both" + rows[type_id] = { + "typeId": type_id, + "name": item.get("name") or type_id, + "group": group, + "groupName": groups.get(group, group), + "groupOrder": group_keys.index(group) if group in group_keys else len(group_keys), + "typeOrder": order, + "placement": base.get("자리꼴") or item.get("placement") or "point", + "owner": "pipe_points" if item.get("managed_by") == "pipe_points" else "structures", + "list": listed, + "station": part("횡단도"), + "title": part("카드이름"), + "shape": part("형상"), + "shapeOpts": {**(base.get("형상칸") or {}), **(mine.get("형상칸") or {})}, + "model3d": part("3D"), + "color": (item.get("style") or {}).get("color") or None, + } + return rows + + +@lru_cache(maxsize=4) +def _catalog_at(path: str, mtime: int, size: int) -> dict[str, dict[str, Any]]: + from B05_Profile.B05_Profile_Structures_Schema import _read_definition + + return resolve_catalog(_read_definition(path, mtime, size)) + + +def catalog() -> dict[str, dict[str, Any]]: + """지금 정의 판의 표 — 정의 파일이 바뀌면(M02 저장) 다음 부름부터 새 표(파일 시각 캐시).""" + from B05_Profile.B05_Profile_Structures_Schema import _stamp + + return _catalog_at(*_stamp()) + + +def catalog_row(type_id: str) -> dict[str, Any] | None: + return catalog().get(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())} + + +def card_title(type_id: str, options: dict[str, Any] | None) -> str | None: + """카드 · 측점 이름 — 카드이름 pipe = 「관종 D관경」(빈칸 = 그 종류 정의 기본값) · 그 밖 = 이름. + + 표에 없는 종류면 None(측점 안 붙임). TS `cardTitle` 짝(옛 빈칸 D800 ↔ 화면 D1000 어긋남 없앰). + """ + from B05_Profile.B05_Profile_Structures_Schema import structure_type_map + + row = catalog_row(type_id) + if row is None: + return None + if row["title"] != "pipe": + return row["name"] + definition = structure_type_map().get(type_id) + defaults = {o.key: o.default for o in definition.options} if definition else {} + values = options or {} + + def value(key: str) -> Any: + own = values.get(key) + return defaults.get(key) if own is None or own == "" else own + + try: + diameter = math.trunc(float(value("pipe_diameter_mm"))) + except (TypeError, ValueError): + diameter = 0 + return f"{value('pipe_kind') or ''} D{diameter}" diff --git a/common_util/common_util_structure_catalog.ts b/common_util/common_util_structure_catalog.ts new file mode 100644 index 000000000..f26739e63 --- /dev/null +++ b/common_util/common_util_structure_catalog.ts @@ -0,0 +1,174 @@ +/* ============================================================================= + * common_util/common_util_structure_catalog.ts + * 구조물 표(catalog · PLAN 322-1) — 종류마다 「무엇을 할지」 한 줄 = 정의 `반영` ⊕ 부품 상자 처음값 ⊕ 부품 끔. + * + * 종류 키 · 군 글자 · 자리꼴 · managed_by 로 가리던 자리(목록 · 측점 · 카드 이름 · 형상 · 3D · 알약)가 이 한 줄을 + * 읽음. 그리는 함수는 그대로(그림 자료 열쇠만 봄). 부품 상자 = `common_util_structure_parts.json`(파이썬과 같은 파일). + * 정의를 받기 전 = 처음값만으로 선 표(이름 · 군 · 정본 · 색 없음) — 받으면 `useCatalogDefinition` 이 갈아 끼움. + * 파이썬 짝 = `common_util_structure_catalog.py` · 거울 시험 = `resources/tester/test_322_1_catalog_mirror.py`. + * ========================================================================== */ + +import PARTS from "./common_util_structure_parts.json" with { type: "json" }; + +/** 고르기 목록 자리 — both = B05 · B06 · b06 = B06 만 · hidden = 안 보임(옛 자료는 그대로 그림). */ +export type ListPlace = "both" | "b06" | "hidden"; +export type PipeOwner = "pipe_points" | "structures"; + +export interface CatalogRow { + typeId: string; + name: string; + group: string; + groupName: string; + groupOrder: number; + typeOrder: number; + placement: string; + owner: PipeOwner; + list: ListPlace; + /** 횡단도 부품 — none · anchor · span3. */ + station: string; + /** 카드 이름 부품 — name · pipe. */ + title: string; + /** 형상 부품 — none · pipe_set · box · ford · ford_pavement · hidden_revet · wall · berm · underdrain · pavement · spoil. */ + shape: string; + shapeOpts: Record; + /** 3D 부품 — none · pipe · box · ford · chute · wall · surface. */ + model3d: string; + color: string | null; +} + +type Branch = "횡단도" | "카드이름" | "형상" | "3D"; +type Reflect = Partial> & { + 형상칸?: Record; + 자리꼴?: string; +}; + +/** 정의 한 벌 중 표가 읽는 칸 — `반영` · `부품` 은 M02 [저장] 때 생김(없으면 처음값). */ +export interface CatalogDefinition { + 구조물군?: Record; + types?: ReadonlyArray<{ + type_id: string; + name?: string; + group?: string; + placement?: string; + managed_by?: string | null; + enabled?: boolean; + style?: { color?: string }; + options?: ReadonlyArray<{ key: string; choices?: readonly string[] }>; + }>; + 반영?: Record; + 부품?: Record; +} + +interface Parts { + parts: Record>; + 처음값: Record; +} + +/** 갈래 → 부품을 끈 값(`부품.{갈래}.{id} === false`). */ +const OFF: Record = { + 횡단도: "none", + 카드이름: "name", + 형상: "none", + "3D": "none", +}; + +/** 정의 + 부품 상자 → 종류마다 한 줄(정의 types 차례 · 정의가 없으면 처음값 차례). */ +export function resolveCatalog( + definition: CatalogDefinition | null, + parts: Parts = PARTS as unknown as Parts, +): Map { + const start = parts.처음값; + const groups = definition?.구조물군 ?? {}; + const groupKeys = Object.keys(groups); + const off = new Set( + Object.entries(definition?.부품 ?? {}) + .filter(([, on]) => on === false) + .map(([id]) => id), + ); + const types = definition?.types?.length + ? definition.types + : Object.keys(start).map((type_id) => ({ type_id }) as const); + const rows = new Map(); + types.forEach((item, typeOrder) => { + const id = item.type_id; + const base = start[id] ?? {}; + const mine = definition?.반영?.[id] ?? {}; + const part = (branch: Branch): string => { + const value = mine[branch] ?? base[branch] ?? OFF[branch]; + return off.has(`${branch}.${value}`) ? OFF[branch] : value; + }; + const group = "group" in item ? (item.group ?? "") : ""; + const index = groupKeys.indexOf(group); + rows.set(id, { + typeId: id, + name: ("name" in item && item.name) || id, + group, + groupName: groups[group] ?? group, + groupOrder: index < 0 ? groupKeys.length : index, + typeOrder, + placement: base.자리꼴 ?? ("placement" in item ? item.placement : undefined) ?? "point", + owner: + "managed_by" in item && item.managed_by === "pipe_points" ? "pipe_points" : "structures", + // 반영이 없으면 옛 짝 — 처음값 hidden 은 숨김 · 그 밖은 정의 enabled 가 가름(꺼짐 = B06 만) + list: (mine.목록 ?? + (base.목록 === "hidden" + ? "hidden" + : "enabled" in item && item.enabled === false + ? "b06" + : "both")) as ListPlace, + station: part("횡단도"), + title: part("카드이름"), + shape: part("형상"), + shapeOpts: { ...(base.형상칸 ?? {}), ...(mine.형상칸 ?? {}) }, + model3d: part("3D"), + color: ("style" in item && item.style?.color) || null, + }); + }); + return rows; +} + +let current = resolveCatalog(null); +let pipeKinds: string[] = []; + +/** 정의를 받으면(종류 레지스트리 · `fetchStructureTypes`) 표를 갈아 끼움 — 못 받았으면 처음값 표 그대로. */ +export function useCatalogDefinition(definition: CatalogDefinition | null): void { + current = resolveCatalog(definition); + pipeKinds = [ + ...new Set( + (definition?.types ?? []) + .filter((type) => current.get(type.type_id)?.title === "pipe") + .flatMap( + (type) => type.options?.find((option) => option.key === "pipe_kind")?.choices ?? [], + ), + ), + ]; +} + +/** 종류 한 줄 — 표에 없는 종류면 undefined. */ +export const catalogRow = (typeId: string): CatalogRow | undefined => current.get(typeId); + +/** 고르기 목록에 보일 종류인가 — B05 = both · B06 = both + b06(표에 없는 종류는 보임). */ +export const listPick = + (page: "b05" | "b06") => + (type: { type_id: string }): boolean => { + const list = catalogRow(type.type_id)?.list ?? "both"; + return list === "both" || (page === "b06" && list === "b06"); + }; + +/** 카드 · 측점 이름 — 카드이름 pipe = 「관종 D관경」(빈칸 = 그 종류 정의 기본값) · 그 밖 = 종류 이름. */ +export function cardTitle( + typeId: string, + options: Record | null | undefined, + defaults: Record = {}, +): string { + const row = catalogRow(typeId); + if (row?.title !== "pipe") return row?.name ?? typeId; + const value = (key: string): unknown => { + const own = options?.[key]; + return own === undefined || own === null || own === "" ? defaults[key] : own; + }; + return `${value("pipe_kind") ?? ""} D${Math.trunc(Number(value("pipe_diameter_mm"))) || 0}`; +} + +/** 관 공식 이름의 관종들(정의 선택지) — 표시 이름이 「배수관」으로 줄일 이름을 알아봄. */ +export const pipeTitleKinds = (): readonly string[] => pipeKinds; diff --git a/common_util/common_util_structure_parts.json b/common_util/common_util_structure_parts.json new file mode 100644 index 000000000..ceef3a46b --- /dev/null +++ b/common_util/common_util_structure_parts.json @@ -0,0 +1,485 @@ +{ + "comment": "구조물 부품 상자(PLAN 322-1) — 코드가 그리는 방법 한 벌. TS(import) · 파이썬(json.load)이 같은 파일을 읽음. parts = 갈래(횡단도 · 카드이름 · 형상 · 3D)마다 부품 id → {이름 · 읽는 칸(옵션 키) · 자리꼴 · 정본}. 처음값 = 종류마다 정의 `반영` 이 없을 때 쓰는 값(지금 동작 · ★ = PLAN_구조물표 4장). 표 읽기 = common_util_structure_catalog.ts · .py(짝).", + "parts": { + "횡단도": { + "anchor": { + "이름": "기준 한 장", + "읽는 칸": [], + "자리꼴": ["point", "interval", "site"], + "정본": ["pipe_points", "structures"] + }, + "span3": { + "이름": "시작 · 기준 · 끝", + "읽는 칸": ["length_m", "before_m", "after_m"], + "자리꼴": ["interval"], + "정본": ["pipe_points", "structures"] + } + }, + "카드이름": { + "pipe": { + "이름": "관종 D관경", + "읽는 칸": ["pipe_kind", "pipe_diameter_mm"], + "자리꼴": ["point"], + "정본": ["pipe_points"] + } + }, + "형상": { + "pipe_set": { + "이름": "배관 세트(관 · 유입 · 유출 · 흙막이 · 도수로)", + "읽는 칸": [ + "pipe_kind", + "pipe_diameter_mm", + "revet_foundation", + "inlet_type", + "inlet_revet_form", + "inlet_revet_length_m", + "inlet_revet_before_m", + "inlet_revet_after_m", + "inlet_revet_height_m", + "inlet_basin_length_m", + "inlet_basin_before_m", + "inlet_basin_after_m", + "inlet_basin_height_m", + "outlet_type", + "outlet_revet_form", + "outlet_revet_length_m", + "outlet_revet_before_m", + "outlet_revet_after_m", + "outlet_revet_height_m" + ], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "box": { + "이름": "BOX 본체 · 날개벽", + "읽는 칸": [ + "body_width_m", + "body_height_m", + "wall_thickness_m", + "slab_thickness_m", + "wing_in", + "wing_in_height_m", + "wing_in_length_m", + "wing_in_angle_deg", + "wing_out", + "wing_out_height_m", + "wing_out_length_m", + "wing_out_angle_deg" + ], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "ford": { + "이름": "세월교(관 · 월류 · 날개벽)", + "읽는 칸": [ + "pipe_kind", + "pipe_diameter_mm", + "pipe_count", + "ford_width_m", + "ford_height_m", + "wing_in", + "wing_in_height_m", + "wing_in_length_m", + "wing_in_angle_deg", + "wing_out", + "wing_out_height_m", + "wing_out_length_m", + "wing_out_angle_deg" + ], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "ford_pavement": { + "이름": "물넘이 포장(파인 노면 · 도수로)", + "읽는 칸": ["ford_width_m", "ford_height_m", "ford_slope_deg", "ford_slope_pct"], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "hidden_revet": { + "이름": "독립 기슭막이(관 숨김 벽)", + "읽는 칸": [ + "form", + "height_m", + "length_m", + "before_m", + "after_m", + "tiers", + "side", + "foundation", + "face_role", + "face_slope_ratio" + ], + "자리꼴": ["interval"], + "정본": ["pipe_points"] + }, + "wall": { + "이름": "벽(옹벽 · 흙막이 · 돌쌓기)", + "읽는 칸": [ + "form", + "height_m", + "side", + "foundation", + "tiers", + "lift_m", + "shift_m", + "face_role", + "face_slope_ratio" + ], + "자리꼴": ["interval"], + "정본": ["structures"] + }, + "berm": { + "이름": "소단(사면 계단)", + "읽는 칸": ["width_m", "interval_m", "slope_deg", "depth_m"], + "자리꼴": ["interval"], + "정본": ["structures"] + }, + "underdrain": { + "이름": "맹암거", + "읽는 칸": ["culvert_height_m", "culvert_slope_pct"], + "자리꼴": ["point"], + "정본": ["structures"] + }, + "pavement": { + "이름": "포장층", + "읽는 칸": ["pave_spec", "height_m", "widening_off"], + "자리꼴": ["interval"], + "정본": ["structures"] + }, + "spoil": { + "이름": "사토(성토 채움)", + "읽는 칸": ["capacity_m3"], + "자리꼴": ["site"], + "정본": ["structures"] + } + }, + "3D": { + "pipe": { + "이름": "관 · 유입 · 유출 벽", + "읽는 칸": [], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "box": { + "이름": "BOX 본체 · 날개벽", + "읽는 칸": [], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "ford": { + "이름": "세월교 몸체 · 관", + "읽는 칸": [], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "chute": { + "이름": "물넘이 도수로 띠", + "읽는 칸": [], + "자리꼴": ["point"], + "정본": ["pipe_points"] + }, + "wall": { + "이름": "벽 스윕", + "읽는 칸": [], + "자리꼴": ["interval"], + "정본": ["pipe_points", "structures"] + }, + "surface": { + "이름": "설계선만(몸체 없음)", + "읽는 칸": [], + "자리꼴": ["point", "interval", "site"], + "정본": ["pipe_points", "structures"] + } + } + }, + "처음값": { + "pipe": { + "목록": "both", + "횡단도": "anchor", + "카드이름": "pipe", + "형상": "pipe_set", + "3D": "pipe" + }, + "box_culvert": { + "목록": "both", + "횡단도": "anchor", + "카드이름": "name", + "형상": "box", + "3D": "box" + }, + "ford_pavement": { + "목록": "both", + "횡단도": "anchor", + "카드이름": "name", + "형상": "ford_pavement", + "3D": "chute" + }, + "ford_bridge": { + "목록": "both", + "횡단도": "anchor", + "카드이름": "name", + "형상": "ford", + "3D": "ford" + }, + "cross_drain_exposed": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "open_ditch": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "cross_underdrain": { + "목록": "both", + "횡단도": "anchor", + "카드이름": "name", + "형상": "underdrain", + "3D": "surface" + }, + "ditch_side": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "ditch_ridge": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "ditch_berm": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "chute": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "slope_drain": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "underdrain": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "retaining_wall": { + "목록": "both", + "횡단도": "span3", + "카드이름": "name", + "형상": "wall", + "형상칸": { "form": "콘크리트", "weep": true }, + "3D": "wall" + }, + "masonry_wet": { + "목록": "hidden", + "횡단도": "span3", + "카드이름": "name", + "형상": "wall", + "형상칸": { "form": "돌쌓기(찰)" }, + "3D": "wall" + }, + "masonry_dry": { + "목록": "hidden", + "횡단도": "span3", + "카드이름": "name", + "형상": "wall", + "형상칸": { "form": "돌쌓기(메)" }, + "3D": "wall" + }, + "soil_guard": { + "목록": "both", + "횡단도": "span3", + "카드이름": "name", + "형상": "wall", + "형상칸": { "form": "통나무·목재틀" }, + "3D": "wall" + }, + "boulder_masonry": { + "목록": "hidden", + "횡단도": "span3", + "카드이름": "name", + "형상": "wall", + "형상칸": { "form": "돌쌓기(메)" }, + "3D": "wall" + }, + "berm": { + "목록": "both", + "횡단도": "span3", + "카드이름": "name", + "형상": "berm", + "3D": "surface" + }, + "erosion_check": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "bed_sill": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "check_dam_small": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "revetment": { + "목록": "hidden", + "횡단도": "span3", + "카드이름": "name", + "형상": "hidden_revet", + "3D": "wall", + "자리꼴": "interval" + }, + "evac_road": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "refuge": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "work_yard": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "turnaround": { + "목록": "hidden", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "mirror": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "curb": { "목록": "both", "횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none" }, + "hazard_sign": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "guardrail": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "position_sign": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "water_intake": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "barrier_gate": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "completion_sign": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "spoil_bank": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "spoil", + "3D": "surface" + }, + "borrow_pit": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "wildlife_path": { + "목록": "b06", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "revegetation": { + "목록": "b06", + "횡단도": "none", + "카드이름": "name", + "형상": "none", + "3D": "none" + }, + "pavement_concrete": { + "목록": "both", + "횡단도": "none", + "카드이름": "name", + "형상": "pavement", + "3D": "surface" + }, + "gravel_surfacing": { + "목록": "b06", + "횡단도": "none", + "카드이름": "name", + "형상": "pavement", + "3D": "surface" + }, + "etc": { "목록": "both", "횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none" } + } +} diff --git a/common_util/common_util_structure_pavement.py b/common_util/common_util_structure_pavement.py index 26d12b7d6..11411dde6 100644 --- a/common_util/common_util_structure_pavement.py +++ b/common_util/common_util_structure_pavement.py @@ -105,10 +105,10 @@ def attach_pavement_specs(sections: list[dict[str, Any]], specs: list[dict[str, def attach_pavement_structures(project_root: Path, cross_sections: list[dict[str, Any]]) -> int: """저장된 구조물 정본으로 `section.pavement` 를 얹음. 정본을 못 읽으면 0(비치명).""" 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 type_ids_with try: - type_ids = {key for key, value in structure_type_map().items() if value.group == "G"} + type_ids = type_ids_with(shape="pavement") # 표 형상(322-1 · 옛 G 군) structures = [item.model_dump() for item in load_structures(str(project_root))[1]] except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 감 logger.exception("B06 노면공 정본을 읽지 못했습니다 (없는 것으로 본다)") diff --git a/common_util/common_util_structure_station.ts b/common_util/common_util_structure_station.ts index ac0cb7d52..43bd4c06e 100644 --- a/common_util/common_util_structure_station.ts +++ b/common_util/common_util_structure_station.ts @@ -9,6 +9,8 @@ * 일반 측점 = 격자 · 시종점(구조물 측점 kind "irregular" 은 뺌) — 끝(노선 길이)을 넘지 않게 자르는 데만 · * 상세를 읽을 때 한 번 둔다. * + * 심는 측점 = 구조물 표(catalog) 횡단도 부품(322-1) — anchor = 기준 하나 · span3 = 시작 · 기준 · 끝 · none = 없음. + * * 측점 한 벌(272) — 측점을 심는 관 · 구조물(`plantedChainages`) = 서버 `resolve_extra_stations` * (B05_Profile_Engine_Sections.py) 짝 · 같은 m 면 측점 하나 · 구조물이 위(`oneStationList`) = 서버 * `_merge_irregular_into_longitudinal`(B05_Profile_Router_Confirm.py) 짝. 거울 시험 = @@ -16,6 +18,8 @@ * 이 목록 하나를 씀. * ========================================================================== */ +import { catalogRow } from "./common_util_structure_catalog"; + const EPS = 1e-6; /** 같은 측점으로 볼 거리(m) — 서버 측점 만들기의 격자 붙임 폭(`Engine_Sections_Core`)과 같음. */ @@ -111,22 +115,22 @@ function marksOf( ); } -/** 관 시설 하나가 심는 측점 — 독립 기슭막이 = 시작 · 기준 · 끝 · 그 밖(물넘이포장 포함 · 286-1) = 기준. */ +/** 관 시설 하나가 심는 측점 — 표 횡단도(독립 기슭막이 = span3 · 물넘이포장 포함 그 밖 = anchor · 286-1). */ export function pipeStationMarks(pipe: PlantingPipe): StationMark[] { - const facility = pipe.facility ?? "pipe"; - return marksOf(pipe.start_m, pipe.chainage_m, pipe.end_m, facility === "revetment"); + const station = catalogRow(pipe.facility ?? "pipe")?.station ?? "anchor"; + if (station === "none") return []; + return marksOf(pipe.start_m, pipe.chainage_m, pipe.end_m, station === "span3"); } -/** 구조물 하나가 심는 측점 — A군 = 기준 · C · D군 구간형 = 시작 · 기준 · 끝 · 다른 정본 소관 · 그 밖 = 없음. */ +/** 구조물 하나가 심는 측점 — 표 횡단도(anchor = 기준 · span3 = 시작 · 기준 · 끝) · 관 지점 정본 소관 · 모르는 종류 = 없음. */ export function structureStationMarks( structure: PlantingStructure, type: PlantingType | undefined, ): StationMark[] { - if (!type || type.managed_by) return []; - const interval = type.placement === "interval"; - if (type.group !== "A" && !(interval && (type.group === "C" || type.group === "D"))) return []; + const row = type && catalogRow(type.type_id); + if (!row || row.owner === "pipe_points" || row.station === "none") return []; const anchor = structure.chainage_m ?? structure.start_m; - return marksOf(structure.start_m, anchor, structure.end_m, interval); + return marksOf(structure.start_m, anchor, structure.end_m, row.station === "span3"); } /** 측점을 심는 자리 전부(오름차순 · 겹침 그대로) — 서버 `resolve_extra_stations` 의 누가거리. */ diff --git a/common_util/common_util_structure_underdrain.ts b/common_util/common_util_structure_underdrain.ts index ca2a91d44..e992610e6 100644 --- a/common_util/common_util_structure_underdrain.ts +++ b/common_util/common_util_structure_underdrain.ts @@ -5,9 +5,11 @@ * 기준 측점(0.02 m 안) 횡단 dict 에 `underdrain` = 높이 · 경사 — 횡단도가 돌 무늬 형상을 그리고, * 설계 계산은 그 측점을 측구 없이 지대 낮은 쪽 한 경사로 기울임(`tilted`). * 파이썬 짝 = `B06_Section/B06_Section_Engine_Tilt.py`(`load_underdrains` · `attach_underdrains`). + * 맹암거 종류 = 구조물 표 형상 underdrain(322-1). * ========================================================================== */ -export const UNDERDRAIN_TYPE_ID = "cross_underdrain"; +import { catalogRow } from "./common_util_structure_catalog"; + const EDGE_TOLERANCE_M = 0.02; /** 맹암거 제원 — 높이 · 경사가 비면 null(높이 없으면 형상 없음 · 경사 없으면 표준 횡단경사). */ @@ -35,7 +37,7 @@ const positive = (value: unknown): number | null => { export function underdrainSpecsFrom(structures: readonly UnderdrainInput[]): UnderdrainSpec[] { return structures.flatMap((structure) => { const at = structure.chainage_m ?? structure.start_m; - if (structure.type_id !== UNDERDRAIN_TYPE_ID || typeof at !== "number") return []; + if (catalogRow(structure.type_id)?.shape !== "underdrain" || typeof at !== "number") return []; const options = structure.options ?? {}; return [ { diff --git a/common_util/common_util_structure_walls.ts b/common_util/common_util_structure_walls.ts index 8e1d65f49..1cdc8ca17 100644 --- a/common_util/common_util_structure_walls.ts +++ b/common_util/common_util_structure_walls.ts @@ -8,8 +8,12 @@ * 산식이 같아야 하므로 두 파일 머리에 짝임을 적고 거울 테스트를 둔다. * * 얹는 것은 제원뿐이다 — 도형·면적은 `B06_Section_UI_Cross_Revetment` 가 그린다. + * 벽 기본 형태(폼에서 형태를 비웠을 때) = 구조물 표 형상칸.form(322-1 · 옛 `FORM_BY_TYPE`). * ========================================================================== */ +// `.js` = 짝 거울 시험이 tsc → Node ESM 으로 이 파일만 엮음(확장자 있어야 찾음 · vite 는 .ts 로 풂) +import { catalogRow } from "./common_util_structure_catalog.js"; + /** 구조물 정본 한 건(필요한 칸만). `structures.json` 과 같은 이름을 쓴다. */ export interface WallStructureInput { structure_id?: string | null; @@ -54,7 +58,7 @@ export interface WallTier { const EDGE_TOLERANCE_M = 0.02; /** - * 구조물 종류 → 횡단 기하가 아는 형태 이름 — 파이썬 `_FORM_BY_TYPE` 와 같은 표. + * 구조물 종류 → 횡단 기하가 아는 형태 이름 = 표 형상칸.form(파이썬 `_wall_form` 짝). * * ⚠⚠ **「형상 동일」이 「수량 동일」이 아니다**(2026-09-09 사용자 확정 4차 원문: * 「흙막이는 횡단도에서 표현방식들과 옵션들은 동일하게 반영 · **형상은 동일** · @@ -64,12 +68,9 @@ const EDGE_TOLERANCE_M = 0.02; * ⚠ 반대 방향의 사례가 골막이다 — 그림도 수량도 다르고, 두께식마저 다른 식이었다. * **구조물 계열이 다르면 상수를 나눠 쓸 것.** */ -export const FORM_BY_TYPE: Record = { - masonry_wet: "돌쌓기(찰)", - masonry_dry: "돌쌓기(메)", - boulder_masonry: "돌쌓기(메)", - retaining_wall: "콘크리트", - soil_guard: "통나무·목재틀", +const formOf = (typeId: string): string | null => { + const form = catalogRow(typeId)?.shapeOpts.form; + return typeof form === "string" ? form : null; }; const num = (value: unknown): number | null => @@ -117,7 +118,7 @@ export function wallSpecsFrom( start_m: Math.min(start, end), end_m: Math.max(start, end), anchor_m: num(structure.chainage_m) ?? Math.min(start, end), - form: (options.form as string) || FORM_BY_TYPE[structure.type_id] || null, + form: (options.form as string) || formOf(structure.type_id), height_m: num(options.height_m), side: (options.side as string) ?? null, // 기초 축 — 저장 칸과 **같은 글자**. 초안 경로에서 빠지면 터파기가 안 그려진다. diff --git a/resources/tester/helper_107_ford_free.cjs b/resources/tester/helper_107_ford_free.cjs index 884f1d5fa..5ee26f04f 100644 --- a/resources/tester/helper_107_ford_free.cjs +++ b/resources/tester/helper_107_ford_free.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -30,6 +31,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) diff --git a/resources/tester/helper_116_right_click_add.cjs b/resources/tester/helper_116_right_click_add.cjs index d13906f8a..1853a63c6 100644 --- a/resources/tester/helper_116_right_click_add.cjs +++ b/resources/tester/helper_116_right_click_add.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -30,6 +31,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); diff --git a/resources/tester/helper_129_menu_equals_panel.cjs b/resources/tester/helper_129_menu_equals_panel.cjs index 64e9e46b3..aa75ceee4 100644 --- a/resources/tester/helper_129_menu_equals_panel.cjs +++ b/resources/tester/helper_129_menu_equals_panel.cjs @@ -30,6 +30,9 @@ function loadTs(file) { cache.set(full, box.exports); const localRequire = (request) => { if (STUBS[request]) return STUBS[request]; + // 구조물 부품 상자(322-1) — JSON 그대로(트랜스파일이 기본 내보내기로 감쌈) + if (request.endsWith(".json")) + return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8")); if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); if (request.endsWith(".css")) return {}; @@ -44,16 +47,19 @@ const B05 = path.join(ROOT, "B05_Profile"); const T = loadTs(path.join(B05, "B05_Profile_UI_Structures_Panel_Types.ts")); const M = loadTs(path.join(B05, "B05_Profile_UI_Profile_Structures.ts")); const D = loadTs(path.join(B05, "B05_Profile_UI_Structures_Define.ts")); +const C = loadTs(path.join(ROOT, "common_util", "common_util_structure_catalog.ts")); const read = (file) => JSON.parse(fs.readFileSync(file, "utf8")); const types = D.withDefinition(read(process.argv[2]), read(process.argv[3])); +// 화면 `fetchStructureTypes` 처럼 구조물 표를 정의로 세움(322-1) +C.useCatalogDefinition({ types: read(process.argv[2]), ...read(process.argv[3]) }); /** 패널 드롭다운에 보이는 것 — 군(차례 · 이름) → 그 군 고르면 보이는 종류(차례 · 이름 · id). */ function panelList(pick) { return T.groupOptions(types, pick) .filter((option) => !option.hidden) .map((group) => { - const candidates = types.filter((t) => t.group === group.value && D.pickable(t)); + const candidates = types.filter((t) => t.group === group.value); const kinds = T.typeOptions(candidates, pick).filter((option) => !option.hidden); return { label: group.text, types: kinds.map((option) => [option.text, option.value]) }; }); @@ -72,8 +78,9 @@ function menuList(pick) { })); } -const enabled = (t) => t.enabled; -const all = () => true; +// 페이지 거르기 = 구조물 표 목록(322-1 · B05 = both · B06 = both + b06) +const enabled = C.listPick("b05"); +const all = C.listPick("b06"); process.stdout.write( JSON.stringify({ b05: { panel: panelList(enabled), menu: menuList(enabled) }, diff --git a/resources/tester/helper_136_chute_form_cache.cjs b/resources/tester/helper_136_chute_form_cache.cjs index c06e7abc0..bbbc053dc 100644 --- a/resources/tester/helper_136_chute_form_cache.cjs +++ b/resources/tester/helper_136_chute_form_cache.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -27,6 +28,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} }); if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); diff --git a/resources/tester/helper_139_4_buried_outlet.cjs b/resources/tester/helper_139_4_buried_outlet.cjs index a8d7075b9..34a26c770 100644 --- a/resources/tester/helper_139_4_buried_outlet.cjs +++ b/resources/tester/helper_139_4_buried_outlet.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -23,6 +24,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) diff --git a/resources/tester/helper_152_3_chute_cut.cjs b/resources/tester/helper_152_3_chute_cut.cjs index 9bbaf3331..9963064cb 100644 --- a/resources/tester/helper_152_3_chute_cut.cjs +++ b/resources/tester/helper_152_3_chute_cut.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -23,6 +24,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) diff --git a/resources/tester/helper_153_b07_cross_layout.cjs b/resources/tester/helper_153_b07_cross_layout.cjs index 147bce9d6..272cd5cbe 100644 --- a/resources/tester/helper_153_b07_cross_layout.cjs +++ b/resources/tester/helper_153_b07_cross_layout.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const B07 = path.join(ROOT, "B07_DesignDetail"); @@ -73,6 +74,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (request === "@ui/ui_template_elements") return { ...loadTs(path.join(UI, "ui_template_elements.ts")), diff --git a/resources/tester/helper_165_pavement_flow.cjs b/resources/tester/helper_165_pavement_flow.cjs index 156a2845b..3eee4915c 100644 --- a/resources/tester/helper_165_pavement_flow.cjs +++ b/resources/tester/helper_165_pavement_flow.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -15,6 +16,7 @@ function loadTs(file, stubs = {}) { }).outputText; const box = { exports: {} }; const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (stubs[request]) return stubs[request]; throw new Error(`stub 없음: ${request}`); }; diff --git a/resources/tester/helper_173_7_inlet_animal.cjs b/resources/tester/helper_173_7_inlet_animal.cjs index 89f9a38be..f6e63557d 100644 --- a/resources/tester/helper_173_7_inlet_animal.cjs +++ b/resources/tester/helper_173_7_inlet_animal.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -105,6 +106,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); diff --git a/resources/tester/helper_178_3_slope_tiers.cjs b/resources/tester/helper_178_3_slope_tiers.cjs index 4c155ed27..290e69156 100644 --- a/resources/tester/helper_178_3_slope_tiers.cjs +++ b/resources/tester/helper_178_3_slope_tiers.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -29,6 +30,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) diff --git a/resources/tester/helper_219_1_arrow_owner.cjs b/resources/tester/helper_219_1_arrow_owner.cjs index dd6bae490..a1f70e5b9 100644 --- a/resources/tester/helper_219_1_arrow_owner.cjs +++ b/resources/tester/helper_219_1_arrow_owner.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -67,6 +68,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => + (CATALOG.isCatalogRequest(request) ? CATALOG : null) ?? STUBS[request] ?? (request.startsWith(".") ? loadTs(path.join(path.dirname(full), `${request}.ts`)) diff --git a/resources/tester/helper_231_1_pave_reload.cjs b/resources/tester/helper_231_1_pave_reload.cjs index 821a57de2..70d7a0fd8 100644 --- a/resources/tester/helper_231_1_pave_reload.cjs +++ b/resources/tester/helper_231_1_pave_reload.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -14,6 +15,7 @@ function loadTs(file, stubs = {}) { }).outputText; const box = { exports: {} }; const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (stubs[request]) return stubs[request]; throw new Error(`stub 없음: ${request}`); }; diff --git a/resources/tester/helper_234_1_structure_station.cjs b/resources/tester/helper_234_1_structure_station.cjs index 865900800..74e6c98f2 100644 --- a/resources/tester/helper_234_1_structure_station.cjs +++ b/resources/tester/helper_234_1_structure_station.cjs @@ -52,7 +52,11 @@ function fakeElement(tag) { global.document = { createElement: fakeElement }; const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); -const station = loadTs("common_util/common_util_structure_station.ts"); +// 심는 측점 = 구조물 표(322-1) — 공용 표 모듈(실제 정의로 세움) +const catalog = require("./helper_322_catalog_module.cjs"); +const station = loadTs("common_util/common_util_structure_station.ts", { + "./common_util_structure_catalog": catalog, +}); const STATIONS = input.stations; const out = {}; diff --git a/resources/tester/helper_272_1_station_list.cjs b/resources/tester/helper_272_1_station_list.cjs index d446f7156..a5d9e158c 100644 --- a/resources/tester/helper_272_1_station_list.cjs +++ b/resources/tester/helper_272_1_station_list.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -13,6 +14,7 @@ function loadTs(file) { }).outputText; const box = { exports: {} }; const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; throw new Error(`stub 없음: ${request} (${file})`); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); diff --git a/resources/tester/helper_273_2_preview_stations.cjs b/resources/tester/helper_273_2_preview_stations.cjs index 6f7528072..48c397c6f 100644 --- a/resources/tester/helper_273_2_preview_stations.cjs +++ b/resources/tester/helper_273_2_preview_stations.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -13,6 +14,7 @@ function loadTs(file, stubs = {}) { }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, (name) => { + if (CATALOG.isCatalogRequest(name)) return CATALOG; if (!stubs[name]) throw new Error(`stub 없음: ${name} (${file})`); return stubs[name]; }); diff --git a/resources/tester/helper_284_1_pill_pick.cjs b/resources/tester/helper_284_1_pill_pick.cjs index 42d219a92..dc349389d 100644 --- a/resources/tester/helper_284_1_pill_pick.cjs +++ b/resources/tester/helper_284_1_pill_pick.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -14,6 +15,7 @@ function loadTs(file, stubs = {}) { }).outputText; const box = { exports: {} }; const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (stubs[request]) return stubs[request]; throw new Error(`stub 없음: ${request}`); }; diff --git a/resources/tester/helper_286_1_ford_pavement.cjs b/resources/tester/helper_286_1_ford_pavement.cjs index 0d0b018cc..85d319513 100644 --- a/resources/tester/helper_286_1_ford_pavement.cjs +++ b/resources/tester/helper_286_1_ford_pavement.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); @@ -13,6 +14,7 @@ function loadTs(file, stubs = {}) { }).outputText; const box = { exports: {} }; const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (stubs[request]) return stubs[request]; throw new Error(`stub 없음: ${request}`); }; diff --git a/resources/tester/helper_288_underdrain.cjs b/resources/tester/helper_288_underdrain.cjs index 04282f3c8..5a81ab179 100644 --- a/resources/tester/helper_288_underdrain.cjs +++ b/resources/tester/helper_288_underdrain.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -15,6 +16,7 @@ const load = (file) => { const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { + if (CATALOG.isCatalogRequest(m)) return CATALOG; if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`)); if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); diff --git a/resources/tester/helper_313_clear_all.cjs b/resources/tester/helper_313_clear_all.cjs index 43801ce9a..118801d67 100644 --- a/resources/tester/helper_313_clear_all.cjs +++ b/resources/tester/helper_313_clear_all.cjs @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -24,6 +25,7 @@ const load = (file) => { const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { + if (CATALOG.isCatalogRequest(m)) return CATALOG; if (m.startsWith("@ui/")) return UI; if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`)); if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); diff --git a/resources/tester/helper_314_stale_structure.cjs b/resources/tester/helper_314_stale_structure.cjs index a660a2372..2e975655c 100644 --- a/resources/tester/helper_314_stale_structure.cjs +++ b/resources/tester/helper_314_stale_structure.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -16,6 +17,7 @@ const load = (file) => { const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { + if (CATALOG.isCatalogRequest(m)) return CATALOG; if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); return new Proxy({}, { get: () => () => {} }); diff --git a/resources/tester/helper_317_kind_change_station.cjs b/resources/tester/helper_317_kind_change_station.cjs index a26ffa7d0..6b4a74352 100644 --- a/resources/tester/helper_317_kind_change_station.cjs +++ b/resources/tester/helper_317_kind_change_station.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedStructures: [] }; @@ -32,6 +33,7 @@ const load = (file) => { const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { + if (CATALOG.isCatalogRequest(m)) return CATALOG; if (STUBS[m]) return STUBS[m]; if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); diff --git a/resources/tester/helper_317_stale_preview.cjs b/resources/tester/helper_317_stale_preview.cjs index af6868dc5..2e2fe9ab6 100644 --- a/resources/tester/helper_317_stale_preview.cjs +++ b/resources/tester/helper_317_stale_preview.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); let refreshCalls = 0; @@ -26,6 +27,7 @@ const load = (file) => { const box = { exports: {} }; cache.set(file, box.exports); const near = (m) => { + if (CATALOG.isCatalogRequest(m)) return CATALOG; if (STUBS[m]) return STUBS[m]; if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); diff --git a/resources/tester/helper_319_kind_swap.cjs b/resources/tester/helper_319_kind_swap.cjs index b26796131..88bdd05af 100644 --- a/resources/tester/helper_319_kind_swap.cjs +++ b/resources/tester/helper_319_kind_swap.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const B05 = (name) => path.join(ROOT, "B05_Profile", `${name}.ts`); @@ -65,6 +66,7 @@ function makeLoader(real, stubs = {}) { const box = { exports: {} }; cache.set(full, box.exports); const need = (spec) => { + if (CATALOG.isCatalogRequest(spec)) return CATALOG; if (spec in stubs) return stubs[spec]; let target = null; if (spec.startsWith("@util/")) target = path.join(ROOT, "common_util", `${spec.slice(6)}.ts`); diff --git a/resources/tester/helper_322_0_golden.cjs b/resources/tester/helper_322_0_golden.cjs index b60244658..39c462d16 100644 --- a/resources/tester/helper_322_0_golden.cjs +++ b/resources/tester/helper_322_0_golden.cjs @@ -134,7 +134,7 @@ const STUBS = new Map([ [file("ui_template", "ui_template_elements.ts"), { showToast: () => {} }], ]); const resolveFile = (base) => - [base, `${base}.ts`, path.join(base, "index.ts")].find( + [base, `${base}.ts`, base.replace(/\.js$/, ".ts"), path.join(base, "index.ts")].find( (p) => fs.existsSync(p) && fs.statSync(p).isFile(), ) ?? `${base}.ts`; const cache = new Map(); @@ -232,6 +232,7 @@ const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment")) const { buildStructureLane } = loadTs(B05("B05_Profile_UI_Structures_Marks")); const { pickGroups } = loadTs(B05("B05_Profile_UI_Structures_Panel_Types")); const { structureDisplayName } = loadTs(B06("B06_Section_UI_Section_Common")); +const { listPick } = loadTs(file("common_util", "common_util_structure_catalog.ts")); async function main() { const types = await fetchStructureTypes(true); @@ -239,7 +240,7 @@ async function main() { const out = { samples: {}, pick: {} }; // 고르기 목록 — 페이지가 넘기는 거르기 그대로(B05 = 켜진 것 · B06 = 전부) - const pagePicks = { b05: (type) => type.enabled, b06: () => true }; + const pagePicks = { b05: listPick("b05"), b06: listPick("b06") }; // 페이지 거르기 = 표 목록(322-1) for (const [page, pick] of Object.entries(pagePicks)) { const seen = {}; for (const group of pickGroups(types, pick)) diff --git a/resources/tester/helper_322_1_catalog.cjs b/resources/tester/helper_322_1_catalog.cjs new file mode 100644 index 000000000..f1724f663 --- /dev/null +++ b/resources/tester/helper_322_1_catalog.cjs @@ -0,0 +1,43 @@ +/* PLAN 322-1 구조물 표 거울 시험 헬퍼 — 진짜 `common_util_structure_catalog.ts` 를 그 자리에서 트랜스파일해 + * 견본 정의마다 `resolveCatalog` 한 벌 · 실제 정의로 `cardTitle` · `listPick` 을 표준출력 JSON 으로. + * 입력 = argv[2] JSON `{cases: {이름: 정의|null}, definition, titles: [[typeId, options, defaults]]}`. + * 짝 = `test_322_1_catalog_mirror.py`. + */ +const fs = require("fs"); +const path = require("path"); +const ROOT = path.join(__dirname, "..", ".."); +const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); + +function loadTs(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: {} }; + const need = (spec) => loadTs(path.join(path.dirname(full), spec)); + new Function("exports", "module", "require", js)(box.exports, box, need); + return box.exports; +} + +const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +const catalog = loadTs(path.join(ROOT, "common_util", "common_util_structure_catalog.ts")); +const rows = (map) => Object.fromEntries([...map].map(([id, row]) => [id, row])); +const out = { cases: {} }; +for (const [name, definition] of Object.entries(input.cases)) + out.cases[name] = rows(catalog.resolveCatalog(definition)); +catalog.useCatalogDefinition(input.definition); +out.titles = input.titles.map(([id, options, defaults]) => + catalog.cardTitle(id, options, defaults), +); +out.pick = Object.fromEntries( + input.definition.types.map((t) => [ + t.type_id, + [catalog.listPick("b05")(t), catalog.listPick("b06")(t)], + ]), +); +out.kinds = [...catalog.pipeTitleKinds()].sort(); +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_322_catalog_module.cjs b/resources/tester/helper_322_catalog_module.cjs new file mode 100644 index 000000000..f06833065 --- /dev/null +++ b/resources/tester/helper_322_catalog_module.cjs @@ -0,0 +1,23 @@ +/* PLAN 322-1 — 옛 도움 파일들이 함께 쓰는 구조물 표(catalog) 모듈 한 벌. + * 진짜 `common_util_structure_catalog.ts` + 부품 상자 JSON · 실제 정의(마스터 읽기만)로 세운 표(화면이 종류 + * 레지스트리를 받은 뒤와 같음). 각 도움 파일 로더가 `…common_util_structure_catalog` 요청을 이것으로 갈음함. + */ +const fs = require("fs"); +const path = require("path"); +const ROOT = path.join(__dirname, "..", ".."); +const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); +const read = (...parts) => JSON.parse(fs.readFileSync(path.join(ROOT, ...parts), "utf8")); + +const js = ts.transpileModule( + fs.readFileSync(path.join(ROOT, "common_util", "common_util_structure_catalog.ts"), "utf8"), + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }, +).outputText; +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, () => + read("common_util", "common_util_structure_parts.json"), +); +box.exports.useCatalogDefinition(read("resources", "master_template", "구조물정의.json")); + +/** 로더 요청이 구조물 표 모듈인가. */ +box.exports.isCatalogRequest = (request) => /common_util_structure_catalog(\.js)?$/.test(request); +module.exports = box.exports; diff --git a/resources/tester/helper_54_10_soil_tiers.cjs b/resources/tester/helper_54_10_soil_tiers.cjs index aea1daad9..536bc2821 100644 --- a/resources/tester/helper_54_10_soil_tiers.cjs +++ b/resources/tester/helper_54_10_soil_tiers.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -105,6 +106,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); diff --git a/resources/tester/helper_54_12_live_required.cjs b/resources/tester/helper_54_12_live_required.cjs index c0e2a1c68..7d322b0db 100644 --- a/resources/tester/helper_54_12_live_required.cjs +++ b/resources/tester/helper_54_12_live_required.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -31,6 +32,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); diff --git a/resources/tester/helper_54_27_soil_tier_field.cjs b/resources/tester/helper_54_27_soil_tier_field.cjs index 3637338be..3e60b790d 100644 --- a/resources/tester/helper_54_27_soil_tier_field.cjs +++ b/resources/tester/helper_54_27_soil_tier_field.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -152,6 +153,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); diff --git a/resources/tester/helper_b05_station_roundtrip.cjs b/resources/tester/helper_b05_station_roundtrip.cjs index fee14a6c5..ab81f8aef 100644 --- a/resources/tester/helper_b05_station_roundtrip.cjs +++ b/resources/tester/helper_b05_station_roundtrip.cjs @@ -5,6 +5,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -18,9 +19,11 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => - request.startsWith(".") - ? loadTs(path.join(path.dirname(full), `${request}.ts`)) - : require(request); + CATALOG.isCatalogRequest(request) + ? CATALOG + : request.startsWith(".") + ? loadTs(path.join(path.dirname(full), `${request}.ts`)) + : require(request); new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; diff --git a/resources/tester/helper_b05_structures_form_summary.cjs b/resources/tester/helper_b05_structures_form_summary.cjs index f295049b1..3e5bc6e52 100644 --- a/resources/tester/helper_b05_structures_form_summary.cjs +++ b/resources/tester/helper_b05_structures_form_summary.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -123,6 +124,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; // 화면 공용 부품(@ui/…) — 이 시험이 안 쓰는 것이라 뭘 불러도 아무 일 안 하는 함수로 막아 둠 if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} }); diff --git a/resources/tester/helper_b06_adjust_single_form.cjs b/resources/tester/helper_b06_adjust_single_form.cjs index 4e71e340e..ec94f4aaf 100644 --- a/resources/tester/helper_b06_adjust_single_form.cjs +++ b/resources/tester/helper_b06_adjust_single_form.cjs @@ -6,6 +6,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -101,6 +102,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; if (STUBS[request]) return STUBS[request]; if (request.startsWith("@util/")) return loadTs(path.join(ROOT, "common_util", `${request.slice(6)}.ts`)); diff --git a/resources/tester/helper_b06_draft_pipe_replay.cjs b/resources/tester/helper_b06_draft_pipe_replay.cjs index a8bd8f1e9..50718cd1b 100644 --- a/resources/tester/helper_b06_draft_pipe_replay.cjs +++ b/resources/tester/helper_b06_draft_pipe_replay.cjs @@ -7,6 +7,7 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); +const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); @@ -30,6 +31,7 @@ function loadTs(file) { const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; // 화면 공용 부품(@ui/…)은 이 시험이 안 쓰는 것이라 아무 일 안 하는 함수로 막아 둠 if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} }); if (request.startsWith("@util/")) diff --git a/resources/tester/test_272_1_station_list.py b/resources/tester/test_272_1_station_list.py index 82181a3d4..61c6c61af 100644 --- a/resources/tester/test_272_1_station_list.py +++ b/resources/tester/test_272_1_station_list.py @@ -41,6 +41,7 @@ PIPES = [ }, ] STRUCTURES = [ + # 노출형 = 구조물 표 횡단도 none(322-1 ★1) — 측점 안 심음 {"type_id": "cross_drain_exposed", "placement": "point", "chainage_m": 120.0}, { "type_id": "retaining_wall", @@ -66,7 +67,7 @@ STRUCTURES = [ }, {"type_id": "evac_road", "placement": "point", "chainage_m": 720.0}, ] -EXPECTED = [95, 120, 160, 230, 300, 395, 400, 410, 500, 510, 530, 530, 545, 560] +EXPECTED = [95, 160, 230, 300, 395, 400, 410, 500, 510, 530, 530, 545, 560] # 서버 종단 측점(맞추기 전) — 격자 0~100 · 옛 관 측점 95.38 · 구조물이 떠난 격자 자리 40(`grid`) · # 옛 판 「regular + 글」 60 · 새 목록 = 격자 위 100(구조물이 위) · 새 자리 35 diff --git a/resources/tester/test_314_stale_structure.py b/resources/tester/test_314_stale_structure.py index 0b680b005..302515919 100644 --- a/resources/tester/test_314_stale_structure.py +++ b/resources/tester/test_314_stale_structure.py @@ -78,4 +78,4 @@ def test_structure_change_reconciles_and_ford_reattach() -> None: kinds = store.split("async function withDraftPipeKinds", 1)[1].split("\n}\n", 1)[0] assert "if (!pipes) return;" in kinds assert "delete section.ford_pavement" in kinds - assert 'if (!here.length) return pipe.facility === "ford_pavement";' in kinds + assert 'if (!here.length) return shapeOf(pipe.facility) === "ford_pavement";' in kinds diff --git a/resources/tester/test_322_0_golden.py b/resources/tester/test_322_0_golden.py index d9ee6099e..69948eccf 100644 --- a/resources/tester/test_322_0_golden.py +++ b/resources/tester/test_322_0_golden.py @@ -36,7 +36,7 @@ ROOT = HERE.parent.parent GOLDEN = HERE / "golden_322_structures.json" FIXTURE = HERE / "fixtures" / "b06_73_4_body_guard_sections.json" #: 지금 단계 — ★ 칸은 `단계` 가 이 값 이하면 「다음」 값과 맞춤 -STAGE = 0 +STAGE = 1 GRID = [float(at) for at in range(0, 201, 20)] SHAPE_KEYS = ("culvert", "ford", "box", "ford_pavement", "revetment", "pavement", "underdrain") diff --git a/resources/tester/test_322_1_catalog_mirror.py b/resources/tester/test_322_1_catalog_mirror.py new file mode 100644 index 000000000..05e8f71b0 --- /dev/null +++ b/resources/tester/test_322_1_catalog_mirror.py @@ -0,0 +1,229 @@ +"""PLAN 322-1 구조물 표(catalog) — 부품 상자 · 처음값 · 파이썬 = TS 거울. + +- 거울: 실제 정의(42종) · 반영 덮어쓰기 · 부품 끔 · 정의 없음 — `resolve_catalog` = TS `resolveCatalog` 한 칸도 같음. +- 처음값 = PLAN_구조물표 4장 표(★ 넷 포함) · 부품 id 는 상자에 있음 · 형상 none → 횡단도 none(218). +- 부품 자리꼴 · 정본 = 그 종류 표 줄과 맞음(1-3 규칙 · ★ 기슭막이 interval 이라야 span3 · hidden_revet 에 맞음). +- 카드 이름 한 벌: 빈칸 = 정의 기본값(파이썬 · TS 같은 이름) · 목록 거르기(B05 both · B06 both + b06). +TS 쪽 = `helper_322_1_catalog.cjs`(Node). +""" + +from __future__ import annotations + +import copy +import json +import subprocess +import tempfile +from pathlib import Path + +import pytest + +from B05_Profile.B05_Profile_Structures_Schema import load_definition +from common_util.common_util_structure_catalog import ( + card_title, + catalog, + load_parts, + resolve_catalog, +) + +HERE = Path(__file__).parent +ROOT = HERE.parent.parent + +pytestmark = pytest.mark.skipif( + not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치" +) + +#: PLAN_구조물표 4장 — 종류 → (목록, 횡단도, 카드이름, 형상, 3D) +TABLE = { + "pipe": ("both", "anchor", "pipe", "pipe_set", "pipe"), + "box_culvert": ("both", "anchor", "name", "box", "box"), + "ford_pavement": ("both", "anchor", "name", "ford_pavement", "chute"), + "ford_bridge": ("both", "anchor", "name", "ford", "ford"), + "cross_drain_exposed": ("both", "none", "name", "none", "none"), + "open_ditch": ("both", "none", "name", "none", "none"), + "cross_underdrain": ("both", "anchor", "name", "underdrain", "surface"), + **{ + t: ("hidden", "none", "name", "none", "none") + for t in ( + "ditch_side", + "ditch_ridge", + "ditch_berm", + "chute", + "slope_drain", + "underdrain", + "erosion_check", + "bed_sill", + "check_dam_small", + "refuge", + "work_yard", + "turnaround", + ) + }, + "retaining_wall": ("both", "span3", "name", "wall", "wall"), + "masonry_wet": ("hidden", "span3", "name", "wall", "wall"), + "masonry_dry": ("hidden", "span3", "name", "wall", "wall"), + "soil_guard": ("both", "span3", "name", "wall", "wall"), + "boulder_masonry": ("hidden", "span3", "name", "wall", "wall"), + "berm": ("both", "span3", "name", "berm", "surface"), + "revetment": ("hidden", "span3", "name", "hidden_revet", "wall"), + **{ + t: ("both", "none", "name", "none", "none") + for t in ( + "evac_road", + "mirror", + "curb", + "hazard_sign", + "position_sign", + "water_intake", + "barrier_gate", + "completion_sign", + "guardrail", + "borrow_pit", + "etc", + ) + }, + "spoil_bank": ("both", "none", "name", "spoil", "surface"), + "wildlife_path": ("b06", "none", "name", "none", "none"), + "revegetation": ("b06", "none", "name", "none", "none"), + "pavement_concrete": ("both", "none", "name", "pavement", "surface"), + "gravel_surfacing": ("b06", "none", "name", "pavement", "surface"), +} +FORMS = { + "retaining_wall": {"form": "콘크리트", "weep": True}, + "masonry_wet": {"form": "돌쌓기(찰)"}, + "masonry_dry": {"form": "돌쌓기(메)"}, + "soil_guard": {"form": "통나무·목재틀"}, + "boulder_masonry": {"form": "돌쌓기(메)"}, +} + + +def _cases() -> dict: + real = load_definition() + reflected = copy.deepcopy(real) + reflected["반영"] = { + "open_ditch": {"횡단도": "anchor", "목록": "b06"}, + "pipe": {"카드이름": "name", "3D": "none"}, + "soil_guard": {"형상칸": {"form": "돌쌓기(찰)", "weep": True}}, + "etc": {"목록": "hidden"}, + "wildlife_path": {"목록": "both"}, + } + parts_off = copy.deepcopy(real) + parts_off["부품"] = {"형상.wall": False, "횡단도.span3": False, "카드이름.pipe": False} + renamed = copy.deepcopy(real) + renamed["구조물군"] = dict(reversed(list(real["구조물군"].items()))) + for item in renamed["types"]: + if item["type_id"] == "berm": + item["name"] = "소단(새 이름)" + item["enabled"] = False + return { + "real": real, + "reflected": reflected, + "parts_off": parts_off, + "renamed": renamed, + "none": None, + } + + +TITLES = [ + ["pipe", {}, None], + ["pipe", {"pipe_kind": "흄관", "pipe_diameter_mm": "800"}, None], + ["pipe", {"pipe_diameter_mm": ""}, None], + ["pipe", {"pipe_diameter_mm": 1200.7}, None], + ["box_culvert", {}, None], + ["retaining_wall", {"form": "콘크리트"}, None], +] + + +@pytest.fixture(scope="module") +def ts() -> dict: + definition = load_definition() + defaults = { + t["type_id"]: {o["key"]: o.get("default") for o in t.get("options", [])} + for t in definition["types"] + } + titles = [[tid, options, defaults[tid]] for tid, options, _ in TITLES] + payload = {"cases": _cases(), "definition": definition, "titles": titles} + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / "input.json" + path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") + out = subprocess.run( + ["node", str(HERE / "helper_322_1_catalog.cjs"), str(path)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + check=True, + ) + return json.loads(out.stdout) + + +def test_mirror_python_equals_ts(ts: dict) -> None: + for name, definition in _cases().items(): + assert resolve_catalog(definition) == ts["cases"][name], name + assert len(ts["cases"]["real"]) == 42 + + +def test_start_values_match_plan_table() -> None: + rows = resolve_catalog(load_definition()) + assert set(rows) == set(TABLE) + for type_id, (listed, station, title, shape, model3d) in TABLE.items(): + row = rows[type_id] + got = (row["list"], row["station"], row["title"], row["shape"], row["model3d"]) + assert got == (listed, station, title, shape, model3d), type_id + assert row["shapeOpts"] == FORMS.get(type_id, {}), type_id + assert rows["revetment"]["placement"] == "interval" # ★ 표만(정의 파일 = point 그대로) + assert [t["placement"] for t in load_definition()["types"] if t["type_id"] == "revetment"] == [ + "point" + ] + + +def test_parts_box_is_consistent() -> None: + parts = load_parts() + branches = {"횡단도", "카드이름", "형상", "3D"} + assert set(parts["parts"]) == branches + for type_id, start in parts["처음값"].items(): + for branch in branches: + value = start[branch] + assert value in parts["parts"][branch] or value in ("none", "name"), (type_id, value) + if start["형상"] == "none": # 그림 없는 종류는 횡단도 안 만듦(218) + assert start["횡단도"] == "none", type_id + rows = resolve_catalog(load_definition()) + for type_id, row in rows.items(): # 부품 자리꼴 · 정본 = 종류 표 줄(1-3) + for branch, value in (("횡단도", row["station"]), ("형상", row["shape"])): + part = parts["parts"][branch].get(value) + if part is None: + continue + assert row["placement"] in part["자리꼴"], (type_id, branch, value) + assert row["owner"] in part["정본"], (type_id, branch, value) + + +def test_reflect_and_parts_off_win() -> None: + cases = _cases() + reflected = resolve_catalog(cases["reflected"]) + assert reflected["open_ditch"]["station"] == "anchor" + assert reflected["open_ditch"]["list"] == "b06" + assert reflected["pipe"]["title"] == "name" and reflected["pipe"]["model3d"] == "none" + assert reflected["soil_guard"]["shapeOpts"] == {"form": "돌쌓기(찰)", "weep": True} + assert reflected["wildlife_path"]["list"] == "both" + off = resolve_catalog(cases["parts_off"]) + assert off["retaining_wall"]["shape"] == "none" and off["retaining_wall"]["station"] == "none" + assert off["pipe"]["title"] == "name" and off["pipe"]["station"] == "anchor" + renamed = resolve_catalog(cases["renamed"]) + assert renamed["berm"]["name"] == "소단(새 이름)" and renamed["berm"]["list"] == "b06" + assert renamed["etc"]["groupOrder"] == 0 # 군 차례 = 구조물군 열쇠 차례 + + +def test_card_title_one_formula(ts: dict) -> None: + py = [card_title(tid, options) for tid, options, _ in TITLES] + assert py == ts["titles"] + assert py[0] == "파형강관 D1000" and py[2] == "파형강관 D1000" # 빈칸 = 정의 기본값(옛 D800) + assert py[1] == "흄관 D800" and py[3] == "파형강관 D1200" + assert py[4] == "BOX암거" and py[5] == "옹벽" + assert ts["kinds"] == sorted({"흄관", "VR관", "파형강관"}) + + +def test_list_pick_pages(ts: dict) -> None: + rows = catalog() + for type_id, (b05, b06) in ts["pick"].items(): + listed = rows[type_id]["list"] + assert b05 == (listed == "both"), type_id + assert b06 == (listed in ("both", "b06")), type_id diff --git a/resources/tester/test_52_7_safety.py b/resources/tester/test_52_7_safety.py index 824382bef..5f6c26ef7 100644 --- a/resources/tester/test_52_7_safety.py +++ b/resources/tester/test_52_7_safety.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """안전부대용지(표 07) — PLAN 52-7. -정의(대피로 · 가드레일 컨테이너 · 점형 일곱) · 옛 대피 종류 읽기(`asRoad` · `pickable`) · -화면은 ORCA `ui_checks/52-7_안전부대용지.js`. +정의(대피로 · 가드레일 컨테이너 · 점형 일곱) · 옛 대피 종류 읽기(`asRoad`) · 고르기 목록 숨김 = +구조물 표 처음값(322-1 · 옛 `pickable`) · 화면은 ORCA `ui_checks/52-7_안전부대용지.js`. """ import json @@ -10,6 +10,7 @@ import os import subprocess from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from common_util.common_util_structure_catalog import load_parts from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition HERE = os.path.dirname(os.path.abspath(__file__)) @@ -77,8 +78,6 @@ const d = m.exports; process.stdout.write(JSON.stringify({ old: d.asRoad({ type_id: "turnaround", options: { width_m: 10 } }), other: d.asRoad({ type_id: "mirror", options: { facing: "좌" } }), - pick: ["refuge", "work_yard", "turnaround", "evac_road", "guardrail"].map( - (type_id) => d.pickable({ type_id })), })); """ @@ -98,4 +97,6 @@ def test_옛_대피_종류는_대피로_상세로_읽고_목록에서_뺌(): "options": {"detail": "차돌림곳", "width_m": 10}, } assert out["other"] == {"type_id": "mirror", "options": {"facing": "좌"}} - assert out["pick"] == [False, False, False, True, True] + start = load_parts()["처음값"] # 고르기 목록 = 구조물 표(322-1 · 옛 대피 셋 hidden) + kinds = ("refuge", "work_yard", "turnaround", "evac_road", "guardrail") + assert [start[t]["목록"] for t in kinds] == ["hidden"] * 3 + ["both"] * 2 diff --git a/resources/tester/test_b05_extra_stations_supplier.py b/resources/tester/test_b05_extra_stations_supplier.py index f6d078ee0..18b397cd6 100644 --- a/resources/tester/test_b05_extra_stations_supplier.py +++ b/resources/tester/test_b05_extra_stations_supplier.py @@ -82,9 +82,10 @@ def test_pipe_points_become_labelled_stations(tmp_path): assert resolve_extra_stations(tmp_path, parse_pipe_points(LIVE_POINTS)) == LIVE_LABELS -def test_pipe_without_options_falls_back_to_config_defaults(tmp_path): +def test_pipe_without_options_falls_back_to_definition_defaults(tmp_path): + """빈칸 = 정의 기본값(322-1 관 공식 한 벌 · 옛 설정값 D800 ↔ 화면 D1000 어긋남 없앰).""" extras = resolve_extra_stations(tmp_path, parse_pipe_points([{"chainage_m": 30.0}])) - assert extras == ((30.0, "파형강관 D800"),) + assert extras == ((30.0, "파형강관 D1000"),) def test_ford_pavement_plants_its_station(tmp_path): @@ -106,7 +107,8 @@ def test_group_a_point_and_revetment_facility_join(tmp_path): """ _write_structures( tmp_path, - [{"type_id": "cross_drain_exposed", "chainage_m": 60.0, "placement": "point"}], + # 측점 심는 A군 점형 = 맹암거(노출형은 322-1 ★1 로 횡단도 none) + [{"type_id": "cross_underdrain", "chainage_m": 60.0, "placement": "point"}], ) points = [ *LIVE_POINTS, @@ -119,7 +121,7 @@ def test_group_a_point_and_revetment_facility_join(tmp_path): }, ] extras = resolve_extra_stations(tmp_path, parse_pipe_points(points)) - assert (60.0, "노출형 횡단수로") in extras + assert (60.0, "맹암거") in extras assert [chainage for chainage, label in extras if label == "기슭막이"] == [95.0, 100.0, 115.0] assert [chainage for chainage, _label in extras] == [ 60.0, diff --git a/resources/tester/test_b05_station_roundtrip.py b/resources/tester/test_b05_station_roundtrip.py index 0c82da4d2..2df3f71e0 100644 --- a/resources/tester/test_b05_station_roundtrip.py +++ b/resources/tester/test_b05_station_roundtrip.py @@ -83,7 +83,7 @@ def test_사용자_구조물_119_8_저장_다시_읽기_그대로(tmp_path): from B05_Profile.B05_Profile_Structures_Schema import StructureInstance item = StructureInstance( - type_id="cross_drain_exposed", + type_id="cross_underdrain", # 측점 심는 점형(노출형은 322-1 ★1 로 횡단도 none) placement="point", chainage_m=119.8, memo="메모만", diff --git a/resources/tester/test_b06_structure_walls_mirror.py b/resources/tester/test_b06_structure_walls_mirror.py index d16dc199c..fd758950e 100644 --- a/resources/tester/test_b06_structure_walls_mirror.py +++ b/resources/tester/test_b06_structure_walls_mirror.py @@ -81,7 +81,7 @@ writeFileSync(process.argv[3], JSON.stringify({ specs, sections })); def _python_result() -> dict: from B06_Section.B06_Section_Engine_Structures_Wall import ( - _FORM_BY_TYPE, + _wall_form, attach_wall_structures, tier_extras, ) @@ -102,7 +102,8 @@ def _python_result() -> dict: "start_m": float(min(start, end)), "end_m": float(max(start, end)), "anchor_m": float(item["chainage_m"]), - "form": options.get("form") or _FORM_BY_TYPE.get(item["type_id"]), + # 빈 형태 = 구조물 표 형상칸.form(322-1 · 옛 `_FORM_BY_TYPE`) + "form": options.get("form") or _wall_form(item["type_id"]), "height_m": options.get("height_m"), "side": options.get("side"), # 기초 축(2026-09-09 신설) — 저장 칸과 같은 글자. 초안 경로에서 빠지면 diff --git a/resources/tester/test_b06_wall_drain_pipe.py b/resources/tester/test_b06_wall_drain_pipe.py index 62502cca2..f55524466 100644 --- a/resources/tester/test_b06_wall_drain_pipe.py +++ b/resources/tester/test_b06_wall_drain_pipe.py @@ -122,6 +122,13 @@ def test_옹벽만_높이_15m_띠마다_파이프_하나(tmp_path: Path) -> None def test_옹벽_표식은_구조물_종류로() -> None: source = (B06 / "B06_Section_UI_Cross_Revetment.ts").read_text(encoding="utf-8") - assert 'spec.type_id === "retaining_wall"' in source + # 물빼기 표식 = 구조물 표 형상칸.weep(322-1) — 처음값은 옹벽만 + assert "catalogRow(spec.type_id)?.shapeOpts.weep" in source + start = json.loads( + (PROJECT_ROOT / "common_util" / "common_util_structure_parts.json").read_text("utf-8") + ) + assert [t for t, v in start["처음값"].items() if v.get("형상칸", {}).get("weep")] == [ + "retaining_wall" + ] css = (B06 / "B06_Section_UI_Style_Cross_Areas.css").read_text(encoding="utf-8") assert re.search(r"\.b06-chart__wall-drain line \{[^}]*var\(--color-", css), "색 토큰 없음"