feat(구조물표): 322-1 부품 상자 · 표 읽기 — 목록 · 측점 · 카드 이름 · 형상 고르기 · 3D 몸체 · 알약 띠가 종류 키 · 군 글자 · 자리꼴 · managed_by 대신 구조물 표(catalog) 한 줄을 읽음 · 코드 숨김 목록 걷음 · ★ 넷 + 관 공식 한 벌

- 부품 상자 common_util_structure_parts.json(갈래마다 부품 · 읽는 칸 · 자리꼴 · 정본 · 42종 처음값) · 표 읽기 common_util_structure_catalog.ts / .py 짝(정의 반영 ⊕ 처음값 ⊕ 부품 끔) · 거울 시험 test_322_1_catalog_mirror
- 목록: pickGroups 군 차례 = 구조물군 열쇠 · 페이지 거르기 listPick(B05 both · B06 both + b06) · HIDDEN_PICK_GROUPS · HIDDEN_PICK_TYPES · pickable 걷음(옛 대피 셋 asRoad 그대로)
- 측점: structureStationMarks · pipeStationMarks · is_station_planting_type · resolve_extra_stations = 표 횡단도(anchor · span3 · none)
- 카드 이름: cardTitle / card_title 한 공식(빈칸 = 그 종류 정의 기본값) · 표시 이름이 정의 관종도 앎
- 형상: 벽 = shape wall(형태 = 형상칸.form · 물빼기 = 형상칸.weep) · 포장 = shape pavement(「사용 중」 거르기 없앰) · 맹암거 · 사토 · 관 지점 시설 세트 = shape
- 3D 몸체 = 그림 자료 주인 종류의 표 3D 부품 · 알약 띠 = 표 자리꼴 구간(관 지점 시설은 알약만)
- ★1 노출형 · 개거 횡단도 none · ★2 쇄석 화면에서도 포장층 · ★3 기슭막이 표 자리꼴 interval(정의 파일 그대로) · ★4 코드 숨김 10종 list hidden · ★5 관 빈칸 D800 → 정의 기본 D1000
- 옛 시험: 도움 파일 로더 27개가 표 모듈 공용 한 벌(helper_322_catalog_module) 씀 · 기대값 고침(272_1 · b05_extra_stations · roundtrip · 314 · 52_7 · walls 거울 · wall_drain_pipe)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 15:09:45 +09:00
co-authored by Claude Opus 5.5
parent 863abc7766
commit bbd6254892
70 changed files with 1384 additions and 207 deletions
@@ -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<StructureT
.then(async (payload) => {
const defined = await definition;
typesVersion = defined?.판 ?? null;
useCatalogDefinition({ types: payload.types, ...defined }); // 구조물 표(322-1 · 반영 · 부품)
return withDefinition(payload.types, defined);
})
.catch((error) => {
+13 -18
View File
@@ -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(),)
+7 -12
View File
@@ -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]:
@@ -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) =>
@@ -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");
}
@@ -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(
@@ -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<Record<string, string>> = {
@@ -225,9 +225,6 @@ export const ROAD_DETAILS: Readonly<Record<string, string>> = {
turnaround: "차돌림곳",
};
/** 고르기 목록에 낼 종류인지 — 옛 대피 종류만 뺌. */
export const pickable = (type: { type_id: string }): boolean => !ROAD_DETAILS[type.type_id];
/** 옛 대피 종류 기록 → 대피로 + 상세(옛 값 그대로 · 상세만 채움). 다른 기록은 그대로. */
export function asRoad<T extends { type_id: string; options?: Record<string, string | number> }>(
record: T,
@@ -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);
@@ -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 : "";
@@ -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<string, string> = {
호환: "기타",
};
/** 새로 놓을 때 고르기 목록(군 드롭다운 · 우클릭 메뉴)에서 뺄 군 — 종단배수·계류사방(표 07).
* 옛 값 · 도면 · 집계는 그대로이고 데이터도 지우지 않는다. */
export const HIDDEN_PICK_GROUPS: ReadonlySet<string> = new Set(["B", "D"]);
/** 고르기 목록에서 뺄 종류 — 사면안정은 흙막이 · 옹벽 · 소단만(54-31). 옛 값은 그대로. */
export const HIDDEN_PICK_TYPES: ReadonlySet<string> = 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<StructureType>,
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` 그대로. */
+9 -13
View File
@@ -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)
+2 -3
View File
@@ -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):
@@ -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`). 사용자가 정하고 싶어지면 그때 칸을 낸다.
+2 -2
View File
@@ -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"))
+6 -11
View File
@@ -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,
+23 -28
View File
@@ -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<Record<string, unknown>>,
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<string, (typeof FACILITY_KEYS)[number]> = {
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<string, unknown> | 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<string, unknown>) =>
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<string, unknown> | 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));
}
@@ -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 사용자 확정).
@@ -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;
+5 -1
View File
@@ -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(
@@ -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),
});
}
+13 -12
View File
@@ -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<string, unknown>;
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);
}
@@ -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}"
@@ -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<string, unknown>;
/** 3D 부품 — none · pipe · box · ford · chute · wall · surface. */
model3d: string;
color: string | null;
}
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
type Reflect = Partial<Record<Branch | "목록", string>> & {
형상칸?: Record<string, unknown>;
자리꼴?: string;
};
/** 정의 한 벌 중 표가 읽는 칸 — `반영` · `부품` 은 M02 [저장] 때 생김(없으면 처음값). */
export interface CatalogDefinition {
구조물군?: Record<string, string>;
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<string, Reflect>;
부품?: Record<string, boolean>;
}
interface Parts {
parts: Record<Branch, Record<string, unknown>>;
처음값: Record<string, Reflect>;
}
/** 갈래 → 부품을 끈 값(`부품.{갈래}.{id} === false`). */
const OFF: Record<Branch, string> = {
횡단도: "none",
카드이름: "name",
형상: "none",
"3D": "none",
};
/** 정의 + 부품 상자 → 종류마다 한 줄(정의 types 차례 · 정의가 없으면 처음값 차례). */
export function resolveCatalog(
definition: CatalogDefinition | null,
parts: Parts = PARTS as unknown as Parts,
): Map<string, CatalogRow> {
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<string, CatalogRow>();
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<string, unknown> | null | undefined,
defaults: Record<string, unknown> = {},
): 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;
@@ -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" }
}
}
@@ -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 노면공 정본을 읽지 못했습니다 (없는 것으로 본다)")
+12 -8
View File
@@ -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` 의 누가거리. */
@@ -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 [
{
+9 -8
View File
@@ -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<string, string> = {
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,
// 기초 축 — 저장 칸과 **같은 글자**. 초안 경로에서 빠지면 터파기가 안 그려진다.
@@ -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))
@@ -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: () => () => {} });
@@ -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) },
@@ -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`));
@@ -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))
@@ -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))
@@ -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")),
@@ -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}`);
};
@@ -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`));
@@ -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))
@@ -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`))
@@ -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}`);
};
@@ -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 = {};
@@ -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);
@@ -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];
});
@@ -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}`);
};
@@ -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}`);
};
@@ -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`));
@@ -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`));
@@ -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: () => () => {} });
@@ -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`));
@@ -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`));
@@ -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`);
+3 -2
View File
@@ -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))
+43
View File
@@ -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));
@@ -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;
@@ -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`));
@@ -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: () => () => {} });
@@ -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`));
@@ -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;
@@ -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: () => () => {} });
@@ -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`));
@@ -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/"))
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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")
@@ -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
+6 -5
View File
@@ -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
@@ -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,
@@ -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="메모만",
@@ -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 신설) — 저장 칸과 같은 글자. 초안 경로에서 빠지면
+8 -1
View File
@@ -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), "색 토큰 없음"