feat(structure): 65-1~5 세월교 · 박스암거 유입 · 유출 흙막이 양식 공통 · 없음 = 램프 뺌 · 세월교 기본값 · 물넘이 월류 폭 · 높이

- 흙막이 양식 한 벌(design_structure_guard) — 배관 · 박스 · 세월교가 같은 칸(형태 · 길이 · 높이 · 측점 전후) · 추가 흙막이 · 도수로/돌붙임 상자
- 세월교 · 박스 유입 · 유출: 흙막이 고르면 흙막이 칸(키 {side}_revet_* · 기본 찰 10 · 1.5 · 5 · 5) · [연동][경사] 옵션 줄
- B06 측벽 높이 · 좌우 · 상하 · 바닥판 읽기 줄을 세월교 · 박스 유입 · 유출 칸에 안 옮김(강조만)
- 유입 · 유출 「없음」 = 목록 램프 · 구조물 조합 잇기 줄 없음(명세 none)
- 세월교: 수량(련) 관경 바로 뒤 · 련 기본 2 · 새 시설 빈 월류 높이 = 유량 오면 필요 수심 · 종류 바꿀 때 유량 넘김
- 물넘이 포장: 경사각 기본 3° · 월류 폭 · 높이 칸(세월교와 같은 키 ford_width_m · ford_height_m · 같은 계산)
- 구조물정의 판 18(판_17 이력) · 이름 표 · 집계표 선택 컨테이너 짝 · 저장 병합 키
- 시험: 세월교 · 박스 · 물넘이 명세 기대값 · 흙막이 짝 자리 옮김

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
2026-10-06 23:09:38 +09:00
co-authored by Claude Opus 5.5
parent d42efb162b
commit d7faee1cd2
24 changed files with 4174 additions and 209 deletions
+36 -28
View File
@@ -2,17 +2,20 @@
* design_structure_box.ts
* 횡단배수 › 박스암거 명세 — 사용자 구조물 표 02 고친 판(`docs/raw/구조물정의_표/02_횡단배수_박스암거.md`) 그대로.
*
* 컨테이너 셋(모두 필수): 본체 · 유입 구조 · 유출 구조. 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 컨테이너: 본체 · 유입 구조 · 유출 구조(필수) · 흙막이(선택 · 여러 개) · 도수로/돌붙임(선택 · 하나 · 65-2).
* 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리):
* · 본체 = `body_width_m` · `body_height_m` 수 — 규격 칸은 저장 안 함(폭 · 높이로 되찾음 · `bodyOf`)
* · 사용자 입력에 두 칸 다 비면 `body_custom` = 「사용자 입력」 표시만
* · 유입 · 유출 구조 = `wing_in` · `wing_out`(없음 · 날개벽 · 흙막이 · 옛 「있음」 = 날개벽)
* — 엔진은 날개벽만 세움(`_wing_spec` ↔ `wingSpec` 짝)
* — 엔진은 날개벽만 세움(`_wing_spec` ↔ `wingSpec` 짝) · 흙막이 = 배관과 같은 양식(`{side}_revet_*` · 65-2) ·
* 「없음」 = 목록 램프 · 잇기 줄 없음(65-3)
* 표 02 에 없는 옛 날개벽 치수 칸은 화면에서 빼고 값은 남김(지난 정의 값).
* import 없음(꼴만) — 시험이 이 파일만 옮겨 돌림.
* import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림.
* ========================================================================== */
import type { Instance, SpecItem, StructureSpec } from "./design_structure_spec";
import type { Instance, SpecContainer, SpecItem, StructureSpec } from "./design_structure_spec";
import { CHUTE_ADD, guardItems, revetKeys, SOIL_ADD } from "./design_structure_guard";
export const BOX_CUSTOM = "사용자 입력";
/** 사용자 입력에 가로 · 세로 둘 다 빈 채 저장했다는 표시(폭 · 높이 수가 없을 때만 적음). */
@@ -23,16 +26,31 @@ const BOX_PRESETS: Record<string, readonly [number, number]> = {
"3.0×3.0": [3, 3],
};
/** 유입 · 유출 구조 — 같은 선택지 · 키만 다름. */
const structureItem = (key: string): SpecItem => ({
id: "structure",
label: "구조",
kind: "select",
key,
choices: [
{ value: "없음", label: "없음" },
{ value: "날개벽", label: "날개벽" },
{ value: "흙막이", label: "흙막이" },
/** 유입 · 유출 구조 — 같은 선택지 · 키만 다름 · 흙막이면 흙막이 칸(65-2). */
const guardContainer = (
id: "inlet" | "outlet",
title: string,
link: string,
key: string,
): SpecContainer => ({
id,
title,
required: true,
none: "없음",
link,
items: [
{
id: "structure",
label: "구조",
kind: "select",
key,
choices: [
{ value: "없음", label: "없음" },
{ value: "날개벽", label: "날개벽" },
{ value: "흙막이", label: "흙막이" },
],
},
...guardItems({ 흙막이: revetKeys(id) }, undefined, true),
],
});
@@ -70,20 +88,10 @@ export const BOX_SPEC: StructureSpec = {
customSide("height", "세로 (m)", "body_height_m"),
],
},
{
id: "inlet",
title: "유입 구조",
required: true,
link: "gboxin",
items: [structureItem("wing_in")],
},
{
id: "outlet",
title: "유출 구조",
required: true,
link: "gboxout",
items: [structureItem("wing_out")],
},
guardContainer("inlet", "유입 구조", "gboxin", "wing_in"),
guardContainer("outlet", "유출 구조", "gboxout", "wing_out"),
SOIL_ADD,
CHUTE_ADD,
],
// 표 02 에 없는 옛 날개벽 치수 — 화면에서 빼고 값은 남김(지난 정의 값)
retired: ["in", "out"].flatMap((side) =>
+13 -5
View File
@@ -1,15 +1,18 @@
/* =============================================================================
* design_structure_ford.ts
* 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) 그대로.
* 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) + 65장.
*
* 컨테이너 셋: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 새 키 = 유입 · 유출 구조
* 컨테이너: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수) · 흙막이(선택 · 여러 개) · 도수로/돌붙임(선택 · 하나).
* 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈.
* 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 유입 · 유출 구조
* (`inlet_type` · `outlet_type` = 없음 · 날개벽 · 흙막이) · 고르면 옛 날개벽 설치 칸(`wing_in` · `wing_out`)도 함께 적음.
* 흙막이 = 배관과 같은 양식(`design_structure_guard` · 키 `{side}_revet_*` · 65-2) · 구조 「없음」 = 목록 램프 · 잇기 줄 없음(65-3).
* 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래로는 안 받음) — 계산은 `design_structure_ford_calc`.
* import 없음 — 시험이 이 파일만 옮겨 돌림.
* import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림.
* ========================================================================== */
import type { SpecContainer, SpecChoice, StructureSpec } from "./design_structure_spec";
import { CHUTE_ADD, guardItems, revetKeys, SOIL_ADD } from "./design_structure_guard";
/** 유입 · 유출 구조 — 날개벽만 옛 설치 칸이 「있음」. */
const guard = (side: "in" | "out"): SpecChoice[] => [
@@ -22,6 +25,7 @@ const guardContainer = (id: "inlet" | "outlet", title: string, link: string): Sp
id,
title,
required: true,
none: "없음",
link,
items: [
{
@@ -31,6 +35,7 @@ const guardContainer = (id: "inlet" | "outlet", title: string, link: string): Sp
key: `${id}_type`,
choices: guard(id === "inlet" ? "in" : "out"),
},
...guardItems({ 흙막이: revetKeys(id) }, undefined, true),
],
});
@@ -51,13 +56,16 @@ export const FORD_SPEC: StructureSpec = {
asNumber: true,
choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })),
},
{ id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 }, // 관경 바로 뒤(65-4)
// 월류 폭 · 높이 = 물넘이 포장과 같은 키 · 같은 계산(65-5 · B06 횡단이 읽음)
{ id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 },
{ id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 },
{ id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 },
],
},
guardContainer("inlet", "유입 구조", "gfordin"),
guardContainer("outlet", "유출 구조", "gfordout"),
SOIL_ADD,
CHUTE_ADD,
],
// 표 04 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
retired: [
@@ -2,8 +2,9 @@
* design_structure_ford_pavement.ts
* 횡단배수 › 물넘이 포장 명세 — 사용자 구조물 표 03(`docs/raw/구조물정의_표/03_횡단배수_물넘이포장.md`) 그대로.
*
* 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도) · 도수로/돌붙임(선택 · 구조 = 도수로 · 돌붙임).
* 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_structure`.
* 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도 + 월류 폭 · 높이 · 65-5) · 도수로/돌붙임(선택 · 구조 = 도수로 · 돌붙임).
* 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_structure` ·
* 월류 폭 · 높이 = 세월교와 같은 키(`ford_width_m` · `ford_height_m`) · 같은 계산(`design_structure_ford_calc`).
* 경사각도는 옛 「바닥 경사(%)」(`ford_slope_pct` · B06 파임 기울기)와 뜻이 같은지 또렷하지 않아 잇지 않음.
* import 없음 — 시험이 이 파일만 옮겨 돌림.
* ========================================================================== */
@@ -20,6 +21,8 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = {
link: "gfordpaveslope",
items: [
{ id: "angle", label: "경사각도 (°)", kind: "number", key: "ford_slope_deg", step: 0.1 },
{ id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 },
{ id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 },
],
},
{
@@ -43,5 +46,5 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = {
},
],
// 표 03 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
retired: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"],
retired: ["ford_slope_pct", "thickness_cm", "length_m"],
};
+5 -2
View File
@@ -16,6 +16,7 @@ import {
choiceLabel,
choicesOf,
containerTitle,
isNone,
itemLabel,
linkOf,
readState,
@@ -190,11 +191,13 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
for (const r of ["length", "before", "after"] as SplitRole[])
values[role(r).id] = solved[r] ?? "";
}
// 위 항목이 바뀌면 갈래(선택지 · 저장 키)가 달라짐 — 선택지 밖 값은 비움
// 위 항목이 바뀌면 갈래(선택지 · 저장 키)가 달라짐 — 선택지 밖 값은 비움 · 새로 선 빈 칸은 기본값(65-2)
for (const child of c.items.filter((i) => i.by?.item === item.id)) {
const allowed = choicesOf(child, values);
if (child.kind === "select" && !allowed.some((ch) => ch.value === values[child.id]))
values[child.id] = "";
if (child.default && !values[child.id] && branchOf(child, values))
values[child.id] = child.default;
}
paint();
hooks.onChange?.(c, item);
@@ -252,7 +255,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
}
box.body.append(slot);
}
if (links) {
if (links && !isNone(c, values)) {
const key = linkOf(c, index + 1);
const row = buildSummaryRow();
row.root.querySelector(".b05-structure__summary-label")!.textContent = "구조물 조합";
+114
View File
@@ -0,0 +1,114 @@
/* =============================================================================
* design_structure_guard.ts
* 흙막이 공통 양식 — 배관(표 01) · 박스암거(표 02) · 세월교(표 04)가 같은 칸을 씀(PLAN 65-2).
*
* · 흙막이 = 형태 6 · 길이 · 높이 · 기준측점 전 · 후(길이 = 전 + 후 맞물림)
* · 필수 보호공(유입 · 유출)은 구조 갈래마다 저장 키(`{side}_revet_*` 옛 키) · 선택 흙막이 · 도수로/돌붙임은
* 인스턴스 키(`soil_add_{n}_*` · `chute_add_{n}_*`)
* import = 꼴만 — 시험이 이 폴더 파일만 옮겨 돌림.
* ========================================================================== */
import type { SpecChoice, SpecContainer, SpecItem } from "./design_structure_spec";
export const SOIL_FORMS: SpecChoice[] = [
{ value: "돌쌓기(찰)", label: "돌 쌓기(찰)" },
{ value: "돌쌓기(메)", label: "돌 쌓기(메)" },
{ value: "콘크리트", label: "콘크리트" },
{ value: "돌망태", label: "돌망태" },
{ value: "통나무·목재틀", label: "통나무/목재틀" },
{ value: "바자", label: "바자" },
];
/** 흙막이 기본값(54-36 · 사면안정 흙막이 정의 기본값과 같음). */
const SOIL_DEFAULTS: Record<string, string> = {
form: "돌쌓기(찰)",
length: "10",
height: "1.5",
before: "5",
after: "5",
};
/** 형태 · 길이 · 높이 · 전 · 후 — 필수 보호공은 구조 갈래마다 옛 키(`keys`) · 선택 흙막이는 인스턴스 키.
* `forms` = 갈래별 형태 선택지(배관 집수정 등) · `defaults` = 새 상자 · 흙막이를 고를 때 칸에 실제 값. */
export function guardItems(
keys?: Record<string, Record<string, string>>,
forms?: Record<string, SpecChoice[]>,
defaults = !keys,
): SpecItem[] {
const by = (field: string, choices?: Record<string, SpecChoice[]>) =>
keys
? {
item: "structure",
map: Object.fromEntries(
// 키 없는 갈래 = 그 구조에 없는 칸(집수정 길이 · 전 · 후) — 가림
Object.entries(keys)
.filter(([, k]) => k[field])
.map(([structure, k]) => [
structure,
{ key: k[field], choices: choices?.[structure] },
]),
),
}
: undefined;
const items: SpecItem[] = [
{ id: "form", label: "형태", kind: "select", choices: SOIL_FORMS, by: by("form", forms) },
{ id: "length", label: "길이 (m)", kind: "number", split: "length", step: 1, by: by("length") },
{ id: "height", label: "높이 (m)", kind: "number", step: 0.1, by: by("height") },
{
id: "before",
label: "기준측점 전 (m)",
kind: "number",
split: "before",
step: 0.5,
by: by("before"),
},
{
id: "after",
label: "기준측점 후 (m)",
kind: "number",
split: "after",
step: 0.5,
by: by("after"),
},
];
return defaults ? items.map((item) => ({ ...item, default: SOIL_DEFAULTS[item.id] })) : items;
}
export const revetKeys = (side: "inlet" | "outlet"): Record<string, string> => ({
form: `${side}_revet_form`,
length: `${side}_revet_length_m`,
height: `${side}_revet_height_m`,
before: `${side}_revet_before_m`,
after: `${side}_revet_after_m`,
});
/** 추가 흙막이(선택 · 여러 개) — 사면안정 흙막이(표 09 · 54-26)와 같은 칸 · 구조 고르기 없음(54-30). */
export const SOIL_ADD: SpecContainer = {
id: "soil_add",
title: "흙막이",
required: false,
prefix: "soil_add",
link: "gsoiladd",
items: guardItems(),
};
/** 도수로/돌붙임(선택 · 하나만 · 54-48) — 옛 둘 이상은 보이되 [추가]만 숨김. */
export const CHUTE_ADD: SpecContainer = {
id: "chute_add",
title: "도수로/돌붙임",
required: false,
prefix: "chute_add",
link: "gchuteadd",
max: 1,
items: [
{
id: "structure",
label: "구조",
kind: "select",
choices: [
{ value: "도수로", label: "도수로" },
{ value: "돌붙임", label: "돌붙임" },
],
},
],
};
+8 -102
View File
@@ -13,6 +13,7 @@
* ========================================================================== */
import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec";
import { CHUTE_ADD, guardItems, revetKeys, SOIL_ADD, SOIL_FORMS } from "./design_structure_guard";
import { BOX_SPEC } from "./design_structure_box";
import { FORD_SPEC } from "./design_structure_ford";
import { FORD_PAVEMENT_SPEC } from "./design_structure_ford_pavement";
@@ -20,83 +21,14 @@ import { ETC_SPEC, PAVEMENT_SPEC } from "./design_structure_road";
const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이
const BASIN = "집수정";
const SOIL_FORMS: SpecChoice[] = [
{ value: "돌쌓기(찰)", label: "돌 쌓기(찰)" },
{ value: "돌쌓기(메)", label: "돌 쌓기(메)" },
{ value: "콘크리트", label: "콘크리트" },
{ value: "돌망태", label: "돌망태" },
{ value: "통나무·목재틀", label: "통나무/목재틀" },
{ value: "바자", label: "바자" },
];
const BASIN_FORMS: SpecChoice[] = [
{ value: "I", label: "I형" },
{ value: "L", label: "L형" },
{ value: "U", label: "ㄷ형" },
];
/** 추가 흙막이 기본값(54-36 · 사면안정 흙막이 정의 기본값과 같음) — 필수 보호공 기본값은 정의 파일. */
const SOIL_ADD_DEFAULTS: Record<string, string> = {
form: "돌쌓기(찰)",
length: "10",
height: "1.5",
before: "5",
after: "5",
};
/** 형태 · 길이 · 높이 · 전 · 후 — 필수 보호공은 구조 갈래마다 옛 키 · 선택 흙막이는 인스턴스 키. */
function guardItems(keys?: Record<string, Record<string, string>>): SpecItem[] {
const by = (field: string, choices?: Record<string, SpecChoice[]>) =>
keys
? {
item: "structure",
map: Object.fromEntries(
// 키 없는 갈래 = 그 구조에 없는 칸(집수정 길이 · 전 · 후) — 가림
Object.entries(keys)
.filter(([, k]) => k[field])
.map(([structure, k]) => [
structure,
{ key: k[field], choices: choices?.[structure] },
]),
),
}
: undefined;
const items: SpecItem[] = [
{
id: "form",
label: "형태",
kind: "select",
choices: SOIL_FORMS,
by: by("form", { [BASIN]: BASIN_FORMS, [SOIL]: SOIL_FORMS }),
},
{ id: "length", label: "길이 (m)", kind: "number", split: "length", step: 1, by: by("length") },
{ id: "height", label: "높이 (m)", kind: "number", step: 0.1, by: by("height") },
{
id: "before",
label: "기준측점 전 (m)",
kind: "number",
split: "before",
step: 0.5,
by: by("before"),
},
{
id: "after",
label: "기준측점 후 (m)",
kind: "number",
split: "after",
step: 0.5,
by: by("after"),
},
];
return keys ? items : items.map((item) => ({ ...item, default: SOIL_ADD_DEFAULTS[item.id] }));
}
const revetKeys = (side: "inlet" | "outlet"): Record<string, string> => ({
form: `${side}_revet_form`,
length: `${side}_revet_length_m`,
height: `${side}_revet_height_m`,
before: `${side}_revet_before_m`,
after: `${side}_revet_after_m`,
});
/** 보호공 형태 · 길이 · 높이 · 전 · 후 — 구조 갈래마다 옛 키(흙막이 양식 = `design_structure_guard`). */
const protectItems = (keys: Record<string, Record<string, string>>): SpecItem[] =>
guardItems(keys, { [BASIN]: BASIN_FORMS, [SOIL]: SOIL_FORMS });
export const PIPE_SPEC: StructureSpec = {
typeId: "pipe",
@@ -134,7 +66,7 @@ export const PIPE_SPEC: StructureSpec = {
{ value: SOIL, label: "흙막이", also: { inlet_structure: "revet" } },
],
},
...guardItems({
...protectItems({
[BASIN]: { form: "inlet_structure", height: "inlet_basin_height_m" },
[SOIL]: revetKeys("inlet"),
}),
@@ -153,37 +85,11 @@ export const PIPE_SPEC: StructureSpec = {
key: "outlet_type",
choices: [{ value: SOIL, label: "흙막이" }],
},
...guardItems({ [SOIL]: revetKeys("outlet") }),
],
},
{
id: "soil_add",
title: "흙막이",
required: false,
prefix: "soil_add",
link: "gsoiladd",
// 사면안정 흙막이(표 09 · 54-26)와 같은 칸 — 형태 6 · 길이 · 높이 · 전 · 후 · 구조 고르기 없음(54-30)
items: guardItems(),
},
{
id: "chute_add",
title: "도수로/돌붙임",
required: false,
prefix: "chute_add",
link: "gchuteadd",
max: 1, // 하나만(54-48) — 옛 둘 이상은 보이되 [추가]만 숨김
items: [
{
id: "structure",
label: "구조",
kind: "select",
choices: [
{ value: "도수로", label: "도수로" },
{ value: "돌붙임", label: "돌붙임" },
],
},
...protectItems({ [SOIL]: revetKeys("outlet") }),
],
},
SOIL_ADD, // 개수 = 유출 「추가 기슭막이(단)」(54-1)
CHUTE_ADD,
],
// 표 01 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값)
retired: ["pipe_kind", "wing_wall_type", "inlet_basin_form", "inlet_basin_material"],
@@ -59,6 +59,8 @@ export interface SpecContainer {
max?: number;
/** 구조물 조합 잇기 키 — 필수는 정의 그룹 id 그대로 · 선택은 뒤에 n. */
link: string;
/** 첫 항목이 이 값이면 그 컨테이너는 없는 셈 — 목록 램프 · 잇기 줄 뺌(65-3 · 세월교 · 박스 유입 · 유출 「없음」). */
none?: string;
items: SpecItem[];
}
@@ -103,6 +105,10 @@ export function choicesOf(item: SpecItem, values: Instance): SpecChoice[] {
return branchOf(item, values)?.choices ?? item.choices ?? [];
}
/** 컨테이너가 「없음」으로 골라졌는지(`none`). */
export const isNone = (c: SpecContainer, values: Instance): boolean =>
!!c.none && values[c.items[0]?.id] === c.none;
/** 인스턴스 잇기 키 — 필수 = 그룹 id · 선택 = `{link}{n}`. */
export const linkOf = (c: SpecContainer, n: number): string =>
c.required ? c.link : `${c.link}${n}`;
+60 -22
View File
@@ -46,9 +46,14 @@ import {
WING_IN_KEYS,
WING_OUT_KEYS,
} from "./B05_Profile_UI_Drainage_Facility_Fields";
import { revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
import { guardChoiceFields, revetChoiceRow } from "./B05_Profile_UI_Drainage_Revet_Choice";
import { optionDefaults } from "./B05_Profile_UI_Structures_Fields";
import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form";
import {
createSpecForm,
type SpecForm,
type SpecLinks,
} from "../A00_Common/design/design_structure_form";
import type { SpecContainer, SpecItem } from "../A00_Common/design/design_structure_spec";
import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe";
import {
BOX_SPEC,
@@ -219,6 +224,8 @@ export interface FacilityOptionsForm {
/** 옵션 정의 기본값 — 필수 상자 빈 고르기 칸이 받는 값(54-43). */
options?: Array<{ key: string; default: string | number | null }>;
} | null,
/** 새 시설(등록부 기본값으로 염) — 세월교 · 물넘이 빈 월류 높이를 유량이 오면 필요 수심으로(65-4). */
fresh?: boolean,
) => void;
/** 배관 명세 컨테이너 「구조물 조합」 잇기 자리 — 폼이 패널 그룹 값에 이음(PLAN 52-1). */
bindLinks: (links: SpecLinks) => void;
@@ -372,20 +379,40 @@ export function createFacilityOptionsForm(
: "auto";
};
// ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4) — 세월교 · 유입 구조 · 유출 구조 ──
// 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`)
// ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4 · 65장) — 세월교 · 유입 구조 · 유출 구조 · 흙막이 · 도수로/돌붙임 ──
// 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`) —
// 물넘이 포장도 같은 키 · 같은 계산(65-5) · 새 시설은 유량이 오면 빈 높이를 필요 수심으로(65-4)
let fordFlow: number | null = null;
const fordNeed = (): number | null =>
fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? ""));
const fordForm = createSpecForm(specLinks, {
onChange: (c, item) => {
const need = c.id === "ford" ? fordNeed() : null;
const height = fordForm.state().ford?.[0]?.height ?? "";
const low = height !== "" && !(Number(height) >= (need ?? 0));
if (need !== null && (item.id === "width" || (item.id === "height" && low)))
fordForm.setValue("ford", "height", need.toFixed(2));
emit();
},
let heightPending = false;
const overflow = (form: () => SpecForm, box: string) => {
const values = () => form().state()[box]?.[0] ?? {};
const need = (): number | null => fordHeightM(fordFlow, Number(values().width ?? ""));
return {
onChange: (c: SpecContainer, item: SpecItem) => {
if (c.id === box && item.id === "height") heightPending = false;
const want = c.id === box ? need() : null;
const height = values().height ?? "";
const low = height !== "" && !(Number(height) >= (want ?? 0));
if (want !== null && (item.id === "width" || (item.id === "height" && low)))
form().setValue(box, "height", want.toFixed(2));
emit();
},
/** 새 시설 빈 높이 = 필요 수심(사용자가 넣은 것처럼 · 알림 없음 — [추가] · [수정]이 읽음). */
fill: () => {
const want = heightPending && !values().height ? need() : null;
if (want === null) return;
form().setValue(box, "height", want.toFixed(2));
heightPending = false;
},
};
};
const fordFlowBox = overflow(() => fordForm, "ford");
const fordForm: SpecForm = createSpecForm(specLinks, {
onChange: fordFlowBox.onChange,
fields: guardChoiceFields(
() => chainage,
() => fordForm.refresh(),
),
});
// ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자
@@ -440,8 +467,14 @@ export function createFacilityOptionsForm(
return merged;
};
// ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2) — 본체 · 유입 구조 · 유출 구조 ──
const boxForm = createSpecForm(specLinks, { onChange: () => emit() });
// ── BOX암거 = 사용자 구조물 표 02 명세(PLAN 52-2 · 65장) — 본체 · 유입 구조 · 유출 구조 · 흙막이 · 도수로/돌붙임 ──
const boxForm: SpecForm = createSpecForm(specLinks, {
onChange: () => emit(),
fields: guardChoiceFields(
() => chainage,
() => boxForm.refresh(),
),
});
// 날개벽 칸 — 세월교 · BOX 가 같은 조각을 쓰던 자리 · 조정 행은 명세 유입 · 유출 칸으로 옮겨 붙음
const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS);
const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS);
@@ -450,9 +483,10 @@ export function createFacilityOptionsForm(
const hiddenOptions = createHiddenOptions(root);
// ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도 · 도수로/돌붙임 ·
// 옛 월류 폭 · 높이 · 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
const paveForm = createSpecForm(specLinks, { onChange: () => emit() });
// ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도(+ 월류 폭 · 높이 · 65-5) · 도수로/돌붙임 ·
// 옛 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ──
const paveFlowBox = overflow(() => paveForm, "slope");
const paveForm: SpecForm = createSpecForm(specLinks, { onChange: paveFlowBox.onChange });
// 10-A ⑲ 저장 규칙 글은 [?] 설명서 `구조물_<시설>.md` 로 옮김(54-2) — 병합은 `_Drainage_Facility_Merge`.
@@ -567,8 +601,9 @@ export function createFacilityOptionsForm(
setHiddenOptions(facility, registryKeys) {
hiddenOptions.set(facility, registryKeys);
},
setFacility(facility, options = {}, designFlow = null, definition = null) {
setFacility(facility, options = {}, designFlow = null, definition = null, fresh = false) {
current = facility;
heightPending = fresh;
hiddenOptions.remember(options);
fordFlow = designFlow ?? null;
if (facility === null) {
@@ -609,6 +644,8 @@ export function createFacilityOptionsForm(
);
if (facility === "ford_pavement")
paveForm.write(FORD_PAVEMENT_SPEC, options, names, groups, defaults);
if (facility === "ford_bridge") fordFlowBox.fill();
if (facility === "ford_pavement") paveFlowBox.fill();
wingInFields.write(options);
wingOutFields.write(options);
revetSide.value = text("side") || "양쪽";
@@ -622,7 +659,8 @@ export function createFacilityOptionsForm(
},
setDesignFlow(designFlow) {
fordFlow = designFlow ?? null;
if (current === "ford_bridge") fordForm.refresh(); // 높이 안내
if (current === "ford_bridge") fordFlowBox.fill();
if (current === "ford_pavement") paveFlowBox.fill();
syncVisibility();
},
readOptions() {
@@ -33,10 +33,19 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
...revetKeys("outlet"),
],
// BOX = 표 02 명세 키(PLAN 52-2) — 옛 날개벽 치수는 폼에서 빠져 이어 붙임(지난 정의 값)
box_culvert: ["body_width_m", "body_height_m", "body_custom", "wing_in", "wing_out"],
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) — 옛 월류 · 경사 · 두께 · 길이는
// 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
ford_pavement: ["ford_slope_deg"],
// 흙막이 = 배관과 같은 키(65-2) · 추가 흙막이 · 도수로/돌붙임은 `FORM_OPTION_PREFIXES`
box_culvert: [
"body_width_m",
"body_height_m",
"body_custom",
"wing_in",
"wing_out",
...revetKeys("inlet"),
...revetKeys("outlet"),
],
// 물넘이 포장 = 표 03 명세 키(PLAN 52-3 · `design_structure_ford_pavement`) + 월류 폭 · 높이(65-5) — 옛 경사 ·
// 두께 · 길이는 폼에서 빠져 이어 붙임 · 도수로/돌붙임 키는 `FORM_OPTION_PREFIXES`
ford_pavement: ["ford_slope_deg", "ford_width_m", "ford_height_m"],
// 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임
ford_bridge: [
"pipe_diameter_mm",
@@ -47,6 +56,8 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
"outlet_type",
"wing_in",
"wing_out",
...revetKeys("inlet"),
...revetKeys("outlet"),
],
revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")],
};
@@ -54,6 +65,8 @@ export const FORM_OPTION_KEYS: Readonly<Record<string, readonly string[]>> = {
/** 폼이 인스턴스 번호를 붙여 적는 키 머리(명세 선택 컨테이너 · `{머리}{n}_{항목}`) — 빼면 지워짐. */
export const FORM_OPTION_PREFIXES: Readonly<Record<string, readonly string[]>> = {
pipe: ["soil_add_", "chute_add_"],
box_culvert: ["soil_add_", "chute_add_"],
ford_bridge: ["soil_add_", "chute_add_"],
ford_pavement: ["ford_chute_"],
};
@@ -53,3 +53,21 @@ export function revetChoiceRow(
}
return { paint };
}
/** 세월교 · 박스 흙막이 상자 줄(65-2 · 배관과 같은 줄) — 흙막이를 고른 유입 · 유출 · 추가 흙막이 상자마다 한 줄.
* 명세 폼 `fields` 자리에 그대로 씀. */
export function guardChoiceFields(
chainageOf: () => number | null,
written: () => void,
): (c: { id: string }, values: Record<string, string>) => HTMLElement[] {
const sides = {
inlet: revetChoiceRow(chainageOf, written),
outlet: revetChoiceRow(chainageOf, written),
};
return (c, values) =>
c.id === "soil_add"
? [revetChoiceRow(chainageOf, written).paint(false)]
: c.id === "inlet" || c.id === "outlet"
? [sides[c.id].paint(values.structure !== "흙막이")]
: [];
}
+19 -4
View File
@@ -24,7 +24,13 @@ import {
} from "../common_util/common_util_summary_item";
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
import { pipeExtraCount } from "../A00_Common/design/design_structure_pipe_calc";
import { containerTitle, linkOf, readState } from "../A00_Common/design/design_structure_spec";
import {
containerTitle,
isNone,
linkOf,
readState,
} from "../A00_Common/design/design_structure_spec";
import { withLegacyGuards } from "../A00_Common/design/design_structure_ford";
import { asRoad } from "./B05_Profile_UI_Structures_Define";
export interface StructureListParams {
@@ -100,11 +106,20 @@ export function linkLamps(
// 배관 흙막이 = 명세 이름 「흙막이」가 정의 옛 이름 「흙막이 추가」보다 앞섬(54-1 · 시설 폼과 같음)
const { soil_add: _old, ...pipeNames } = type?.spec_names ?? {};
const names = source.type_id === "pipe" ? pipeNames : (type?.spec_names ?? {});
const state = readState(spec, source.options ?? {});
// 세월교 옛 날개벽 설치 칸 → 구조(시설 폼과 같은 길 · 「없음」이면 램프 없음 · 65-3)
const options = source.options ?? {};
const state = readState(
spec,
source.type_id === "ford_bridge" ? withLegacyGuards(options) : options,
);
return spec.containers.flatMap((c): LinkLamp[] => {
const title = containerTitle(c, names, groupNames[c.link]);
if (c.required) return [{ title, item: links[c.link] ?? null, required: true }];
const n = (c.id === "soil_add" ? pipeExtraCount(chainageM) : null) ?? state[c.id].length;
if (c.required)
return isNone(c, state[c.id][0])
? []
: [{ title, item: links[c.link] ?? null, required: true }];
const pipeSoil = source.type_id === "pipe" && c.id === "soil_add";
const n = (pipeSoil ? pipeExtraCount(chainageM) : null) ?? state[c.id].length;
return Array.from({ length: n }, (_, i) => ({
title: `${title} ${i + 1}`,
item: links[linkOf(c, i + 1)] ?? null,
@@ -250,7 +250,9 @@ export function createStructuresSection(
): void {
const kind = facilityFormKind(currentType());
facilityOptions.setChainage(selectedPipeChainage ?? tempPipeChainage); // 관 길이(자동 · 52-1)
facilityOptions.setFacility(kind, options ?? newDefaults(), designFlow, currentType());
// 값 없이 열면 새 시설 — 빈 월류 높이를 유량이 오면 필요 수심으로(65-4)
const fresh = options === undefined;
facilityOptions.setFacility(kind, options ?? newDefaults(), designFlow, currentType(), fresh);
if (kind)
paintStale(staleBox, currentType(), options, kind, groupBoxes.shapeKeys(currentType()));
// 「구조물 옵션 보이기」에서 끈 옵션은 시설 폼에서도 가림(값은 옛 값 그대로 저장)
@@ -106,7 +106,7 @@ export function bindStructuresEvents(ctx: StructuresEventContext): void {
ctx.setSummary(readSummaryItem(back?.summary_item));
ctx.groupBoxes.set(readGroupItems(back?.group_items));
ctx.groupBoxes.setShapes(back?.options ?? {});
ctx.syncFacilityForm(back?.options);
ctx.syncFacilityForm(back?.options, pipeItem?.design_flow_m3s ?? null); // 유량 = 월류 높이(65-4)
ctx.liveCommit();
// 반영이 막혀(측점 등) 기록이 그대로면 옛 기록으로 되돌림
if (record && draft && ctx.structures[index] === draft) ctx.structures[index] = record;
+6 -3
View File
@@ -65,8 +65,10 @@ const PIPE_INLET_ROWS = [
].join(", ");
const PIPE_OUTLET_ROWS = ".b06-structure-panel__struct--basin-size";
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로. */
function rowsFor(slot: HTMLElement): string {
/** 받을 칸에 맞는 줄 — 배관 유입 · 유출 칸이 아니면 옛 줄 그대로 · 세월교 · 박스 유입 · 유출 칸은 안 받음
* (높이 · 측벽 좌우 · 상하 · 바닥판 읽기는 칸에 안 둠 · 강조만 · 65-1). */
function rowsFor(slot: HTMLElement): string | null {
if (slot === sideSlots?.wingInlet || slot === sideSlots?.wingOutlet) return null;
if (slot === sideSlots?.outlet) return PIPE_OUTLET_ROWS;
return slot === sideSlots?.inlet ? PIPE_INLET_ROWS : SIDE_ROWS;
}
@@ -133,7 +135,8 @@ function clearSideSlots(): void {
}
/** 행 DOM을 제자리 표식과 함께 옮긴다 — 숨은 행은 그대로 둔다. */
function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement): void {
function moveRows(panelRoot: HTMLElement, selector: string | null, target: HTMLElement): void {
if (!selector) return;
panelRoot.querySelectorAll<HTMLElement>(selector).forEach((row) => {
if (row.classList.contains("is-hidden")) return;
// 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다.
+14 -6
View File
@@ -4,7 +4,7 @@
집계 항목 = 잇기 `group_items[{잇기}{n}]` · 복사본 `{구조물 id}__{잇기}{n}` · 옵션 = 그 컨테이너 값.
열 바인딩이 읽는 칸이 갈래마다 달라(독립 기슭막이 `form` · `length_m` … · 관 `{유입|유출}_revet_*`)
컨테이너 값을 그 칸들에 함께 얹음 — 어느 출처로 읽어도 그 컨테이너 값.
짝: `A00_Common/design/design_structure_pipe.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) —
짝: `A00_Common/design/design_structure_guard.ts` · `_road.ts` 선택 컨테이너(머리 · 잇기 · 제목) —
시험이 대조.
기타(52-14) = 사용자가 적은 상세 종류 이름이 집계 건 이름(`그룹`) · 비면 「기타」.
배관 흙막이 = 지형이 세운 유출 다단 수(`흙막이단`)까지만 — 넘친 컨테이너 값은 파일에만(54-11).
@@ -18,11 +18,14 @@ from typing import Any
from common_util.common_util_summary_item import GROUP_JOIN
#: 종류 → 선택 컨테이너 (머리, 잇기, 제목)
_GUARD = (
("soil_add", "gsoiladd", "흙막이"),
("chute_add", "gchuteadd", "도수로/돌붙임"),
)
SPEC_CONTAINERS: dict[str, tuple[tuple[str, str, str], ...]] = {
"pipe": (
("soil_add", "gsoiladd", "흙막이"),
("chute_add", "gchuteadd", "도수로/돌붙임"),
),
"pipe": _GUARD,
"box_culvert": _GUARD, # 흙막이 양식 공통(65-2)
"ford_bridge": _GUARD,
"ford_pavement": (("ford_chute", "gfordpavechute", "도수로/돌붙임"),),
"etc": (("etc_detail", "getcdetail", "상세 내용"),),
}
@@ -75,7 +78,12 @@ def container_rows(item: dict[str, Any], links: dict[str, Any]) -> list[dict[str
if hit:
found.setdefault(int(hit.group(1)), {})[hit.group(2)] = value
for n in sorted(found):
if prefix == "soil_add" and tiers is not None and n > tiers:
if (
prefix == "soil_add"
and item["type_id"] == "pipe"
and tiers is not None
and n > tiers
):
continue
gid = f"{link}{n}"
rows.append(
+92 -6
View File
@@ -1,5 +1,5 @@
{
"판": 17,
"판": 18,
"schema_version": 1,
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.",
"구조물군": {
@@ -486,6 +486,15 @@
"quantity"
],
"options": [
{
"key": "ford_slope_deg",
"label": "경사각도",
"input": "number",
"unit": "°",
"default": 3,
"required": false,
"default_basis": "물넘이 포장 경사각 기본 3° — 사용자 확정값(PLAN 65-5)"
},
{
"key": "ford_width_m",
"label": "월류 폭",
@@ -575,8 +584,9 @@
"label": "배관 수량",
"input": "number",
"unit": "련",
"default": null,
"required": false
"default": 2,
"required": false,
"default_basis": "세월교 배관 수량 기본 2련 — 사용자 확정값(PLAN 65-4)"
},
{
"key": "ford_width_m",
@@ -3381,10 +3391,48 @@
"inlet.structure=없음": "없음",
"inlet.structure=날개벽": "날개벽",
"inlet.structure=흙막이": "흙막이",
"inlet.form": "형태",
"inlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
"inlet.form=돌쌓기(메)": "돌 쌓기(메)",
"inlet.form=콘크리트": "콘크리트",
"inlet.form=돌망태": "돌망태",
"inlet.form=통나무·목재틀": "통나무/목재틀",
"inlet.form=바자": "바자",
"inlet.length": "길이 (m)",
"inlet.height": "높이 (m)",
"inlet.before": "기준측점 전 (m)",
"inlet.after": "기준측점 후 (m)",
"outlet.structure": "구조",
"outlet.structure=없음": "없음",
"outlet.structure=날개벽": "날개벽",
"outlet.structure=흙막이": "흙막이"
"outlet.structure=흙막이": "흙막이",
"outlet.form": "형태",
"outlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
"outlet.form=돌쌓기(메)": "돌 쌓기(메)",
"outlet.form=콘크리트": "콘크리트",
"outlet.form=돌망태": "돌망태",
"outlet.form=통나무·목재틀": "통나무/목재틀",
"outlet.form=바자": "바자",
"outlet.length": "길이 (m)",
"outlet.height": "높이 (m)",
"outlet.before": "기준측점 전 (m)",
"outlet.after": "기준측점 후 (m)",
"soil_add": "흙막이",
"soil_add.form": "형태",
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
"soil_add.form=돌쌓기(메)": "돌 쌓기(메)",
"soil_add.form=콘크리트": "콘크리트",
"soil_add.form=돌망태": "돌망태",
"soil_add.form=통나무·목재틀": "통나무/목재틀",
"soil_add.form=바자": "바자",
"soil_add.length": "길이 (m)",
"soil_add.height": "높이 (m)",
"soil_add.before": "기준측점 전 (m)",
"soil_add.after": "기준측점 후 (m)",
"chute_add": "도수로/돌붙임",
"chute_add.structure": "구조",
"chute_add.structure=도수로": "도수로",
"chute_add.structure=돌붙임": "돌붙임"
}
},
"ford_pavement": {
@@ -3405,17 +3453,55 @@
"ford.diameter=1000": "1000",
"ford.diameter=1200": "1200",
"ford.diameter=1500": "1500",
"ford.count": "수량 (련)",
"ford.width": "월류 폭 (m)",
"ford.height": "월류 높이 (m)",
"ford.count": "수량 (련)",
"inlet.structure": "구조",
"inlet.structure=없음": "없음",
"inlet.structure=날개벽": "날개벽",
"inlet.structure=흙막이": "흙막이",
"inlet.form": "형태",
"inlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
"inlet.form=돌쌓기(메)": "돌 쌓기(메)",
"inlet.form=콘크리트": "콘크리트",
"inlet.form=돌망태": "돌망태",
"inlet.form=통나무·목재틀": "통나무/목재틀",
"inlet.form=바자": "바자",
"inlet.length": "길이 (m)",
"inlet.height": "높이 (m)",
"inlet.before": "기준측점 전 (m)",
"inlet.after": "기준측점 후 (m)",
"outlet.structure": "구조",
"outlet.structure=없음": "없음",
"outlet.structure=날개벽": "날개벽",
"outlet.structure=흙막이": "흙막이"
"outlet.structure=흙막이": "흙막이",
"outlet.form": "형태",
"outlet.form=돌쌓기(찰)": "돌 쌓기(찰)",
"outlet.form=돌쌓기(메)": "돌 쌓기(메)",
"outlet.form=콘크리트": "콘크리트",
"outlet.form=돌망태": "돌망태",
"outlet.form=통나무·목재틀": "통나무/목재틀",
"outlet.form=바자": "바자",
"outlet.length": "길이 (m)",
"outlet.height": "높이 (m)",
"outlet.before": "기준측점 전 (m)",
"outlet.after": "기준측점 후 (m)",
"soil_add": "흙막이",
"soil_add.form": "형태",
"soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)",
"soil_add.form=돌쌓기(메)": "돌 쌓기(메)",
"soil_add.form=콘크리트": "콘크리트",
"soil_add.form=돌망태": "돌망태",
"soil_add.form=통나무·목재틀": "통나무/목재틀",
"soil_add.form=바자": "바자",
"soil_add.length": "길이 (m)",
"soil_add.height": "높이 (m)",
"soil_add.before": "기준측점 전 (m)",
"soil_add.after": "기준측점 후 (m)",
"chute_add": "도수로/돌붙임",
"chute_add.structure": "구조",
"chute_add.structure=도수로": "도수로",
"chute_add.structure=돌붙임": "돌붙임"
}
},
"revetment": {
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
/* 박스암거 명세 단위검증 헬퍼(PLAN 52-2) — `design_structure_spec.ts` · `design_structure_box.ts` 를
* 그 자리에서 트랜스파일해 Node 로 돌림(꼴만 import). 결과 = JSON 한 벌.
* 그 자리에서 트랜스파일해 Node 로 돌림(같은 폴더 명세 import 는 따라 옮김). 결과 = JSON 한 벌.
* 인자 `names` 면 박스암거 이름 표(정의 `종류.box_culvert.이름`)만 냄.
*/
const fs = require("fs");
@@ -13,7 +13,9 @@ const load = (file) => {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, require);
// 같은 폴더 명세(흙막이 양식 · 65-2)는 그 자리에서 옮김
const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m));
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
};
const s = load("design_structure_spec.ts");
@@ -130,6 +130,7 @@ const STUBS = {
// 관 보호공 [연동] · [경사] 줄(54-30) — 여기서는 자리만
"./B05_Profile_UI_Drainage_Revet_Choice": {
revetChoiceRow: () => ({ paint: () => new FakeNode("div") }),
guardChoiceFields: () => () => [],
},
"../../B05_Profile/B05_Profile_UI_Structures_Summary_Row": {
buildSummaryRow: () => {
+5 -1
View File
@@ -75,13 +75,17 @@ def test_집계표_채움에_컨테이너_줄(world):
def test_서버_컨테이너_표는_배관_명세와_짝():
with open(
os.path.join(ROOT, "A00_Common/design/design_structure_pipe.ts"), encoding="utf-8"
os.path.join(ROOT, "A00_Common/design/design_structure_guard.ts"), encoding="utf-8"
) as f:
text = f.read()
found = re.findall(
r'title: "([^"]+)",\s*required: false,\s*prefix: "(\w+)",\s*link: "(\w+)"', text
)
assert [(p, link, t) for t, p, link in found] == list(SPEC_CONTAINERS["pipe"])
# 박스 · 세월교 = 같은 흙막이 양식(65-2)
assert (
SPEC_CONTAINERS["box_culvert"] == SPEC_CONTAINERS["ford_bridge"] == SPEC_CONTAINERS["pipe"]
)
def test_54_11_흙막이는_선_단_수까지만():
@@ -70,22 +70,23 @@ def test_컨테이너_경사각도_필수_도수로돌붙임_선택(out):
def test_저장_값은_도_와_인스턴스_키(out):
assert out["state"] == {"slope": [{"angle": "12.5"}], "chute": [{"structure": "돌붙임"}]}
slope = {
"angle": "12.5",
"width": "6",
"height": "0.3",
} # 월류 폭 · 높이 = 옛 키 그대로 읽음(65-5)
assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임"}]}
assert out["written"] == {
"ford_slope_deg": 15,
"ford_width_m": 6,
"ford_height_m": 0.3,
"ford_chute_1_structure": "돌붙임",
"ford_chute_2_structure": "도수로",
}
def test_옛_칸은_지난_정의_값으로_남음(out):
assert out["retired"] == [
"ford_width_m",
"ford_height_m",
"ford_slope_pct",
"thickness_cm",
"length_m",
]
assert out["retired"] == ["ford_slope_pct", "thickness_cm", "length_m"]
# 폼이 모르는 옛 칸은 그대로 · 폼이 아는 머리(빠진 인스턴스)는 폼 값으로 갈아 끼움
assert out["saved"] == {
"ford_width_m": 6,
@@ -95,7 +96,12 @@ def test_옛_칸은_지난_정의_값으로_남음(out):
"length_m": 7,
**out["written"],
}
assert out["formKeys"] == out["keys"]["keys"] == ["ford_slope_deg"]
# 월류 폭 · 높이 = 세월교와 같은 키(65-5)
assert (
out["formKeys"]
== out["keys"]["keys"]
== ["ford_slope_deg", "ford_width_m", "ford_height_m"]
)
assert out["heads"] == out["keys"]["prefixes"] == ["ford_chute_"]
@@ -39,17 +39,20 @@ def test_컨테이너_셋_모두_필수(out):
["ford", "세월교", True],
["inlet", "유입 구조", True],
["outlet", "유출 구조", True],
["soil_add", "흙막이", False],
["chute_add", "도수로/돌붙임", False],
]
# 수량(련) = 관경 바로 뒤(65-4)
assert out["items"][0] == [
["diameter", "select", "pipe_diameter_mm"],
["count", "number", "pipe_count"],
["width", "number", "ford_width_m"],
["height", "number", "ford_height_m"],
["count", "number", "pipe_count"],
]
assert out["choices"] == [
[["800", "1000", "1200", "1500"]],
[["없음", "날개벽", "흙막이"]],
[["없음", "날개벽", "흙막이"]],
assert [c[0] for c in out["choices"][:3]] == [
["800", "1000", "1200", "1500"],
["없음", "날개벽", "흙막이"],
["없음", "날개벽", "흙막이"],
]
@@ -73,7 +76,7 @@ def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out):
def test_잇기_키_컨테이너마다(out):
assert out["links"] == ["gford", "gfordin", "gfordout"]
assert out["links"] == ["gford", "gfordin", "gfordout", "gsoiladd1", "gchuteadd1"]
def _manning_depth(q, b):
@@ -36,8 +36,10 @@ def test_컨테이너_셋_모두_필수(out):
["body", "본체", True],
["inlet", "유입 구조", True],
["outlet", "유출 구조", True],
["soil_add", "흙막이", False],
["chute_add", "도수로/돌붙임", False],
]
assert out["links"] == ["gboxbody", "gboxin", "gboxout"]
assert out["links"] == ["gboxbody", "gboxin", "gboxout", "gsoiladd1", "gchuteadd1"]
def test_선택지_표_02_그대로(out):
@@ -56,9 +56,12 @@ def test_상자_칸을_고치면_그_부분을_고름() -> None:
def test_흙막이_상자는_사면안정_흙막이와_같은_칸() -> None:
pipe = _src("A00_Common/design/design_structure_pipe.ts")
soil = pipe[pipe.index('id: "soil_add"') :]
assert soil[: soil.index("},")].count("items: guardItems(),") == 1
# 흙막이 양식 한 벌(65-2) — 배관 · 박스 · 세월교가 같은 상자를 씀
guard = _src("A00_Common/design/design_structure_guard.ts")
soil = guard[guard.index('id: "soil_add"') :]
assert soil[: soil.index("};")].count("items: guardItems(),") == 1
for spec in ("pipe", "box", "ford"):
assert "SOIL_ADD" in _src(f"A00_Common/design/design_structure_{spec}.ts")
def test_관_보호공_연동_경사_단추() -> None: