From 0a095c14b1d0034f20cd720abfe2bc54f1db89f8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:13:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(design):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=AA=85=EC=84=B8=20=EA=B3=B5=EC=9A=A9=20=ED=8B=80=20+=20?= =?UTF-8?q?=ED=9A=A1=EB=8B=A8=EB=B0=B0=EC=88=98=20=EB=B0=B0=EA=B4=80(?= =?UTF-8?q?=ED=91=9C=2001)=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=ED=8F=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 공용 틀(A00_Common/design/design_structure_*) — 명세(컨테이너 필수/선택 · 여러 개 · 항목 고르기/숫자/자동 · 상위 값에 따른 선택지 · 저장 키) · 그리기([추가]/[빼기]) · 길이 = 전 + 후 맞물림(마지막에 고친 둘을 지킴) - 배관 명세 = 관 · 관 보호공(유입) · 관 보호공(유출) · 흙막이 추가 · 도수로/돌붙임 · 저장 키는 pipe_points 옛 키 그대로 · 옛 관종 · 날개벽 · 집수정 형식은 화면에서 빼고 지난 정의 값으로 남김 - 컨테이너 인스턴스마다 「구조물 조합」 잇기(group_items · 그룹 [고르기] 길 그대로) · 명세 종류는 그룹 상자 · 종류 단위 줄 가림 · 종류 단위 잇기 줄은 기준 측점 바로 아래 - 관 길이 자동 = 횡단 기하 computeStoredLayouts 값(서버 Node 와 같은 함수) · 높이 빈 칸 안내 = 관경 기준 최소 높이 - 정의 판 6 — 종류.pipe.이름 표(이름 바꿈 키) · 검사기가 이름 표 받음 · 판 5 이력 보관 - 시험 test_52_structure_spec_pipe · 병합 시험 기대값(빠진 옛 칸은 남음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- A00_Common/design/design_structure_form.ts | 252 ++ A00_Common/design/design_structure_pipe.ts | 187 + .../design/design_structure_pipe_calc.ts | 42 + A00_Common/design/design_structure_spec.ts | 239 ++ A00_Common/design/design_view_cross.ts | 3 + B05_Profile/B05_Profile_Api_Structures.ts | 2 + .../B05_Profile_UI_Drainage_Facility.ts | 230 +- .../B05_Profile_UI_Drainage_Facility_Merge.ts | 17 +- .../B05_Profile_UI_Structures_Define.ts | 3 +- B05_Profile/B05_Profile_UI_Structures_Form.ts | 10 +- .../B05_Profile_UI_Structures_Groups.ts | 21 +- .../B05_Profile_UI_Structures_Panel.ts | 14 +- ...B05_Profile_UI_Structures_Panel_Options.ts | 16 +- .../StructOptions/M02_SO_Definition.py | 13 +- resources/master_template/구조물정의.json | 63 +- .../master_template/구조물정의_이력/판_5.json | 3303 +++++++++++++++++ resources/tester/helper_52_structure_spec.cjs | 87 + .../tester/test_52_structure_spec_pipe.py | 109 + .../tester/test_b05_facility_options_merge.py | 3 +- 19 files changed, 4439 insertions(+), 175 deletions(-) create mode 100644 A00_Common/design/design_structure_form.ts create mode 100644 A00_Common/design/design_structure_pipe.ts create mode 100644 A00_Common/design/design_structure_pipe_calc.ts create mode 100644 A00_Common/design/design_structure_spec.ts create mode 100644 resources/master_template/구조물정의_이력/판_5.json create mode 100644 resources/tester/helper_52_structure_spec.cjs create mode 100644 resources/tester/test_52_structure_spec_pipe.py diff --git a/A00_Common/design/design_structure_form.ts b/A00_Common/design/design_structure_form.ts new file mode 100644 index 000000000..0d570821d --- /dev/null +++ b/A00_Common/design/design_structure_form.ts @@ -0,0 +1,252 @@ +/* ============================================================================= + * design_structure_form.ts + * 구조물 명세(`design_structure_spec`) 그리기 — B05 · B06 「구조물 배치」 칸이 같은 노드를 씀. + * + * · 필수 컨테이너 = 늘 보임(한 벌) · 선택 컨테이너 = [추가] 로 인스턴스를 늘리고 인스턴스마다 [빼기] + * · 항목: 고르기(상위 값에 따른 선택지 · 저장 키) · 숫자(맞물림 = 길이 · 전 · 후) · 자동(읽기만) + * · 인스턴스마다 「구조물 조합」 잇기 줄(`buildSummaryRow` · 고르기 모달 = 패널의 그룹 [고르기] 길 그대로) — + * 값은 패널 `group_items` 에 `linkOf` 키로 둠(빼면 뒤 인스턴스 잇기를 한 칸씩 당김) + * · 값은 폼이 쥠 — 저장은 패널이 `read()` 로 읽어 정본 자리(옵션)에 씀 + * · 칸 표식 = `data-spec-item="{컨테이너}.{항목}"`(정의 규칙 · 그룹 상자가 안 건드리게 옵션 키 표식 안 씀) + * ========================================================================== */ + +import { + branchOf, + choiceLabel, + choicesOf, + containerTitle, + itemLabel, + linkOf, + readState, + solveSplit, + writeOptions, + type SpecContainer, + type SpecItem, + type SpecNames, + type SpecState, + type SplitRole, + type StructureSpec, +} from "./design_structure_spec"; +import { + group, + grid, + labeled, + numberInput, + stepper, +} from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields"; +import { buildSummaryRow } from "../../B05_Profile/B05_Profile_UI_Structures_Summary_Row"; +import type { GroupItems, SummaryItem } from "../../common_util/common_util_summary_item"; + +/** 잇기 값 자리 — 패널 그룹 값(`group_items`)을 그대로 씀. */ +export interface SpecLinks { + get: () => GroupItems; + pick: (key: string) => void; + /** [해제] — 패널이 풀고 반영. */ + clear: (key: string) => void; + /** 값만 옮김(빼기 뒤 당김) — 반영은 뒤이은 폼 변경 알림이 함. */ + set: (key: string, item: SummaryItem | null) => void; +} + +export interface SpecFormHooks { + /** 자동 항목 값 글(빈 글 = 「—」). */ + auto?: (c: SpecContainer, item: SpecItem, state: SpecState) => string; + /** 숫자 칸 빈 칸 안내. */ + hint?: (c: SpecContainer, item: SpecItem, state: SpecState) => string; + onChange?: (c: SpecContainer, item: SpecItem) => void; +} + +export interface SpecForm { + root: HTMLElement; + /** 명세 · 저장 값으로 다시 그림. `names` = 정의 이름 바꿈 · `groupNames` = 정의 그룹 이름. */ + write: ( + spec: StructureSpec, + options: Record, + names?: SpecNames, + groupNames?: Record, + ) => void; + read: () => Record; + state: () => SpecState; + /** 한 항목 값을 바꿔 그림(B06 조정창 맞춤 등) — change 알림은 안 함. */ + setValue: (containerId: string, itemId: string, value: string) => void; + /** 필수 컨테이너 끝에 붙일 자리(B06 조정 행). */ + slot: (containerId: string) => HTMLElement; + /** 잇기 값이 바뀌었을 때 줄을 다시 그림. */ + paintLinks: () => void; + refresh: () => void; +} + +export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {}): SpecForm { + const root = document.createElement("div"); + root.className = "b05-structure__spec"; + let spec: StructureSpec | null = null; + let state: SpecState = {}; + let names: SpecNames = {}; + let groupNames: Record = {}; + const recent = new Map(); + const slots = new Map(); + const rows: Array<{ key: string; set: (item: SummaryItem | null) => void }> = []; + type Instance = Record; + + const slotOf = (id: string): HTMLElement => { + let slot = slots.get(id); + if (!slot) { + slot = document.createElement("div"); + slot.className = "b05-structure__adjust-slot"; + slots.set(id, slot); + } + return slot; + }; + + const control = (c: SpecContainer, item: SpecItem, values: Instance): HTMLElement => { + if (item.kind === "select") { + const select = document.createElement("select"); + const choices = choicesOf(item, values); + select.replaceChildren( + new Option("— 안 정함 —", ""), + ...choices.map((ch) => new Option(choiceLabel(c, item, ch, names), ch.value)), + ); + const now = values[item.id] ?? ""; + // 선택지 밖 옛 값은 그 값만 남겨 보임(지운 값을 몰래 바꾸지 않음) + if (now && !choices.some((ch) => ch.value === now)) select.add(new Option(now, now)); + select.value = now; + return select; + } + const input = numberInput(String(item.step ?? 0.1), "0", hooks.hint?.(c, item, state) ?? ""); + if (item.kind === "auto") { + input.type = "text"; + input.readOnly = true; + input.value = hooks.auto?.(c, item, state) || "—"; + input.title = "자동 계산 — 고치지 않음"; + return input; + } + input.value = values[item.id] ?? ""; + return stepper(input, item.step ?? 0.1); + }; + + const onEdit = (c: SpecContainer, item: SpecItem, values: Instance, raw: string): void => { + values[item.id] = raw; + if (item.split) { + const order = [item.split, ...(recent.get(values) ?? []).filter((r) => r !== item.split)]; + recent.set(values, order); + const role = (r: SplitRole) => c.items.find((i) => i.split === r)!; + const solved = solveSplit( + { + length: values[role("length").id], + before: values[role("before").id], + after: values[role("after").id], + }, + order, + ); + for (const r of ["length", "before", "after"] as SplitRole[]) + values[role(r).id] = solved[r] ?? ""; + } + // 위 항목이 바뀌면 갈래(선택지 · 저장 키)가 달라짐 — 선택지 밖 값은 비움 + 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] = ""; + } + paint(); + hooks.onChange?.(c, item); + }; + + const instanceBox = (c: SpecContainer, values: Instance, index: number): HTMLElement => { + const title = containerTitle(c, names, groupNames[c.link]); + const box = group(c.required ? title : `${title} ${index + 1}`); + box.root.dataset.specContainer = c.id; + const fields = c.items + .filter((item) => item.kind === "auto" || !item.by || branchOf(item, values)) + .map((item) => { + const input = control(c, item, values); + const wrap = labeled(itemLabel(c, item, names), input); + wrap.dataset.specItem = `${c.id}.${item.id}`; + const target = input.querySelector?.("input, select") ?? input; + target.addEventListener("change", () => + onEdit(c, item, values, (target as HTMLInputElement | HTMLSelectElement).value), + ); + return wrap; + }); + box.body.append(grid(...fields)); + if (c.required) box.body.append(slotOf(c.id)); + if (links) { + const key = linkOf(c, index + 1); + const row = buildSummaryRow(); + row.root.querySelector(".b05-structure__summary-label")!.textContent = "구조물 조합"; + row.root.dataset.specLink = key; + row.set(links.get()[key] ?? null); + row.bind({ pick: () => links.pick(key), clear: () => links.clear(key) }); + rows.push({ key, set: row.set }); + box.body.append(row.root); + } + if (!c.required) { + const remove = document.createElement("button"); + remove.type = "button"; + remove.className = "b05-route__irregular-btn"; + remove.textContent = "빼기"; + remove.dataset.specRemove = c.id; + remove.addEventListener("click", () => { + const list = state[c.id]; + // 잇기는 인스턴스 차례를 따름 — 뒤 것을 한 칸씩 당기고 마지막 키는 풂 + if (links) { + const now = links.get(); + for (let n = index + 1; n < list.length; n++) + links.set(linkOf(c, n), now[linkOf(c, n + 1)] ?? null); + links.set(linkOf(c, list.length), null); + } + list.splice(index, 1); + paint(); + hooks.onChange?.(c, c.items[0]); + }); + box.root.querySelector("legend")!.append(" ", remove); + } + return box.root; + }; + + function paint(): void { + rows.length = 0; + const boxes: HTMLElement[] = []; + for (const c of spec?.containers ?? []) { + const list = (state[c.id] ??= c.required ? [{}] : []); + list.forEach((values, index) => boxes.push(instanceBox(c, values, index))); + if (c.required) continue; + const add = document.createElement("button"); + add.type = "button"; + add.className = "b05-route__irregular-btn b05-structure__spec-add"; + add.textContent = `+ ${containerTitle(c, names, groupNames[c.link])} 추가`; + add.dataset.specAdd = c.id; + add.addEventListener("click", () => { + list.push({}); + paint(); + hooks.onChange?.(c, c.items[0]); + }); + boxes.push(add); + } + root.replaceChildren(...boxes); + } + + return { + root, + write(next, options, nextNames = {}, nextGroups = {}) { + spec = next; + names = nextNames; + groupNames = nextGroups; + state = readState(next, options); + recent.clear(); + paint(); + }, + read: () => (spec ? writeOptions(spec, state) : {}), + state: () => state, + setValue(containerId, itemId, value) { + const values = state[containerId]?.[0]; + if (!values) return; + values[itemId] = value; + paint(); + }, + slot: slotOf, + paintLinks() { + const now = links?.get() ?? {}; + rows.forEach((row) => row.set(now[row.key] ?? null)); + }, + refresh: paint, + }; +} diff --git a/A00_Common/design/design_structure_pipe.ts b/A00_Common/design/design_structure_pipe.ts new file mode 100644 index 000000000..c72f84c0b --- /dev/null +++ b/A00_Common/design/design_structure_pipe.ts @@ -0,0 +1,187 @@ +/* ============================================================================= + * design_structure_pipe.ts + * 횡단배수 › 배관 명세 — 사용자 구조물 표 01(`docs/raw/구조물정의_표/01_횡단배수_배관.md`) 그대로. + * + * 컨테이너 다섯: 관(필수) · 관 보호공(유입)(필수) · 관 보호공(유출)(필수) · 흙막이 추가(선택 · 여러 개) · + * 도수로/돌붙임(선택 · 여러 개). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈. + * 저장 키 = pipe_points.json 옛 키 그대로(B06 횡단 · B07 · B08 이 읽는 자리) — 흙막이 값은 옛 「기슭막이」 + * (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`). + * 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림. + * 계산(관 길이 · 높이 안내)은 `design_structure_pipe_calc`. + * import 없음 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec"; + +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: "ㄷ형" }, +]; + +/** 형태 · 길이 · 높이 · 전 · 후 — 필수 보호공은 구조 갈래마다 옛 키 · 선택 흙막이는 인스턴스 키. */ +function guardItems(keys?: Record>): SpecItem[] { + const by = (field: string, choices?: Record) => + keys + ? { + item: "structure", + map: Object.fromEntries( + Object.entries(keys).map(([structure, k]) => [ + structure, + { key: k[field], choices: choices?.[structure] }, + ]), + ), + } + : undefined; + return [ + { + 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"), + }, + ]; +} + +const revetKeys = (side: "inlet" | "outlet"): Record => ({ + form: `${side}_revet_form`, + length: `${side}_revet_length_m`, + height: `${side}_revet_height_m`, + before: `${side}_revet_before_m`, + after: `${side}_revet_after_m`, +}); + +export const PIPE_SPEC: StructureSpec = { + typeId: "pipe", + containers: [ + { + id: "pipe", + title: "관", + required: true, + link: "grv6lny", // 정의 그룹 「관」(46장) — 집계표 잇기 자리 그대로 + items: [ + { + id: "diameter", + label: "관경", + kind: "select", + key: "pipe_diameter_mm", + asNumber: true, + choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })), + }, + { id: "length", label: "관 길이 (m)", kind: "auto" }, + ], + }, + { + id: "inlet", + title: "관 보호공 (유입)", + required: true, + link: "gnbf9m9", + items: [ + { + id: "structure", + label: "구조", + kind: "select", + key: "inlet_type", + choices: [ + { value: BASIN, label: "집수정" }, + { value: SOIL, label: "흙막이", also: { inlet_structure: "revet" } }, + ], + }, + ...guardItems({ + [BASIN]: { + form: "inlet_structure", + length: "inlet_basin_length_m", + height: "inlet_basin_height_m", + before: "inlet_basin_before_m", + after: "inlet_basin_after_m", + }, + [SOIL]: revetKeys("inlet"), + }), + ], + }, + { + id: "outlet", + title: "관 보호공 (유출)", + required: true, + link: "g9lq9uk", + items: [ + { + id: "structure", + label: "구조", + kind: "select", + key: "outlet_type", + choices: [{ value: SOIL, label: "흙막이" }], + }, + ...guardItems({ [SOIL]: revetKeys("outlet") }), + ], + }, + { + id: "soil_add", + title: "흙막이 추가", + required: false, + prefix: "soil_add", + link: "gsoiladd", + items: [ + { + id: "structure", + label: "구조", + kind: "select", + choices: [{ value: SOIL, label: "흙막이" }], + }, + ...guardItems(), + ], + }, + { + id: "chute_add", + title: "도수로/돌붙임", + required: false, + prefix: "chute_add", + link: "gchuteadd", + items: [ + { + id: "structure", + label: "구조", + kind: "select", + choices: [ + { value: "도수로", label: "도수로" }, + { value: "돌붙임", label: "돌붙임" }, + ], + }, + ], + }, + ], + // 표 01 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값) + retired: ["pipe_kind", "wing_wall_type", "inlet_basin_form", "inlet_basin_material"], +}; + +/** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */ +export const STRUCTURE_SPECS: Readonly> = { pipe: PIPE_SPEC }; diff --git a/A00_Common/design/design_structure_pipe_calc.ts b/A00_Common/design/design_structure_pipe_calc.ts new file mode 100644 index 000000000..d6432aabe --- /dev/null +++ b/A00_Common/design/design_structure_pipe_calc.ts @@ -0,0 +1,42 @@ +/* ============================================================================= + * design_structure_pipe_calc.ts + * 배관 명세 계산 — 관 길이(자동) · 보호공 높이 안내. 계산 자리 = ② TS 를 서버 Node 로: + * 관 길이는 횡단 기하 `computeStoredLayouts`(B06 · 서버 Node 진입점이 같은 함수로 `pipe_length_m` 를 + * 셈 · 시험 test_b06_pipe_length) 그대로 — 여기서 새로 셈하지 않음 · 화면에 보이기만(저장 안 함). + * 높이 = 손 입력 · 횡단도 조정 높이는 B06 `withSpecDefaults` 가 같은 키(`*_revet_height_m`)에 실음 · + * 빈 칸 안내 = 관경 기준 최소 높이(`pipeWallMinHeightM` ↔ 파이썬 `pipe_wall_min_height_m` 짝). + * ========================================================================== */ + +import { pipeWallMinHeightM } from "../../common_util/common_util_culvert_sets"; +import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch"; +import { computeStoredLayouts } from "../../B06_Section/B06_Section_Structure_Layouts"; +import { PIPE_MATCH_M } from "./design_selection"; + +/** 횡단 측점 목록을 대는 곳 — 횡단 보기가 자료를 받으면 걸어 둠(종단만 연 동안은 없음). */ +let sectionsSource: (() => readonly CrossSection[] | null) | null = null; +export function setPipeSectionsSource(source: () => readonly CrossSection[] | null): void { + sectionsSource = source; +} + +/** 관 길이(m) — 그 관의 주인 측점(관 자리에서 가장 가까운 측점) 횡단 기하 값 · 못 세우면 저장된 + * `pipe_length_m` · 그것도 없으면 null. */ +export function pipeLengthM(chainageM: number | null): number | null { + const sections = sectionsSource?.() ?? null; + if (chainageM === null || !sections?.length) return null; + const holders = sections.filter( + (s) => Math.abs((s.culvert?.chainage_m ?? Infinity) - chainageM) < PIPE_MATCH_M, + ); + if (!holders.length) return null; + const owner = holders.reduce((a, b) => + Math.abs(b.chainage_m - chainageM) < Math.abs(a.chainage_m - chainageM) ? b : a, + ); + const drawn = computeStoredLayouts(owner, sections)?.culvert?.pipe?.lengthM; + const length = typeof drawn === "number" && drawn > 0 ? drawn : owner.design?.pipe_length_m; + return typeof length === "number" && length > 0 ? length : null; +} + +/** 흙막이 높이 빈 칸 안내 — 관경 기준 최소 높이(엔진이 빈 칸을 이 값으로 그림). */ +export function guardHeightHint(diameterMm: string): string { + const d = Number(diameterMm || "1000") / 1000; + return `비우면 관경 기준 ${pipeWallMinHeightM(d)}`; +} diff --git a/A00_Common/design/design_structure_spec.ts b/A00_Common/design/design_structure_spec.ts new file mode 100644 index 000000000..da42af76f --- /dev/null +++ b/A00_Common/design/design_structure_spec.ts @@ -0,0 +1,239 @@ +/* ============================================================================= + * design_structure_spec.ts + * 구조물 명세 공용 틀(PLAN 52) — 화면 요소 없는 순수 규칙. 사용자 구조물 표 한 장 = 명세 한 벌. + * + * · 종류 = 기본 옵션(기준 측점 No + 거리 · 패널 위치 칸이 받음) + 컨테이너[] + 항목[] + * · 컨테이너 = 제목 · 필수(늘 한 벌) / 선택([추가] · [빼기] · 여러 개) + * · 항목 꼴 = 고르기 · 숫자 · 자동(계산이 채움 · 저장 안 함) · 맞물림(길이 = 전 + 후) + * · 상위 값에 따른 선택지 · 저장 키 = `by`(같은 컨테이너 안 상위 항목 값 → 갈래) + * · 저장 = 정본 자리 옵션 그대로(관이면 pipe_points.json `options`) — 필수 컨테이너는 항목 `key` · + * 선택 컨테이너는 인스턴스마다 `{prefix}_{n}_{항목}`(n 은 1부터) + * · 구조물 조합 잇기(`group_items`) 키 = 필수는 `link`(정의 그룹 id) · 선택은 `{link}{n}` + * · 이름 = 정의 `종류.{종류}.이름` 이 있으면 그것(M02 바꿈) · 그룹 이름 · 없으면 명세 글 + * import 없음 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +export interface SpecChoice { + value: string; + label: string; + /** 이 값을 고르면 함께 적는 옵션(하류가 읽는 옛 칸 · 예: 흙막이 = inlet_structure revet). */ + also?: Record; +} + +/** 상위 값 하나에 걸린 갈래 — 저장 키 · 선택지. */ +export interface SpecBranch { + key: string; + choices?: SpecChoice[]; +} + +export type SplitRole = "length" | "before" | "after"; + +export interface SpecItem { + id: string; + label: string; + kind: "select" | "number" | "auto"; + /** 저장 키(필수 컨테이너 · `by` 없을 때). */ + key?: string; + choices?: SpecChoice[]; + /** 같은 컨테이너 상위 항목 값 → 갈래. 갈래가 없으면(상위 비었음) 칸을 가림. */ + by?: { item: string; map: Record }; + /** 길이 = 전 + 후 맞물림 몫. */ + split?: SplitRole; + /** 고르기 값을 수로 저장(관경 등 옛 저장이 수). */ + asNumber?: boolean; + step?: number; + /** 빈 칸 안내 — 함수면 지금 값으로 셈. */ + hint?: string | ((values: Record) => string); +} + +export interface SpecContainer { + id: string; + title: string; + required: boolean; + /** 선택 컨테이너 저장 키 머리 — `{prefix}_{n}_{항목}`. */ + prefix?: string; + /** 구조물 조합 잇기 키 — 필수는 정의 그룹 id 그대로 · 선택은 뒤에 n. */ + link: string; + items: SpecItem[]; +} + +export interface StructureSpec { + typeId: string; + containers: SpecContainer[]; + /** 명세가 화면에서 뺀 옛 키 — 값은 그대로 남고 「지난 정의 값」으로 보임. */ + retired: string[]; +} + +/** 컨테이너 인스턴스 한 벌 값 `{항목 id: 글}`. */ +export type Instance = Record; +export type SpecState = Record; + +/** 항목의 지금 갈래 — `by` 없으면 항목 자체. 상위가 비었으면 null(가림 · 저장 안 함). */ +export function branchOf(item: SpecItem, values: Instance): SpecBranch | null { + if (!item.by) + return item.key || item.choices ? { key: item.key ?? "", choices: item.choices } : null; + return item.by.map[values[item.by.item] ?? ""] ?? null; +} + +const instanceKey = (c: SpecContainer, n: number, item: SpecItem): string => + `${c.prefix}_${n}_${item.id}`; + +/** 항목 저장 키 — 자동 항목 · 갈래 없음 = null. */ +export function keyOf( + c: SpecContainer, + item: SpecItem, + n: number, + values: Instance, +): string | null { + if (item.kind === "auto") return null; + if (!c.required) return instanceKey(c, n, item); + return branchOf(item, values)?.key || null; +} + +/** 지금 고를 수 있는 값 — 갈래 선택지가 앞섬. */ +export function choicesOf(item: SpecItem, values: Instance): SpecChoice[] { + return branchOf(item, values)?.choices ?? item.choices ?? []; +} + +/** 인스턴스 잇기 키 — 필수 = 그룹 id · 선택 = `{link}{n}`. */ +export const linkOf = (c: SpecContainer, n: number): string => + c.required ? c.link : `${c.link}${n}`; + +const text = (value: unknown): string => + value === undefined || value === null ? "" : String(value); + +/** 옵션 → 컨테이너 값. 필수는 한 벌(상위 항목 먼저 읽어 갈래 키를 앎) · 선택은 저장된 n 차례대로. */ +export function readState( + spec: StructureSpec, + options: Record, +): SpecState { + const state: SpecState = {}; + for (const c of spec.containers) { + if (c.required) { + const values: Instance = {}; + const plain = c.items.filter((item) => !item.by); + for (const item of [...plain, ...c.items.filter((item) => item.by)]) { + const key = keyOf(c, item, 1, values); + if (key) values[item.id] = text(options[key]); + } + state[c.id] = [values]; + continue; + } + const head = new RegExp(`^${c.prefix}_(\\d+)_`); + const ns = [ + ...new Set( + Object.keys(options) + .map((k) => head.exec(k)?.[1]) + .filter((n): n is string => !!n) + .map(Number), + ), + ].sort((a, b) => a - b); + state[c.id] = ns.map((n) => + Object.fromEntries(c.items.map((item) => [item.id, text(options[instanceKey(c, n, item)])])), + ); + } + return state; +} + +/** 컨테이너 값 → 옵션(빈 칸 뺌 · 숫자 칸은 수 · 선택 컨테이너는 차례대로 1 부터 다시 매김). */ +export function writeOptions( + spec: StructureSpec, + state: SpecState, +): Record { + const out: Record = {}; + for (const c of spec.containers) { + (state[c.id] ?? []).forEach((values, index) => { + for (const item of c.items) { + const key = keyOf(c, item, index + 1, values); + const raw = (values[item.id] ?? "").trim(); + if (!key || raw === "") continue; + if (item.kind === "number" || item.asNumber) { + const n = Number(raw); + if (Number.isFinite(n)) out[key] = Math.round(n * 100) / 100; + continue; + } + out[key] = raw; + if (c.required) + Object.assign(out, choicesOf(item, values).find((ch) => ch.value === raw)?.also); + } + }); + } + return out; +} + +/** 명세가 다루는 키 전부(필수 갈래 · also 포함) + 선택 컨테이너 머리 — 지난 정의 값 판정 · 저장 병합. */ +export function specKeys(spec: StructureSpec): { keys: string[]; prefixes: string[] } { + const keys = new Set(); + const prefixes: string[] = []; + for (const c of spec.containers) { + if (!c.required) { + prefixes.push(`${c.prefix}_`); + continue; + } + for (const item of c.items) { + const branches = item.by ? Object.values(item.by.map) : [branchOf(item, {})]; + for (const branch of branches) { + if (branch?.key) keys.add(branch.key); + for (const ch of branch?.choices ?? item.choices ?? []) + Object.keys(ch.also ?? {}).forEach((k) => keys.add(k)); + } + } + } + return { keys: [...keys], prefixes }; +} + +/** + * 길이 = 전 + 후 맞물림 — 마지막에 고친 둘을 지키고 나머지 하나를 셈(정함 52). + * `recent` = 고친 차례(최근이 앞) · 둘이 안 차면 길이 → 전 → 후 차례로 채움. 지킬 둘 중 빈 칸이 있으면 안 셈. + */ +export function solveSplit( + values: Partial>, + recent: SplitRole[], +): Partial> { + const keep = [...new Set([...recent, "length", "before", "after"] as SplitRole[])].slice(0, 2); + const num = (role: SplitRole): number | null => { + const raw = (values[role] ?? "").trim(); + const n = Number(raw); + return raw !== "" && Number.isFinite(n) ? n : null; + }; + const [a, b] = keep.map(num); + if (a === null || b === null) return values; + const free = (["length", "before", "after"] as SplitRole[]).find((r) => !keep.includes(r))!; + const got = { length: num("length"), before: num("before"), after: num("after") }; + const result = + free === "length" + ? (got.before ?? 0) + (got.after ?? 0) + : Math.max((got.length ?? 0) - (free === "before" ? (got.after ?? 0) : (got.before ?? 0)), 0); + return { ...values, [free]: String(Math.round(result * 100) / 100) }; +} + +/** 이름 바꿈 표(정의 `종류.{종류}.이름`) — 키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`. */ +export type SpecNames = Record; + +export const containerTitle = (c: SpecContainer, names: SpecNames, groupName?: string): string => + names[c.id] ?? groupName ?? c.title; +export const itemLabel = (c: SpecContainer, item: SpecItem, names: SpecNames): string => + names[`${c.id}.${item.id}`] ?? item.label; +export const choiceLabel = ( + c: SpecContainer, + item: SpecItem, + ch: SpecChoice, + names: SpecNames, +): string => names[`${c.id}.${item.id}=${ch.value}`] ?? ch.label; + +/** 명세의 이름 키 전부(정의 파일에 올릴 표) — `{키: 명세 글}`. */ +export function nameTable(spec: StructureSpec): SpecNames { + const out: SpecNames = {}; + for (const c of spec.containers) { + out[c.id] = c.title; + for (const item of c.items) { + out[`${c.id}.${item.id}`] = item.label; + const all = [ + ...(item.choices ?? []), + ...Object.values(item.by?.map ?? {}).flatMap((b) => b.choices ?? []), + ]; + for (const ch of all) out[`${c.id}.${item.id}=${ch.value}`] = ch.label; + } + } + return out; +} diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 32f187eb0..a2174fee4 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -73,6 +73,7 @@ import { createStandardSlot, } from "../../B06_Section/B06_Section_UI_Page_Panel"; import type { DesignPageHost } from "./design_page"; +import { setPipeSectionsSource } from "./design_structure_pipe_calc"; import { bindDesignView, bindPipeOptions, @@ -94,6 +95,8 @@ export async function mountCrossView( const projectId = shared.projectId || null; let currentRouteId: number | null = null; let sectionDetail: SectionDetailResponse | null = null; + // 배관 명세 관 길이(자동) = 이 횡단 자료의 기하(PLAN 52-1) + setPipeSectionsSource(() => sectionDetail?.cross_sections ?? null); let stationInterval: number | undefined; // 표준 횡단면 설정 패널 자리. context 로드 후 config 기본값으로 채운다. diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index 81e5c579d..c3fb9948f 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -96,6 +96,8 @@ export interface StructureType { spots?: Record | null; /** 상위 → 하위 규칙(48 정의 `규칙` 중 이 종류 것). */ rules?: DefinedRule[] | null; + /** 구조물 명세 이름 바꿈(52 정의 `종류.이름` · `{컨테이너}[.{항목}[={값}]]: 이름`). */ + spec_names?: Record; } /** 옵션 그룹 한 개 — 서버가 M02 「구조물 옵션 보이기」 그룹 표로 채움. */ diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index ee870d18b..e7371d120 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -7,10 +7,9 @@ * 시설 종류는 구조물 배치의 "종류" 드롭다운이, 위치(시작·기준·종료 측점)는 위치 * 칸이 담당하므로 여기에는 부속 옵션 필드만 있다. * - * 배관 부속(2026-08-17 UI 개편): 관종·관경(기본 1000) 한 행 + 유입구·유출구 - * 소그룹. 유입구는 집수정/기슭막이 택일이고, 유출구는 기슭막이뿐이다(현재 다른 - * 선택지 없음). 기슭막이 조각은 형태·길이·높이·바닥 보호공을 한 벌로 묶어 - * 유입·유출·독립 구조물(D4 기슭막이)이 **같은 인터페이스**를 쓴다. + * 배관 부속(PLAN 52-1): 사용자 구조물 표 01 명세 폼(`A00_Common/design/design_structure_pipe`) + * — 관 · 관 보호공(유입 · 유출) · 흙막이 추가 · 도수로/돌붙임 컨테이너. 저장 키는 옛 키 그대로 · + * 옛 관종 · 날개벽 · 집수정 형식 칸은 화면에서 빠지고 값은 남음(지난 정의 값). * * 보호공(2026-08-17 사용자 지시): 구 "돌붙임 있음/없음 + 표면처리(찰/메)" 두 축을 * `돌붙임(찰)·돌붙임(메)·도수로` 한 축으로 합쳤다. 도수로·산비탈수로(B4)는 유출부에서 @@ -38,26 +37,23 @@ import type { import { BOX_SIZE_CUSTOM, BOX_SIZE_PRESETS, - createBasinSplitFields, - createRevetmentFields, createRevetSideGroup, createWingFields, grid, group, - INLET_KINDS, INLET_REVET_KEYS, - inletKindOf, type InletStructureKind, labeled, numberInput, optionalSelect, OUTLET_REVET_KEYS, putNumber, - stepper, WING_IN_KEYS, WING_OUT_KEYS, } from "./B05_Profile_UI_Drainage_Facility_Fields"; -import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets"; +import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form"; +import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe"; +import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc"; import { hasLinks, linkFields, @@ -206,7 +202,17 @@ export interface FacilityOptionsForm { options?: Record, /** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */ designFlow?: number | null, + /** 종류 정의 — 배관 명세 이름 바꿈(`spec_names`) · 그룹 이름(PLAN 52). */ + definition?: { + spec_names?: Record; + groups?: Array<{ id: string; name: string }>; + } | null, ) => void; + /** 배관 명세 컨테이너 「구조물 조합」 잇기 자리 — 폼이 패널 그룹 값에 이음(PLAN 52-1). */ + bindLinks: (links: SpecLinks) => void; + paintLinks: () => void; + /** 고른 관 측점 — 관 길이(자동) 계산에 씀. */ + setChainage: (chainageM: number | null) => void; /** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). 숨긴 칸은 폼이 받은 옛 값 그대로. */ readOptions: () => Record; /** 「구조물 옵션 보이기」에서 끈 레지스트리 옵션 키 — 그 칸을 가린다(값은 옛 값 보존). @@ -244,87 +250,56 @@ export function createFacilityOptionsForm( root.className = "b05-drainage__facility"; root.hidden = true; - // ── 배관 본체 — 관종 > 관경 한 행(2026-08-17 사용자 지시 4) ────────────── - // 기본값(관종 파형강관 · 관경 1000 · 등록부)은 시설이 생길 때 칸에 실제 값으로 들어옴(PLAN 46-7). + // ── 관종 · 관경 한 행 — 세월교 몫(배관은 아래 명세 폼 · PLAN 52-1) ───────── const pipeMaterial = optionalSelect("— 안 정함 —", ["흄관", "VR관", "파형강관"]); const pipeDiameter = document.createElement("select"); pipeDiameter.replaceChildren( new Option("— 안 정함 —", ""), ...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)), ); - // 관보호공 날개벽 — **개소당 원단위**가 형식마다 붙박이라 고르는 칸이 있어야 물량이 선다 - // (2026-09-09, 계획서 4-13 · 소광리 원본 탭 다섯). 비우면 날개벽을 안 센다. - const wingWall = optionalSelect("안 놓음", ["A-TYPE", "C-TYPE", "A-TYPE+집수정"]); const pipeRow = grid( labeled("관종", pipeMaterial, "pipe_kind"), labeled("관경", pipeDiameter, "pipe_diameter_mm"), ); - const wingRow = grid(labeled("관보호공 날개벽", wingWall, "wing_wall_type")); - // ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ───────────────── - const inletGroup = group("유입구"); - // 「안 정함」이면 기슭막이 칸을 보이되 값은 안 실음. - const inletType = document.createElement("select"); - inletType.replaceChildren( - new Option("— 안 정함 —", ""), - ...INLET_KINDS.map((kind) => new Option(kind.label, kind.label)), - ); - const basinForm = optionalSelect("선택", [ - "동물이동형", - "□형(기본형)", - "돌집수정 ㄴ형", - "돌집수정 ㄷ형", - ]); - // 집수정도 재질·표면처리를 형태 한 축으로 합친다(사용자 지시 1). - const basinMaterial = optionalSelect("선택", [ - "콘크리트", - "조립식 주철맨홀", - "돌쌓기(찰)", - "돌쌓기(메)", - ]); - const basinMaterialField = labeled("형태", basinMaterial, "inlet_basin_material"); - // 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자). - const basinLength = numberInput("0.1", "0"); - // 기준측점 전 · 후 — 옛 횡단 조정창에만 있던 칸을 폼으로(계획서 24-4a). - const basinSplit = createBasinSplitFields(basinLength); - basinSplit.onChange(() => emit()); - const basinRows = [ - grid( - labeled("형식", basinForm, "inlet_basin_form"), - labeled("길이 (m)", stepper(basinLength, 1), "inlet_basin_length_m"), - ), - basinSplit.row, - ]; - const height = (): string => - String(pipeWallMinHeightM(Number(pipeDiameter.value || "1000") / 1000)); - const inletRevet = createRevetmentFields(INLET_REVET_KEYS, { height }); - // 첫 행 = [구조][형태] — 구조에 따라 집수정 형태와 기슭막이 형태가 갈아 끼워진다 - // (2026-08-17 사용자 지시 3). - // 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리(2026-08-29 사용자 지시 4). - // 비어 있으면 아무 자리도 차지하지 않는다. - const inletSlot = document.createElement("div"); - inletSlot.className = "b05-structure__adjust-slot"; - const outletSlot = inletSlot.cloneNode() as HTMLDivElement; - inletGroup.body.append( - grid( - labeled("구조", inletType, ["inlet_type", "inlet_structure"]), - basinMaterialField, - inletRevet.formField, - ), - ...basinRows, - ...inletRevet.rows, - inletSlot, - ); - - // ── 유출구 — 구조 선택지는 기슭막이뿐이지만 양식을 맞춘다(사용자 지시 2·4) ── - const outletGroup = group("유출구"); - const outletType = optionalSelect("— 안 정함 —", ["기슭막이"]); - const outletRevet = createRevetmentFields(OUTLET_REVET_KEYS, { height }); - outletGroup.body.append( - grid(labeled("구조", outletType, "outlet_type"), outletRevet.formField), - ...outletRevet.rows, - outletSlot, + // ── 배관 = 사용자 구조물 표 01 명세(PLAN 52-1) — 컨테이너 다섯 · 옛 관종 · 날개벽 칸은 뺌(지난 정의 값) ── + let linkSource: SpecLinks | null = null; + let chainage: number | null = null; + const pipeForm = createSpecForm( + { + get: () => linkSource?.get() ?? {}, + pick: (key) => linkSource?.pick(key), + clear: (key) => linkSource?.clear(key), + set: (key, item) => linkSource?.set(key, item), + }, + { + auto: (_c, item) => { + if (item.id !== "length") return ""; + const length = pipeLengthM(chainage); + return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2); + }, + hint: (c, item, state) => { + const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이"; + return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : ""; + }, + onChange: (c, item) => { + if (c.id === "inlet" && (item.id === "structure" || item.id === "form")) + inletStructureHandler?.(inletStructureOf()); + emit(); + }, + }, ); + // 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — 유입 · 유출 컨테이너 끝(2026-08-29 지시 4) + const inletSlot = pipeForm.slot("inlet"); + const outletSlot = pipeForm.slot("outlet"); + /** 유입 보호공 구조 → B06 유입측 형식 값(조정창과 같은 값). */ + const inletStructureOf = (): InletStructureKind => { + const inlet = pipeForm.state().inlet?.[0] ?? {}; + if (inlet.structure === "기슭막이") return "revet"; + return inlet.structure === "집수정" && ["I", "L", "U"].includes(inlet.form ?? "") + ? (inlet.form as InletStructureKind) + : "auto"; + }; // ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자 // 지시 4). 유입구·유출구와 같은 양식이되, 값은 조정창 사본의 행이 그대로 옮겨 온다 @@ -425,13 +400,11 @@ export function createFacilityOptionsForm( root.append( saveNote, pipeRow, - wingRow, + pipeForm.root, ford.widthRow, ford.slopeRow, ford.countRow, ford.summary, - inletGroup.root, - outletGroup.root, extraGroup.root, revetTopRow, revetInlet.root, @@ -448,12 +421,8 @@ export function createFacilityOptionsForm( if (current === null) return; const isPipe = current === "pipe"; const isBox = current === "box_culvert"; - // 관종·관경은 배수관과 세월교가 공유한다. - pipeRow.hidden = !isPipe && current !== "ford_bridge"; - // 관보호공 날개벽 원단위는 **배수관 Ø800·Ø1000** 것뿐이다 — 다른 시설에서는 숨긴다. - wingRow.hidden = !isPipe; - inletGroup.root.hidden = !isPipe; - outletGroup.root.hidden = !isPipe; + pipeRow.hidden = current !== "ford_bridge"; + pipeForm.root.hidden = !isPipe; boxWrap.hidden = !isBox; // 날개벽은 BOX암거와 세월교가 같은 옵션 한 벌을 쓴다(2026-08-25 사용자 확정). const hasWing = isBox || current === "ford_bridge"; @@ -482,17 +451,6 @@ export function createFacilityOptionsForm( wingInFields.syncVisibility(); wingOutFields.syncVisibility(); } - if (isPipe) { - // 유입구 구조에 따라 집수정 칸과 기슭막이 칸을 갈아 끼운다(사용자 지시 6~8). - // 병합 목록이라 판정은 라벨이 아니라 매핑된 정본 종류로 한다(2026-08-29). - const kind = inletKindOf(inletType.value); - const basin = kind.type === "집수정"; - const revet = kind.type === "기슭막이"; - basinRows.forEach((row) => (row.hidden = !basin)); - basinMaterialField.hidden = !basin; - inletRevet.formField.hidden = !revet; - inletRevet.rows.forEach((row) => (row.hidden = !revet)); - } } function emit(): void { @@ -500,11 +458,6 @@ export function createFacilityOptionsForm( callbacks.onChange?.(); } - [inletType, outletType, wingWall, basinForm, basinMaterial, basinLength].forEach((input) => - input.addEventListener("change", emit), - ); - inletRevet.onChange(emit); - outletRevet.onChange(emit); revetInlet.fields.onChange(emit); revetOutlet.fields.onChange(emit); revetSide.addEventListener("change", emit); @@ -521,9 +474,6 @@ export function createFacilityOptionsForm( /** 유입구 구조 변경 수신자(B06 유입측 형식 제어). B05는 비워 둔다. */ let inletStructureHandler: ((value: InletStructureKind) => void) | null = null; - inletType.addEventListener("change", () => - inletStructureHandler?.(inletKindOf(inletType.value).structure), - ); return { root, @@ -538,23 +488,31 @@ export function createFacilityOptionsForm( revetSideLabels = labels; syncVisibility(); }, - inletStructure: () => inletKindOf(inletType.value).structure, + inletStructure: inletStructureOf, setInletStructure(value) { - // 지금 고른 정본 종류(기슭막이/집수정)는 지키고 형식만 맞춘다. - const type = inletKindOf(inletType.value).type; - const hit = - INLET_KINDS.find((kind) => kind.type === type && kind.structure === value) ?? - INLET_KINDS.find((kind) => kind.structure === value); - if (hit) inletType.value = hit.label; + // 조정창 형식 → 유입 보호공 구조 · 형태(자동 = 고른 구조는 지키고 형태만 비움) + if (value === "revet") pipeForm.setValue("inlet", "structure", "기슭막이"); + else if (value !== "auto") pipeForm.setValue("inlet", "structure", "집수정"); + if (value !== "revet" && pipeForm.state().inlet?.[0]?.structure === "집수정") + pipeForm.setValue("inlet", "form", value === "auto" ? "" : value); syncVisibility(); }, + bindLinks(links) { + linkSource = links; + pipeForm.paintLinks(); + }, + paintLinks: () => pipeForm.paintLinks(), + setChainage(chainageM) { + chainage = chainageM; + if (current === "pipe") pipeForm.refresh(); + }, onInletStructureChange(handler) { inletStructureHandler = handler; }, setHiddenOptions(facility, registryKeys) { hiddenOptions.set(facility, registryKeys); }, - setFacility(facility, options = {}, designFlow = null) { + setFacility(facility, options = {}, designFlow = null, definition = null) { current = facility; hiddenOptions.remember(options); ford.setDesignFlow(designFlow ?? null); @@ -567,25 +525,10 @@ export function createFacilityOptionsForm( // 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). pipeMaterial.value = text("pipe_kind"); pipeDiameter.value = text("pipe_diameter_mm"); - // 병합 목록: 저장된 정본 둘(inlet_type·inlet_structure)로 라벨을 되찾는다. - const savedType = text("inlet_type"); - const savedStructure = text("inlet_structure"); - inletType.value = savedType - ? ( - INLET_KINDS.find( - (kind) => - kind.type === savedType && (!savedStructure || kind.structure === savedStructure), - ) ?? inletKindOf(savedType) - ).label - : ""; - outletType.value = text("outlet_type"); - wingWall.value = text("wing_wall_type"); - basinForm.value = text("inlet_basin_form"); - basinMaterial.value = text("inlet_basin_material"); - basinLength.value = text("inlet_basin_length_m"); - basinSplit.write(options); - inletRevet.write(options); - outletRevet.write(options); + if (facility === "pipe") { + const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); + pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups); + } // 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다. // 값이 없으면 「안 정함」. const savedWidth = Number.parseFloat(text("body_width_m")); @@ -625,24 +568,7 @@ export function createFacilityOptionsForm( if (pipeMaterial.value) options.pipe_kind = pipeMaterial.value; }; if (current === "pipe") { - putPipe(); - const inletKind = inletKindOf(inletType.value); - if (wingWall.value) options.wing_wall_type = wingWall.value; - // B06 유입측 형식(조정창과 같은 값) — 병합 드롭다운이 함께 정한다(2026-08-29). - if (inletType.value) { - options.inlet_type = inletKind.type; - options.inlet_structure = inletKind.structure; - } - if (inletKind.type === "집수정") { - if (basinForm.value) options.inlet_basin_form = basinForm.value; - if (basinMaterial.value) options.inlet_basin_material = basinMaterial.value; - putNumber(options, "inlet_basin_length_m", basinLength.value); - basinSplit.read(options); - } else { - inletRevet.read(options); - } - if (outletType.value) options.outlet_type = outletType.value; - outletRevet.read(options); + Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1) } else if (current === "box_culvert") { // 프리셋을 골랐든 사용자 지정을 적었든 정본은 폭·높이 두 칸이다. putNumber(options, "body_width_m", boxWidth.value); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts index 9a33297c5..4e6afd657 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts @@ -26,15 +26,14 @@ const wingKeys = (side: "in" | "out"): string[] => [ /** 시설 종류별로 **폼이 적는 칸** — `createFacilityOptionsForm().readOptions()` 가 쓰는 키 전부. */ export const FORM_OPTION_KEYS: Readonly> = { + // 배관 = 표 01 명세 키(PLAN 52-1 · `design_structure_pipe`) — 옛 관종 · 날개벽 · 집수정 형식은 폼에서 + // 빠져 이어 붙임(지난 정의 값) · 선택 컨테이너 키는 `FORM_OPTION_PREFIXES` pipe: [ "pipe_diameter_mm", - "pipe_kind", - "wing_wall_type", "inlet_type", "inlet_structure", - "inlet_basin_form", - "inlet_basin_material", "inlet_basin_length_m", + "inlet_basin_height_m", "inlet_basin_before_m", "inlet_basin_after_m", ...revetKeys("inlet"), @@ -55,6 +54,11 @@ export const FORM_OPTION_KEYS: Readonly> = { revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")], }; +/** 폼이 인스턴스 번호를 붙여 적는 키 머리(명세 선택 컨테이너 · `{머리}{n}_{항목}`) — 빼면 지워짐. */ +export const FORM_OPTION_PREFIXES: Readonly> = { + pipe: ["soil_add_", "chute_add_"], +}; + /** 독립 기슭막이 레지스트리 키 → 폼은 유입·유출 두 채널 키로 적는다. */ const REVETMENT_FIELDS = ["form", "length_m", "height_m", "before_m", "after_m"]; @@ -95,7 +99,10 @@ export function mergeFacilityOptions( ): T { if (!previous || previous.facility !== next.facility) return next; const managed = new Set(FORM_OPTION_KEYS[next.facility] ?? []); - const kept = Object.entries(previous.options ?? {}).filter(([key]) => !managed.has(key)); + const heads = FORM_OPTION_PREFIXES[next.facility] ?? []; + const kept = Object.entries(previous.options ?? {}).filter( + ([key]) => !managed.has(key) && !heads.some((head) => key.startsWith(head)), + ); const options = { ...Object.fromEntries(kept), ...(next.options ?? {}) }; const summary = next.summary_item === undefined && previous.summary_item !== undefined diff --git a/B05_Profile/B05_Profile_UI_Structures_Define.ts b/B05_Profile/B05_Profile_UI_Structures_Define.ts index 4b1b22660..4c5ecf599 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define.ts @@ -186,7 +186,7 @@ export function shapeFields( export interface StructureDefinition { 판?: number; 구조물군?: Record; - 종류?: Record; + 종류?: Record }>; 그룹?: Record>; 배치?: Record>; 규칙?: DefinedRule[]; @@ -206,6 +206,7 @@ export function withDefinition< group_name: definition.구조물군?.[type.group], show_anchor: kind.기준측점, behaviors: kind.동작 ?? [], + spec_names: kind.이름 ?? {}, spots: definition.배치?.[type.type_id] ?? null, rules: (definition.규칙 ?? []).filter((rule) => rule.종류 === type.type_id), ...(type.groups diff --git a/B05_Profile/B05_Profile_UI_Structures_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Form.ts index e4a177b27..c0e28f3fc 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Form.ts @@ -156,6 +156,14 @@ export function buildStructuresForm(options: { const summary = buildSummaryRow(); summary.root.hidden = true; const groupBoxes = createGroupBoxes(); + // 배관 명세 컨테이너 「구조물 조합」 잇기 = 그룹 값(`group_items`)과 같은 자리 · 같은 [고르기] 길(PLAN 52-1) + facilityOptions.bindLinks({ + get: groupBoxes.get, + pick: groupBoxes.pick, + clear: groupBoxes.clear, + set: groupBoxes.setOne, + }); + groupBoxes.watch(facilityOptions.paintLinks); const staleBox = document.createElement("fieldset"); staleBox.className = "b05-structure__group b05-structure__stale"; staleBox.hidden = true; @@ -164,11 +172,11 @@ export function buildStructuresForm(options: { typeRow, ownerNote, positionRow, + summary.root, // 종류 단위 「구조물 조합」 잇기 — 기준 측점 바로 아래(PLAN 52-1 · 컨테이너 없는 종류) positionDivider, optionRow, facilityOptions.root, staleBox, - summary.root, groupBoxes.root, actions, ); diff --git a/B05_Profile/B05_Profile_UI_Structures_Groups.ts b/B05_Profile/B05_Profile_UI_Structures_Groups.ts index ea74cd5ad..f41119d99 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Groups.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Groups.ts @@ -22,6 +22,7 @@ import { buildSummaryRow, type SummaryRow } from "./B05_Profile_UI_Structures_Su import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary"; import { SHAPE_FIELDS, shapeFields, shapeKey } from "./B05_Profile_UI_Structures_Define"; import { field, numberInput } from "./B05_Profile_UI_Structures_Fields"; +import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { GROUP_JOIN, type GroupItems, @@ -46,6 +47,11 @@ export interface GroupBoxes { shapeKeys: (type?: StructureType | null) => string[]; /** 구조물 옵션에서 형상 값을 실음(폼 불러오기). */ setShapes: (options: Record | undefined) => void; + /** 명세 컨테이너 잇기 줄(PLAN 52-1)이 쓰는 [고르기] · [해제] — 그룹 상자와 같은 길. */ + pick: (groupId: string) => void; + clear: (groupId: string) => void; + /** 값이 바뀔 때마다 부름(명세 컨테이너 잇기 줄 다시 그림). */ + watch: (listener: () => void) => void; } interface GroupHandlers { @@ -63,6 +69,8 @@ export function createGroupBoxes(): GroupBoxes { let handlers: GroupHandlers | null = null; let shapes: Record = {}; let current: StructureType | null = null; + const listeners: Array<() => void> = []; + const changed = (): void => listeners.forEach((listener) => listener()); const keysOf = (type: StructureType | null | undefined): string[] => (type?.groups ?? []).flatMap((g) => SHAPE_FIELDS.map((f) => shapeKey(g.id, f.suffix))); const shapeInputs = new Map(); @@ -107,7 +115,8 @@ export function createGroupBoxes(): GroupBoxes { rows.clear(); shapeInputs.clear(); current = type; - const groups = type?.groups ?? []; + // 명세 종류(PLAN 52 · 배관)는 컨테이너가 잇기 줄을 가짐 — 그룹 상자를 안 세움 + const groups = type && STRUCTURE_SPECS[type.type_id] ? [] : (type?.groups ?? []); const boxes: HTMLElement[] = []; bodies = []; // 「구조물 옵션 보이기」에서 끈 칸 — 시설 폼 밖으로 옮긴 칸도 같은 규칙으로 가림(Hidden 과 같은 식) @@ -187,7 +196,10 @@ export function createGroupBoxes(): GroupBoxes { boxes.push(box); bodies.push(body); } - const stale = type ? paintStale(new Set(groups.map((g) => g.id))) : null; + const stale = + type && !STRUCTURE_SPECS[type.type_id] + ? paintStale(new Set(groups.map((g) => g.id))) + : null; root.replaceChildren(...boxes, ...(stale ? [stale] : [])); root.hidden = !type || (!boxes.length && !stale); // 잠금은 계곡 통과 시설 서브폼(`inert`)을 따름 — 레지스트리 칸은 칸마다 disabled 라 그대로 @@ -204,6 +216,7 @@ export function createGroupBoxes(): GroupBoxes { set(next) { items = { ...next }; for (const [id, row] of rows) row.set(items[id] ?? null); + changed(); }, setOne(groupId, item) { if (item) items = { ...items, [groupId]: item }; @@ -212,7 +225,11 @@ export function createGroupBoxes(): GroupBoxes { items = rest; } rows.get(groupId)?.set(item); + changed(); }, + pick: (groupId) => handlers?.pick(groupId), + clear: (groupId) => handlers?.clear(groupId), + watch: (listener) => listeners.push(listener), bind(next) { handlers = next; }, diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 5731e515d..7f1c2ffe0 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -24,6 +24,7 @@ import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; import { field, optionDefaults } from "./B05_Profile_UI_Structures_Fields"; import { + hasSpecContainers, paintStale, renderOptionFields as renderOptionFieldsInto, } from "./B05_Profile_UI_Structures_Panel_Options"; @@ -56,11 +57,9 @@ import { hasComputedRange, readBeforeOption, readLengthOption, -} from "./B05_Profile_UI_Structures_Panel_Types"; -import type { - PipeFacilityItem, - StructuresCallbacks, - StructuresSection, + type PipeFacilityItem, + type StructuresCallbacks, + type StructuresSection, } from "./B05_Profile_UI_Structures_Panel_Types"; export function createStructuresSection( @@ -228,7 +227,7 @@ export function createStructuresSection( !!type && !managed && type.placement === "interval" && !hasComputedRange(type); positionRow.hidden = !type; positionDivider.hidden = !type; - summary.root.hidden = !type; + summary.root.hidden = !type || hasSpecContainers(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1) startFields.wrap.hidden = !isInterval; endFields.wrap.hidden = !isInterval; primary.disabled = !type; @@ -252,7 +251,8 @@ export function createStructuresSection( designFlow: number | null = null, ): void { const kind = facilityFormKind(currentType()); - facilityOptions.setFacility(kind, options ?? newDefaults(), designFlow); + facilityOptions.setChainage(selectedPipeChainage ?? tempPipeChainage); // 관 길이(자동 · 52-1) + facilityOptions.setFacility(kind, options ?? newDefaults(), designFlow, currentType()); if (kind) paintStale(staleBox, currentType(), options, kind, groupBoxes.shapeKeys(currentType())); // 「구조물 옵션 보이기」에서 끈 옵션은 시설 폼에서도 가림(값은 옛 값 그대로 저장) diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index 5a30bf77c..97032d397 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -20,6 +20,12 @@ import { staleValues, } from "./B05_Profile_UI_Structures_Define"; import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge"; +import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; +import { specKeys } from "../A00_Common/design/design_structure_spec"; + +/** 구조물 명세 컨테이너로 그리는 종류인지 — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1). */ +export const hasSpecContainers = (type: StructureType): boolean => + !!STRUCTURE_SPECS[type.type_id]?.containers.length; /** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */ export interface OptionRenderContext { @@ -175,12 +181,18 @@ export function paintStale( /** 그룹 형상 키(`{그룹 id}__{칸}`) — 정의된 그룹 몫이라 지난 값이 아님. */ shapeKeys: string[] = [], ): void { + // 명세 종류(52) — 명세가 뺀 옛 키(retired)는 지난 정의 값 · 선택 컨테이너 키(머리)는 앎 + const spec = type ? STRUCTURE_SPECS[type.type_id] : undefined; + const own = spec ? specKeys(spec) : { keys: [], prefixes: [] }; const known = [ ...shapeKeys, ...(type?.options ?? []).map((o) => o.key), ...(facility ? (FORM_OPTION_KEYS[facility] ?? []) : []), - ]; - const stale = type ? staleValues(values, known) : []; + ...own.keys, + ].filter((key) => !spec?.retired.includes(key)); + const stale = type + ? staleValues(values, known).filter(([key]) => !own.prefixes.some((p) => key.startsWith(p))) + : []; box.replaceChildren(); box.hidden = !stale.length; if (!stale.length) return; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Definition.py b/M02_MasterTemplete/StructOptions/M02_SO_Definition.py index 0ba02af12..439e0daf0 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Definition.py +++ b/M02_MasterTemplete/StructOptions/M02_SO_Definition.py @@ -87,8 +87,15 @@ def _type_errors(doc: dict[str, Any]) -> tuple[list[str], dict[str, dict[str, An return errors, keys +def _names_ok(value: Any) -> bool: + """구조물 명세 이름 바꿈 표 — `{컨테이너[.항목[=값]]: 빈 글 아닌 이름}`.""" + return isinstance(value, dict) and all( + isinstance(k, str) and isinstance(v, str) and v.strip() for k, v in value.items() + ) + + def _kind_errors(doc: dict[str, Any], keys: dict[str, dict[str, Any]]) -> list[str]: - """종류마다 덧칸 — `{기준측점: 참거짓, 동작: [동작 이름]}`.""" + """종류마다 덧칸 — `{기준측점: 참거짓, 동작: [동작 이름], 이름: {명세 이름 키: 이름}}`(이름 = PLAN 52).""" errors: list[str] = [] acts = doc.get("동작") if isinstance(doc.get("동작"), dict) else {} for tid, row in doc["종류"].items(): @@ -100,8 +107,10 @@ def _kind_errors(doc: dict[str, Any], keys: dict[str, dict[str, Any]]) -> list[s continue if name == "동작" and isinstance(value, list) and all(a in acts for a in value): continue + if name == "이름" and _names_ok(value): + continue errors.append( - f"종류 덧칸 「{tid}.{name}」 이 틀림(기준측점 참거짓 · 동작 = 동작 이름들)" + f"종류 덧칸 「{tid}.{name}」 이 틀림(기준측점 참거짓 · 동작 = 동작 이름들 · 이름 = 글 묶음)" ) return errors diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index ce30d572d..132169e26 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -1,5 +1,5 @@ { - "판": 5, + "판": 6, "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`가 새 키로 옮긴다.", "구조물군": { @@ -3232,7 +3232,66 @@ "계곡통과시설", "유입유출_묶음", "기슭막이_한벌" - ] + ], + "이름": { + "pipe": "관", + "pipe.diameter": "관경", + "pipe.diameter=800": "800", + "pipe.diameter=1000": "1000", + "pipe.diameter=1200": "1200", + "pipe.diameter=1500": "1500", + "pipe.length": "관 길이 (m)", + "inlet": "관 보호공 (유입)", + "inlet.structure": "구조", + "inlet.structure=집수정": "집수정", + "inlet.structure=기슭막이": "흙막이", + "inlet.form": "형태", + "inlet.form=돌쌓기(찰)": "돌 쌓기(찰)", + "inlet.form=돌쌓기(메)": "돌 쌓기(메)", + "inlet.form=콘크리트": "콘크리트", + "inlet.form=돌망태": "돌망태", + "inlet.form=통나무·목재틀": "통나무/목재틀", + "inlet.form=바자": "바자", + "inlet.form=I": "I형", + "inlet.form=L": "L형", + "inlet.form=U": "ㄷ형", + "inlet.length": "길이 (m)", + "inlet.height": "높이 (m)", + "inlet.before": "기준측점 전 (m)", + "inlet.after": "기준측점 후 (m)", + "outlet": "관 보호공 (유출)", + "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.structure": "구조", + "soil_add.structure=기슭막이": "흙막이", + "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=돌붙임": "돌붙임" + } }, "box_culvert": { "동작": [ diff --git a/resources/master_template/구조물정의_이력/판_5.json b/resources/master_template/구조물정의_이력/판_5.json new file mode 100644 index 000000000..ce30d572d --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_5.json @@ -0,0 +1,3303 @@ +{ + "판": 5, + "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`가 새 키로 옮긴다.", + "구조물군": { + "A": "A 횡단배수", + "B": "B 종단배수", + "C": "C 사면안정", + "D": "D 계류·사방", + "E": "E 안전·부대·용지", + "F": "F 생태·녹화", + "G": "G 노면공", + "호환": "기타" + }, + "동작": { + "계곡통과시설": "관 지점 정본(pipe_points.json) 시설 — 부속 옵션 서브폼을 손코딩으로 그림 · 저장은 폼이 아는 칸만 갈아 끼움", + "유입유출_묶음": "관 유입구(집수정 · 기슭막이 택일) · 유출구(기슭막이) 소그룹", + "기슭막이_한벌": "기슭막이 형태 · 길이 · 높이 · 바닥 보호공 한 벌 — 관 유입 · 유출 · 독립 기슭막이가 같은 조각", + "BOX_본체규격": "BOX암거 본체 규격 프리셋 고르기(폭 × 높이 한 칸)", + "날개벽": "날개벽 유입 · 유출 · 짧은쪽 높이 · 길이 · 각도 조각", + "월류단면": "월류 폭 · 높이 · 바닥 경사 · 포장 두께 + 개략 단면 줄 · 필요 수심 미만은 되돌림", + "포장_물넘이_겹침": "포장 구간이 물넘이포장을 품으면 앞 · 뒤로 나눔 · 끝만 걸치면 저장 멈춤" + }, + "types": [ + { + "type_id": "pipe", + "group": "A", + "name": "배관", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#4f8ef7", + "abbr": "배수관" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "pipe_kind", + "label": "관종", + "input": "select", + "choices": [ + "흄관", + "VR관", + "파형강관" + ], + "default": "파형강관", + "required": false, + "enabled": false + }, + { + "key": "pipe_diameter_mm", + "label": "관경", + "input": "select", + "choices": [ + "800", + "1000", + "1200", + "1500" + ], + "unit": "mm", + "default": "1000", + "required": false + }, + { + "key": "inlet_type", + "label": "유입구 구조", + "input": "select", + "choices": [ + "기슭막이", + "집수정" + ], + "default": "기슭막이", + "required": false + }, + { + "key": "wing_wall_type", + "label": "관보호공 날개벽", + "input": "select", + "choices": [ + "A-TYPE", + "C-TYPE", + "A-TYPE+집수정" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 날개벽을 안 셈 — 고르면 그 형식의 개소당 원단위로 한 줄이 섬(Ø800·Ø1000 만 있음)" + }, + { + "key": "inlet_basin_form", + "label": "집수정 형식", + "input": "select", + "choices": [ + "동물이동형", + "□형(기본형)", + "돌집수정 ㄴ형", + "돌집수정 ㄷ형" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_material", + "label": "집수정 형태", + "input": "select", + "choices": [ + "콘크리트", + "조립식 주철맨홀", + "돌쌓기(찰)", + "돌쌓기(메)" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_length_m", + "label": "집수정 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_before_m", + "label": "집수정 기준측점 전", + "input": "number", + "unit": "m", + "default": 1, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_after_m", + "label": "집수정 기준측점 후", + "input": "number", + "unit": "m", + "default": 1, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_form", + "label": "유입 기슭막이 형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(찰)", + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_length_m", + "label": "유입 기슭막이 길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false + }, + { + "key": "inlet_revet_before_m", + "label": "유입 기슭막이 기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_after_m", + "label": "유입 기슭막이 기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_height_m", + "label": "유입 기슭막이 높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" + }, + { + "key": "outlet_type", + "label": "유출구 구조", + "input": "select", + "choices": [ + "기슭막이" + ], + "default": "기슭막이", + "required": false + }, + { + "key": "outlet_revet_form", + "label": "유출 기슭막이 형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(메)", + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_length_m", + "label": "유출 기슭막이 길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false + }, + { + "key": "outlet_revet_before_m", + "label": "유출 기슭막이 기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_after_m", + "label": "유출 기슭막이 기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_height_m", + "label": "유출 기슭막이 높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" + }, + { + "key": "revet_foundation", + "label": "기슭막이 기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": false, + "phase": "detail" + } + ] + }, + { + "type_id": "box_culvert", + "group": "A", + "name": "BOX암거", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#3b6fd4", + "abbr": "암거" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "body_width_m", + "label": "본체 폭", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "revet_foundation", + "label": "기슭막이 기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "body_height_m", + "label": "본체 높이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "length_m", + "label": "연장(계류 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 수량이 안 섬 — 구체 길이는 설계자 입력(기본값 없음 · 2026-09-15 브레인 판정 ③)" + }, + { + "key": "wall_thickness_m", + "label": "측벽 두께", + "input": "number", + "unit": "m", + "default": 0.3, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 벽 0.3 · KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "slab_thickness_m", + "label": "상·저판 두께", + "input": "number", + "unit": "m", + "default": 0.25, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 판 0.25 — ⚠ KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ 에 못 미침(임도 예제 0.25 · KDS 도로 최소 300㎜) · 설계자가 고름", + "required": false, + "phase": "detail" + }, + { + "key": "haunch_m", + "label": "헌치(모따기) 크기", + "input": "number", + "unit": "m", + "default": 0.2, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 0.2×0.2 · 실무 울진 기번3 구조도 2×2 도 0.2 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "blinding_thickness_m", + "label": "기초(버림) 두께", + "input": "number", + "unit": "m", + "default": 0.1, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 기초 0.1(양쪽 0.1 여유) · 실무 울진 기번3 구조도 2×2 버림 0.1 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "trench_depth_m", + "label": "평균 터파기고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 구체 터파기·되메우기가 안 섬 — 원지반에서 터파기 바닥(기초잡석 밑)까지 · 토공집계로 감(2026-09-15 브레인 판정 ③)" + }, + { + "key": "wing_in", + "label": "날개벽(유입)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_in_height_m", + "label": "유입 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_in_length_m", + "label": "유입 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_in_angle_deg", + "label": "유입 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + }, + { + "key": "wing_out", + "label": "날개벽(유출)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_out_height_m", + "label": "유출 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_out_length_m", + "label": "유출 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_out_angle_deg", + "label": "유출 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + } + ] + }, + { + "type_id": "ford_pavement", + "group": "A", + "name": "물넘이포장", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#2f9e8f", + "abbr": "물넘이" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "ford_width_m", + "label": "월류 폭", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail", + "default_basis": "물넘이 월류 폭 기본 5m — 사용자 확정값(엔진 FORD_PAVEMENT_DEFAULT_WIDTH_M 과 같음)" + }, + { + "key": "ford_slope_pct", + "label": "바닥 경사(유입→유출)", + "input": "number", + "unit": "%", + "default": null, + "required": false + }, + { + "key": "thickness_cm", + "label": "포장 두께", + "input": "number", + "unit": "㎝", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 표가 안 서고 「두께를 적어야 섬」 사유가 뜸 — 두께로 관측 원단위(울진 20㎝)를 고름(2026-09-14 A3)" + }, + { + "key": "length_m", + "label": "포장 길이(노폭 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 면적(월류 폭 × 길이)이 0 이라 표가 안 서고 사유가 뜸 — 노견까지 전폭을 적음(2026-09-14 A3)" + } + ] + }, + { + "type_id": "ford_bridge", + "group": "A", + "name": "세월교", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#28857a", + "abbr": "세월교" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "pipe_kind", + "label": "관종", + "input": "select", + "choices": [ + "흄관", + "VR관", + "파형강관" + ], + "default": "파형강관", + "required": false + }, + { + "key": "pipe_diameter_mm", + "label": "관경", + "input": "select", + "choices": [ + "800", + "1000", + "1200", + "1500" + ], + "unit": "mm", + "default": "1000", + "required": false + }, + { + "key": "pipe_count", + "label": "배관 수량", + "input": "number", + "unit": "련", + "default": null, + "required": false + }, + { + "key": "ford_width_m", + "label": "월류 폭", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "detail", + "default_basis": "세월교 월류 폭 기본 10m — 사용자 확정값(엔진 FORD_DEFAULT_WIDTH_M 과 같음)" + }, + { + "key": "wing_in", + "label": "날개벽(유입)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_in_height_m", + "label": "유입 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_in_length_m", + "label": "유입 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_in_angle_deg", + "label": "유입 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + }, + { + "key": "wing_out", + "label": "날개벽(유출)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_out_height_m", + "label": "유출 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_out_length_m", + "label": "유출 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_out_angle_deg", + "label": "유출 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + } + ] + }, + { + "type_id": "cross_drain_exposed", + "group": "A", + "name": "노출형 횡단수로", + "placement": "point", + "style": { + "color": "#5aa7e8", + "abbr": "횡수로" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "quantity" + ], + "options": [] + }, + { + "type_id": "open_ditch", + "group": "A", + "name": "개거(겉도랑)", + "placement": "point", + "style": { + "color": "#6d9bd1", + "abbr": "개거" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "ditch_spec", + "label": "규격", + "input": "select", + "choices": [ + "콘크리트 개거 150×200", + "L형수로 H=0.2" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 붙박이(콘크리트 개거)로 돎" + }, + { + "key": "length_m", + "label": "연장", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)" + } + ] + }, + { + "type_id": "ditch_side", + "enabled": false, + "group": "B", + "name": "측구(옆도랑)", + "design_owner": "횡단 설계", + "placement": "interval", + "style": { + "color": "#2eaadc", + "abbr": "측구" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "일반형(제형)", + "L형", + "U형", + "J형" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "depth_cm", + "label": "깊이", + "input": "number", + "unit": "cm", + "default": 30 + } + ] + }, + { + "type_id": "ditch_ridge", + "enabled": false, + "group": "B", + "name": "산마루측구", + "placement": "interval", + "style": { + "color": "#2e8bdc", + "abbr": "산마루" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "콘크리트", + "돌", + "떼" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "ditch_berm", + "enabled": false, + "group": "B", + "name": "소단측구", + "placement": "interval", + "style": { + "color": "#2ec5dc", + "abbr": "소단구" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "width_m", + "label": "폭", + "input": "number", + "unit": "m", + "default": 0.5, + "required": false, + "phase": "b05" + }, + { + "key": "interval_m", + "label": "간격(사면길이)", + "input": "number", + "unit": "m", + "default": 3, + "required": false, + "phase": "b05" + }, + { + "key": "slope_deg", + "label": "사면 쪽 기울기", + "input": "number", + "unit": "°", + "default": 0, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "chute", + "enabled": false, + "group": "B", + "name": "도수로·산비탈수로", + "placement": "interval", + "style": { + "color": "#1f9bb8", + "abbr": "도수로" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "떼", + "돌", + "콘크리트", + "블록판", + "벤치플륨관", + "반원관" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "section", + "label": "단면형", + "input": "select", + "choices": [ + "반달꼴", + "U자형", + "사다리꼴" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "slope_drain", + "enabled": false, + "group": "B", + "name": "절토사면 배수로", + "placement": "interval", + "style": { + "color": "#28b1a1", + "abbr": "사면로" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "떼", + "돌" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "underdrain", + "enabled": false, + "group": "B", + "name": "맹암거", + "placement": "interval", + "style": { + "color": "#1f7fb8", + "abbr": "맹암거" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "note", + "label": "규격", + "input": "text", + "default": "" + } + ] + }, + { + "type_id": "retaining_wall", + "group": "C", + "name": "옹벽", + "placement": "interval", + "style": { + "color": "#e07a2e", + "abbr": "옹벽" + }, + "drawing_views": [ + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2, + "default_basis": "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음", + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "중력식", + "반중력식", + "캔틸레버식", + "부벽식", + "식생옹벽블럭" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + } + ] + }, + { + "type_id": "masonry_wet", + "group": "C", + "name": "돌쌓기(찰)", + "placement": "interval", + "style": { + "color": "#d0692a", + "abbr": "찰쌓기" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이(≤3.0)", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "masonry_dry", + "group": "C", + "name": "돌쌓기(메)", + "placement": "interval", + "style": { + "color": "#c25d2c", + "abbr": "메쌓기" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이(≤2.0)", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)", + "warn_above": 2, + "warn_message": "메쌓기 높이 기준을 넘음 — 임도기술교본 7-3:27 「메쌓기는 2.0m 이하」 · 품셈 13-4-4 [주]⑪(원문 L7192) 「높이 3m 이상이면 전부 또는 하부를 찰쌓기」 · 막지 않음(설계자 확인)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "soil_guard", + "group": "C", + "name": "흙막이", + "placement": "interval", + "style": { + "color": "#b8742e", + "abbr": "흙막이" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "콘크리트", + "돌(찰)", + "돌(메)", + "블록쌓기", + "돌망태", + "흙포대", + "통나무쌓기", + "떼" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "tiers", + "label": "단 수(다단)", + "input": "number", + "unit": "단", + "default": 1, + "required": false, + "phase": "b05", + "default_basis": "1 = 다단 없음 — 도메인 수치가 아니라 「단이 하나」라는 표식(기슭막이와 같은 벌)" + }, + { + "key": "lift_m", + "label": "기준 올림(사면 위로)", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로 — 올리지 않음을 뜻함(기슭막이와 같은 벌)" + }, + { + "key": "shift_m", + "label": "기준 좌우 이동", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로 — 옮기지 않음을 뜻함(기슭막이와 같은 벌)" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 돌쌓기 기본값으로 서고 「기본값으로 섰음」이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "default_basis": "「채집」 — 별표2 「야면석 등은 가급적 현장에서 채취·사용」 권고. 구조물마다 바꿀 수 있음", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + } + ] + }, + { + "type_id": "boulder_masonry", + "group": "C", + "name": "큰돌쌓기", + "placement": "interval", + "style": { + "color": "#a86a2a", + "abbr": "큰돌" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "stone_cm", + "label": "돌규격", + "input": "select", + "choices": [ + "40~60", + "60~80", + "80~100" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "bond", + "label": "쌓기 방식", + "input": "select", + "choices": [ + "메쌓기", + "찰쌓기" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 채움 콘크리트를 품에 포함 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "berm", + "group": "C", + "name": "소단", + "placement": "interval", + "style": { + "color": "#7f9a63", + "abbr": "소단" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "width_m", + "label": "폭", + "input": "number", + "unit": "m", + "default": 0.5, + "required": false, + "phase": "b05" + }, + { + "key": "interval_m", + "label": "간격(사면길이)", + "input": "number", + "unit": "m", + "default": 3, + "required": false, + "phase": "b05" + }, + { + "key": "slope_deg", + "label": "안쪽 기울기", + "input": "number", + "unit": "°", + "default": 0, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "erosion_check", + "group": "D", + "name": "골막이", + "placement": "point", + "style": { + "color": "#7a52c7", + "abbr": "골막이" + }, + "drawing_views": [ + "plan", + "profile", + "detail", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "돌", + "돌망태", + "콘크리트", + "통나무", + "바자", + "흙" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "spillway", + "label": "방수로", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "back_len_cm", + "label": "뒷길이 ℓ3", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 골막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 골막이는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "top_length_m", + "label": "상장 ⓐ (윗변)", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "bottom_length_m", + "label": "하장 ⓑ (아랫변)", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 4.5 + }, + { + "key": "height_m", + "label": "높이(≤2.0)", + "input": "number", + "unit": "m", + "default": 2 + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + } + ] + }, + { + "type_id": "bed_sill", + "group": "D", + "name": "바닥막이", + "placement": "point", + "style": { + "color": "#7c5f3b", + "abbr": "바닥" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형태", + "input": "select", + "choices": [ + "돌붙임(찰)", + "돌붙임(메)" + ], + "default": "돌붙임(메)", + "required": false, + "phase": "b05" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이는 평면적(돌붙임 ㎡)이라 기울기 몫이 없음 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 0.3, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "check_dam_small", + "group": "D", + "name": "소형사방댐(복합형)", + "placement": "point", + "style": { + "color": "#6b46b8", + "abbr": "사방댐" + }, + "drawing_views": [ + "plan", + "profile", + "detail", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "revetment", + "group": "D", + "name": "기슭막이", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#9b6bdc", + "abbr": "기슭" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "양쪽", + "좌", + "우" + ], + "default": "양쪽", + "required": false, + "phase": "b05", + "default_basis": "「좌」는 셀렉트 첫 항목일 뿐 도메인 확정값이 아님 — 측점마다 사용자가 고름" + }, + { + "key": "tiers", + "label": "단 수(다단)", + "input": "number", + "unit": "단", + "default": 1, + "required": false, + "phase": "b05", + "default_basis": "1 = 다단 없음 — 도메인 수치가 아니라 「단이 하나」라는 표식" + }, + { + "key": "lift_m", + "label": "기준 올림(사면 위로)", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로" + }, + { + "key": "shift_m", + "label": "기준 좌우 이동", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로" + }, + { + "key": "form", + "label": "형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(메)", + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 13-4-4 [주]⑩ 뒷길이 표준 하한(높이·찰/메로 · 범위 안 가장 작은 규격 — 예 메쌓기 1.5~3m 는 36~45㎝ 라 45㎝)으로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "b05", + "empty_means": "설계자 입력 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181 · 사방(계류) 교본 기준). 비우면 줄만 서고 미확정이라 금액에 안 들어감 · 옛 기본 2.5 는 근거 없어 걷음(2026-09-14 브레인 판정)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "refuge", + "group": "E", + "name": "대피소", + "placement": "interval", + "style": { + "color": "#2fa84f", + "abbr": "대피소" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "length_m", + "label": "유효길이", + "input": "number", + "unit": "m", + "default": 15 + } + ] + }, + { + "type_id": "work_yard", + "group": "E", + "name": "정차·작업장", + "placement": "interval", + "style": { + "color": "#268f43", + "abbr": "작업장" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": 7 + }, + { + "key": "length_m", + "label": "유효길이", + "input": "number", + "unit": "m", + "default": 20 + } + ] + }, + { + "type_id": "turnaround", + "group": "E", + "name": "차돌림곳", + "placement": "site", + "style": { + "color": "#37b95a", + "abbr": "차돌림" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": 10 + } + ] + }, + { + "type_id": "mirror", + "group": "E", + "name": "반사경", + "placement": "point", + "style": { + "color": "#43c76b", + "abbr": "반사경" + }, + "drawing_views": [ + "plan" + ], + "options": [ + { + "key": "facing", + "label": "방향", + "input": "select", + "choices": [ + "좌", + "우", + "양면" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "guardrail", + "group": "E", + "name": "가드레일·경계석·위험표지", + "placement": "interval", + "style": { + "color": "#1f9e46", + "abbr": "가드" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "kind", + "label": "종류", + "input": "select", + "choices": [ + "가드레일", + "경계석", + "위험표지" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "position_sign", + "group": "E", + "name": "국가지점번호판", + "placement": "point", + "style": { + "color": "#53a832", + "abbr": "번호판" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "barrier_gate", + "group": "E", + "name": "차단기", + "placement": "point", + "style": { + "color": "#3f9e2e", + "abbr": "차단기" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "completion_sign", + "group": "E", + "name": "준공표지판", + "placement": "point", + "style": { + "color": "#66b344", + "abbr": "준공" + }, + "drawing_views": [ + "plan" + ], + "options": [] + }, + { + "type_id": "spoil_bank", + "group": "E", + "name": "유용토운반작업장(구 사토장)", + "placement": "interval", + "style": { + "color": "#8c9a2e", + "abbr": "사토장" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "side", + "label": "쌓는 쪽", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "required": false, + "phase": "b05", + "default_basis": "「자동(성토 쪽)」 — 흙은 성토 쪽에 쌓는다는 지금까지의 동작을 이어받는 표식. 측점마다 좌·우로 바꿈" + }, + { + "key": "fill_slope_ratio", + "label": "비탈 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 그 측점의 노선 성토 기울기를 그대로 씀 — 지식DB 가 「기본 비탈기울기는 근거에 없다」고 못 박은 자리" + }, + { + "key": "extra_distance_m", + "label": "추가 운반거리", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "detail", + "default_basis": "0 = 「추가 없음」 — 노선 안 사토장은 측점 누가거리로 거리가 나오므로 이 칸은 외부 사토장 대비 여벌" + }, + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "borrow_pit", + "group": "E", + "name": "토사채취장(토취장)", + "placement": "site", + "style": { + "color": "#7d8a2a", + "abbr": "토취장" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "wildlife_path", + "enabled": false, + "group": "F", + "name": "야생동물 이동통로", + "placement": "point", + "style": { + "color": "#3aa88f", + "abbr": "동물길" + }, + "drawing_views": [ + "plan", + "cross_section" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "경사로", + "자연형계단" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "revegetation", + "enabled": false, + "group": "F", + "name": "비탈면 녹화", + "placement": "interval", + "style": { + "color": "#4cb84c", + "abbr": "녹화" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "method", + "label": "공법", + "input": "select", + "choices": [ + "씨뿌리기(줄파종)", + "새심기", + "선떼붙이기", + "줄떼", + "평떼", + "비탈덮기", + "녹생토 뿜어붙이기", + "조공", + "편책", + "나무심기" + ], + "default": null, + "default_basis": "「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)", + "required": true, + "phase": "detail" + }, + { + "key": "face", + "label": "대상 면", + "input": "select", + "choices": [ + "절토면", + "성토면", + "양쪽" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "cover_kind", + "label": "덮개(비탈덮기)", + "input": "select", + "choices": [ + "거적", + "짚망", + "방초·야자섬유매트" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비탈덮기일 때만 고름 — 비면 비탈덮기 줄이 안 섬(품셈 5-25 거적덮기 ㎡ · 5-28-1 짚망 · 5-28-2 매트 · 재료가 공종을 가름)" + }, + { + "key": "step_height_m", + "label": "단 직고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음 · 비면 선떼 연장(품셈 5-16-1 [주]② 대상지 직고 ÷ 단 직고 × 연장)이 안 섬" + }, + { + "key": "sod_grade", + "label": "선떼 급수", + "input": "select", + "choices": [ + "1급", + "2급", + "3급", + "4급", + "5급", + "6급", + "7급", + "8급", + "9급" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음 · 비면 m당 떼 매수(사방기술교본 표 2-2-6)가 안 섬" + }, + { + "key": "sod_size", + "label": "선떼 떼 크기", + "input": "select", + "choices": [ + "대형 40×40", + "소형 18×18" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "사방기술교본 표 2-2-6 이 대형(40×40)·소형(18×18) 두 줄을 나란히 줌 — 우리가 고를 자리가 아니라 기본값 없음 · 비면 선떼 떼 매수가 안 섬" + }, + { + "key": "cutting_soil", + "label": "단끊기 토질", + "input": "select", + "choices": [ + "보통토사", + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "품셈 5-16-1 단끊기 표의 토질 셋 — B06 지반(토사/암)과 다른 축이라 자동으로 안 고름 · 비면 단끊기 조각만 막힘" + } + ] + }, + { + "type_id": "pavement_concrete", + "group": "G", + "name": "콘크리트 포장", + "placement": "interval", + "style": { + "color": "#8a8f98", + "abbr": "포장" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10 + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 포장층 두께(표준 횡단면 설정 · 기본 0.2m = 임도기술교본 3-2 「두께 0.2m」)를 이음 — 적으면 적은 값이 이김" + }, + { + "key": "width_m", + "label": "포장 폭", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 노폭(구간 측점의 차도 표준 폭 · 교본 3-2 「포장 폭 3.0m」 · 실무 관측 3.5~4.0)을 이음 — 적으면 적은 값이 이김" + }, + { + "key": "widening_area_m2", + "label": "확폭 면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 곡선부 확폭을 측점 사이 평균 × 거리로 더함(교본 3-2 「곡선부에는 확폭」) — 확폭이 없으면 0 을 적을 것" + }, + { + "key": "joint_spacing_m", + "label": "수축줄눈 간격", + "input": "number", + "unit": "m", + "default": 6, + "default_basis": "제안값 · 임도기술교본 부록 4-7 7-1 3.5.1 「수축줄눈의 간격은 4-6m를 기준」 · 실무 소광 6m 관측 · 바꿀 수 있음", + "phase": "detail" + }, + { + "key": "separation_sheet", + "label": "비닐(깔기·양생)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 비닐 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「양생 재료비(비닐 등)는 별도 계상」이라 설계자가 고름(기본값 없음)" + }, + { + "key": "wire_mesh", + "label": "철망", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 철망 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「철망재료비는 별도 계상」 · 교본 부록 7-1 3.4 「설계도서에 따라」 · 실무 소광 「철망포함」 관측" + } + ] + }, + { + "type_id": "gravel_surfacing", + "enabled": false, + "group": "G", + "name": "쇄석(자갈) 포설", + "placement": "interval", + "style": { + "color": "#9aa0a8", + "abbr": "쇄석" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "etc", + "group": "호환", + "name": "기타", + "placement": "point", + "style": { + "color": "#98a2b3", + "abbr": "기타" + }, + "drawing_views": [ + "profile" + ], + "options": [ + { + "key": "name", + "label": "이름", + "input": "text", + "default": "기타 구조물" + } + ] + } + ], + "종류": { + "pipe": { + "동작": [ + "계곡통과시설", + "유입유출_묶음", + "기슭막이_한벌" + ] + }, + "box_culvert": { + "동작": [ + "계곡통과시설", + "BOX_본체규격", + "날개벽" + ] + }, + "ford_pavement": { + "동작": [ + "계곡통과시설", + "월류단면" + ] + }, + "ford_bridge": { + "동작": [ + "계곡통과시설", + "월류단면", + "날개벽" + ] + }, + "revetment": { + "동작": [ + "계곡통과시설", + "기슭막이_한벌" + ] + }, + "pavement_concrete": { + "동작": [ + "포장_물넘이_겹침" + ] + } + }, + "표시": { + "pipe": { + "pipe_kind": false + } + }, + "그룹": { + "pipe": [ + { + "id": "grv6lny", + "이름": "관", + "옵션": [ + "pipe_kind", + "pipe_diameter_mm" + ], + "형상": {} + }, + { + "id": "gnbf9m9", + "이름": "관 보호공(유입)", + "옵션": [ + "inlet_type" + ], + "형상": {} + }, + { + "id": "g9lq9uk", + "이름": "관 보호공(유출)", + "옵션": [], + "형상": {} + } + ] + }, + "배치": {}, + "규칙": [] +} diff --git a/resources/tester/helper_52_structure_spec.cjs b/resources/tester/helper_52_structure_spec.cjs new file mode 100644 index 000000000..0ff82723b --- /dev/null +++ b/resources/tester/helper_52_structure_spec.cjs @@ -0,0 +1,87 @@ +/* 구조물 명세 공용 틀 · 배관 명세 단위검증 헬퍼(PLAN 52-1) — `design_structure_spec.ts` · + * `design_structure_pipe.ts` 를 그 자리에서 트랜스파일해 Node 로 돌림(꼴만 import). 결과 = JSON 한 벌. + * 인자 `names` 면 배관 이름 표(정의 `종류.pipe.이름`)만 냄. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const load = (file, deps = {}) => { + const src = fs.readFileSync(path.join(ROOT, "A00_Common", "design", file), "utf8"); + const js = ts.transpileModule(src, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? require(m)); + return box.exports; +}; +const s = load("design_structure_spec.ts"); +const { PIPE_SPEC } = load("design_structure_pipe.ts"); + +if (process.argv[2] === "names") { + process.stdout.write(JSON.stringify(s.nameTable(PIPE_SPEC))); + process.exit(0); +} + +const old = { + pipe_kind: "흄관", + wing_wall_type: "A-TYPE", + pipe_diameter_mm: 1000, + inlet_type: "집수정", + inlet_structure: "L", + inlet_basin_length_m: 2, + inlet_basin_before_m: 1, + inlet_basin_after_m: 1, + outlet_type: "기슭막이", + outlet_revet_form: "돌쌓기(메)", + outlet_revet_length_m: 10, + outlet_revet_height_m: 2.5, + soil_add_2_structure: "기슭막이", + soil_add_2_form: "바자", + soil_add_5_length_m: 4, + chute_add_1_structure: "도수로", +}; +const state = s.readState(PIPE_SPEC, old); +const back = s.writeOptions(PIPE_SPEC, state); +// 흙막이로 바꿈 → 갈래 키가 기슭막이 쪽 · also(inlet_structure revet) +const soil = JSON.parse(JSON.stringify(state)); +soil.inlet[0].structure = "기슭막이"; +soil.inlet[0].form = "콘크리트"; +soil.inlet[0].length = "6"; +const soilOut = s.writeOptions(PIPE_SPEC, soil); +const inlet = PIPE_SPEC.containers.find((c) => c.id === "inlet"); +const form = inlet.items.find((i) => i.id === "form"); +const keys = s.specKeys(PIPE_SPEC); + +const out = { + containers: PIPE_SPEC.containers.map((c) => [c.id, c.title, c.required]), + state, + back, + soilOut, + formBasin: s.choicesOf(form, { structure: "집수정" }).map((c) => c.label), + formSoil: s.choicesOf(form, { structure: "기슭막이" }).map((c) => c.label), + formNone: s.branchOf(form, {}), + outletForms: s + .choicesOf( + PIPE_SPEC.containers.find((c) => c.id === "outlet").items.find((i) => i.id === "form"), + { structure: "기슭막이" }, + ) + .map((c) => c.label), + keys, + links: PIPE_SPEC.containers.map((c) => s.linkOf(c, 2)), + split: { + lengthFromSides: s.solveSplit({ length: "10", before: "3", after: "4" }, ["after", "before"]), + beforeFromLength: s.solveSplit({ length: "10", before: "3", after: "4" }, ["after", "length"]), + defaultKeepsLength: s.solveSplit({ length: "10", before: "3", after: "" }, ["before"]), + blankKept: s.solveSplit({ length: "", before: "3", after: "4" }, ["before", "length"]), + clamp: s.solveSplit({ length: "2", before: "", after: "5" }, ["after", "length"]), + }, + names: { + renamed: s.itemLabel(inlet, form, { "inlet.form": "모양" }), + title: s.containerTitle(inlet, {}, "그룹 이름"), + titleOwn: s.containerTitle(inlet, { inlet: "유입 보호" }, "그룹 이름"), + choice: s.choiceLabel(inlet, form, { value: "U", label: "ㄷ형" }, {}), + }, +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_52_structure_spec_pipe.py b/resources/tester/test_52_structure_spec_pipe.py new file mode 100644 index 000000000..3f9abdf5c --- /dev/null +++ b/resources/tester/test_52_structure_spec_pipe.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""구조물 명세 공용 틀 + 횡단배수 › 배관(표 01) — PLAN 52-1. + +순수 규칙은 Node 헬퍼(`helper_52_structure_spec.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-1_배관_컨테이너.js`. +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) + + +def _node(*args): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_52_structure_spec.cjs"), *args], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +@pytest.fixture(scope="module") +def out(): + return _node() + + +def test_컨테이너_다섯_필수_셋_선택_둘(out): + assert out["containers"] == [ + ["pipe", "관", True], + ["inlet", "관 보호공 (유입)", True], + ["outlet", "관 보호공 (유출)", True], + ["soil_add", "흙막이 추가", False], + ["chute_add", "도수로/돌붙임", False], + ] + + +def test_형태_선택지는_구조를_따름(out): + assert out["formBasin"] == ["I형", "L형", "ㄷ형"] + assert out["formSoil"] == [ + "돌 쌓기(찰)", + "돌 쌓기(메)", + "콘크리트", + "돌망태", + "통나무/목재틀", + "바자", + ] + assert out["formNone"] is None # 구조 안 고름 = 형태 칸 가림 + assert out["outletForms"] == out["formSoil"] + + +def test_옛_키로_읽고_같은_키로_씀_옛_칸은_안_씀(out): + back = out["back"] + assert back["pipe_diameter_mm"] == 1000 + assert back["inlet_structure"] == "L" and back["inlet_basin_length_m"] == 2 + assert back["outlet_revet_height_m"] == 2.5 + # 표 01 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값) + assert "pipe_kind" not in back and "wing_wall_type" not in back + + +def test_선택_컨테이너는_인스턴스마다_차례대로_다시_매김(out): + assert [i["structure"] for i in out["state"]["soil_add"]] == ["기슭막이", ""] + assert out["back"]["soil_add_1_form"] == "바자" + assert "soil_add_2_form" not in out["back"] + assert out["back"]["chute_add_1_structure"] == "도수로" + + +def test_흙막이로_바꾸면_기슭막이_키와_revet(out): + s = out["soilOut"] + assert s["inlet_type"] == "기슭막이" and s["inlet_structure"] == "revet" + assert s["inlet_revet_form"] == "콘크리트" and s["inlet_revet_length_m"] == 6 + assert "inlet_basin_length_m" not in s + + +def test_길이_전_후_맞물림_마지막에_고친_둘을_지킴(out): + sp = out["split"] + assert sp["lengthFromSides"]["length"] == "7" + assert sp["beforeFromLength"]["before"] == "6" + assert sp["defaultKeepsLength"]["after"] == "7" + assert sp["blankKept"]["length"] == "" # 지킬 칸이 비면 안 셈 + assert sp["clamp"]["before"] == "0" + + +def test_잇기_키와_이름_바꿈(out): + assert out["links"] == ["grv6lny", "gnbf9m9", "g9lq9uk", "gsoiladd2", "gchuteadd2"] + assert out["names"] == { + "renamed": "모양", + "title": "그룹 이름", + "titleOwn": "유입 보호", + "choice": "ㄷ형", + } + assert {"soil_add_", "chute_add_"} == set(out["keys"]["prefixes"]) + + +def test_정의_파일에_이름_표가_올라_있음(): + names = _node("names") + with open( + os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8" + ) as f: + doc = json.load(f) + on_file = doc["종류"]["pipe"]["이름"] + assert set(names) == set(on_file) + assert on_file["inlet.structure=기슭막이"] == "흙막이" diff --git a/resources/tester/test_b05_facility_options_merge.py b/resources/tester/test_b05_facility_options_merge.py index 6b8880720..3370decec 100644 --- a/resources/tester/test_b05_facility_options_merge.py +++ b/resources/tester/test_b05_facility_options_merge.py @@ -96,7 +96,8 @@ def test_폼이_모르는_칸은_남고_폼이_아는_칸은_폼대로(merged) - assert options["revet_foundation"] == "기초유" # 집계표 값이 살아남음 assert "inlet_basin_before_m" not in options # 폼 칸(24-4a) — 집수정을 걷으면 함께 지움 assert options["pipe_diameter_mm"] == 800 # 폼 값이 이김 - assert "inlet_basin_form" not in options # 폼이 아는 칸을 폼이 비움 → 지움 + # 표 01 명세(PLAN 52-1)가 뺀 옛 칸 = 폼이 모르는 칸 → 남음(지난 정의 값) + assert options["inlet_basin_form"] == "□형(기본형)" assert merged["out"][1]["options"] == {"side": "좌", "back_len_cm": "35"}