diff --git a/A00_Common/design/design_structure_ford.ts b/A00_Common/design/design_structure_ford.ts new file mode 100644 index 000000000..55bb897e0 --- /dev/null +++ b/A00_Common/design/design_structure_ford.ts @@ -0,0 +1,88 @@ +/* ============================================================================= + * design_structure_ford.ts + * 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) 그대로. + * + * 컨테이너 셋: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈. + * 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 새 키 = 유입 · 유출 구조 + * (`inlet_type` · `outlet_type` = 없음 · 날개벽 · 흙막이) · 고르면 옛 날개벽 설치 칸(`wing_in` · `wing_out`)도 함께 적음. + * 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래로는 안 받음) — 계산은 `design_structure_ford_calc`. + * import 없음 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { SpecContainer, SpecChoice, StructureSpec } from "./design_structure_spec"; + +/** 유입 · 유출 구조 — 날개벽만 옛 설치 칸이 「있음」. */ +const guard = (side: "in" | "out"): SpecChoice[] => [ + { value: "없음", label: "없음", also: { [`wing_${side}`]: "없음" } }, + { value: "날개벽", label: "날개벽", also: { [`wing_${side}`]: "있음" } }, + { value: "흙막이", label: "흙막이", also: { [`wing_${side}`]: "없음" } }, +]; + +const guardContainer = (id: "inlet" | "outlet", title: string, link: string): SpecContainer => ({ + id, + title, + required: true, + link, + items: [ + { + id: "structure", + label: "구조", + kind: "select", + key: `${id}_type`, + choices: guard(id === "inlet" ? "in" : "out"), + }, + ], +}); + +export const FORD_SPEC: StructureSpec = { + typeId: "ford_bridge", + containers: [ + { + id: "ford", + title: "세월교", + required: true, + link: "gford", + items: [ + { + id: "diameter", + label: "관경", + kind: "select", + key: "pipe_diameter_mm", + asNumber: true, + choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })), + }, + { id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 }, + { id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 }, + { id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 }, + ], + }, + guardContainer("inlet", "유입 구조", "gfordin"), + guardContainer("outlet", "유출 구조", "gfordout"), + ], + // 표 04 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값) + retired: [ + "pipe_kind", + "wing_in_height_m", + "wing_in_length_m", + "wing_in_angle_deg", + "wing_out_height_m", + "wing_out_length_m", + "wing_out_angle_deg", + ], +}; + +/** 구조를 날개벽 설치 칸(옛 저장 · B06 조정창이 고치는 칸)에 맞춤 — 있음 = 날개벽 · 없음 = 날개벽이면 없음 + * (흙막이는 그대로) · 설치 칸이 없으면 구조 그대로. */ +export function withLegacyGuards( + options: Record, +): Record { + const out = { ...options }; + for (const [type, wing] of [ + ["inlet_type", "wing_in"], + ["outlet_type", "wing_out"], + ]) { + if (out[wing] === "있음") out[type] = "날개벽"; + else if (out[wing] === "없음" && (out[type] ?? "날개벽") === "날개벽") out[type] = "없음"; + } + return out; +} diff --git a/A00_Common/design/design_structure_ford_calc.ts b/A00_Common/design/design_structure_ford_calc.ts new file mode 100644 index 000000000..a9ec865a0 --- /dev/null +++ b/A00_Common/design/design_structure_ford_calc.ts @@ -0,0 +1,27 @@ +/* ============================================================================= + * design_structure_ford_calc.ts + * 세월교 명세 계산 — 월류 폭 ↔ 월류 높이 연동(PLAN 52-4). + * + * 높이 = 담당 유역 설계유량을 그 월류 폭으로 넘기는 필요 수심(Manning 개수로 · `fordSection` 그대로). + * 근거: Manning 공식 `resources/knowledge/technical_info/01_임도/03_계산정보/통수단면.md:23~33` + * (KDS 44 40 00 2.7.1~2.7.2) · 조도계수 0.017 = 같은 파일 :59(콘크리트 수로 보통 · 울진 물넘이) · + * 경사 10 % = 실무 수리계산서 역산값(`config_frontend.FORD_SLOPE` · 지식DB 수치 없음). + * 폭을 바꾸면 높이 칸을 이 값으로 채움 · 사용자는 올려 적을 수 있고 이 값 아래로는 안 받음. + * 높이는 저장되는 입력값 — 서버는 받은 높이로 노면을 내림(`B06_Section_Engine_Culvert._ford_set`) · 재계산 안 함. + * 설계유량이 없으면 null = 손 입력. + * ========================================================================== */ + +import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields"; + +/** 월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림. 유량 · 폭이 없으면 null. */ +export function fordHeightM(designFlowM3s: number | null, widthM: number): number | null { + const section = fordSection(designFlowM3s, widthM); + return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null; +} + +/** 높이 빈 칸 안내. */ +export function fordHeightHint(designFlowM3s: number | null, widthM: number): string { + const need = fordHeightM(designFlowM3s, widthM); + if (need !== null) return `필요 수심 ${need.toFixed(2)} 이상`; + return designFlowM3s ? "월류 폭을 넣으면 셈" : "설계유량 없음 — 손 입력"; +} diff --git a/A00_Common/design/design_structure_pipe.ts b/A00_Common/design/design_structure_pipe.ts index c72f84c0b..ffc019894 100644 --- a/A00_Common/design/design_structure_pipe.ts +++ b/A00_Common/design/design_structure_pipe.ts @@ -8,10 +8,11 @@ * (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`). * 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림. * 계산(관 길이 · 높이 안내)은 `design_structure_pipe_calc`. - * import 없음 — 시험이 이 파일만 옮겨 돌림. + * import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림. * ========================================================================== */ import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec"; +import { FORD_SPEC } from "./design_structure_ford"; const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이 const BASIN = "집수정"; @@ -184,4 +185,7 @@ export const PIPE_SPEC: StructureSpec = { }; /** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */ -export const STRUCTURE_SPECS: Readonly> = { pipe: PIPE_SPEC }; +export const STRUCTURE_SPECS: Readonly> = { + pipe: PIPE_SPEC, + ford_bridge: FORD_SPEC, +}; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index e7371d120..3b540ee46 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -53,6 +53,8 @@ import { } from "./B05_Profile_UI_Drainage_Facility_Fields"; import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form"; import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe"; +import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford"; +import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc"; import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc"; import { hasLinks, @@ -250,45 +252,31 @@ export function createFacilityOptionsForm( root.className = "b05-drainage__facility"; root.hidden = true; - // ── 관종 · 관경 한 행 — 세월교 몫(배관은 아래 명세 폼 · 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)), - ); - const pipeRow = grid( - labeled("관종", pipeMaterial, "pipe_kind"), - labeled("관경", pipeDiameter, "pipe_diameter_mm"), - ); - // ── 배관 = 사용자 구조물 표 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), + const specLinks: SpecLinks = { + get: () => linkSource?.get() ?? {}, + pick: (key) => linkSource?.pick(key), + clear: (key) => linkSource?.clear(key), + set: (key, item) => linkSource?.set(key, item), + }; + const pipeForm = createSpecForm(specLinks, { + auto: (_c, item) => { + if (item.id !== "length") return ""; + const length = pipeLengthM(chainage); + return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2); }, - { - 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(); - }, + 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"); @@ -301,6 +289,26 @@ export function createFacilityOptionsForm( : "auto"; }; + // ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4) — 세월교 · 유입 구조 · 유출 구조 ── + // 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`) + let fordFlow: number | null = null; + const fordNeed = (): number | null => + fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? "")); + const fordForm = createSpecForm(specLinks, { + hint: (c, item, state) => + c.id === "ford" && item.id === "height" + ? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? "")) + : "", + onChange: (c, item) => { + const need = c.id === "ford" ? fordNeed() : null; + const height = fordForm.state().ford?.[0]?.height ?? ""; + const low = height !== "" && !(Number(height) >= (need ?? 0)); + if (need !== null && (item.id === "width" || (item.id === "height" && low))) + fordForm.setValue("ford", "height", need.toFixed(2)); + emit(); + }, + }); + // ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자 // 지시 4). 유입구·유출구와 같은 양식이되, 값은 조정창 사본의 행이 그대로 옮겨 온다 // (다단 벽은 폼에 자기 칸이 없다 — 높이·형태·구간값이 전부 조정 채널에 있다). @@ -370,6 +378,8 @@ export function createFacilityOptionsForm( ); const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS); const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS); + const wingInHome = wingInFields.slot.parentElement!; + const wingOutHome = wingOutFields.slot.parentElement!; const hiddenOptions = createHiddenOptions(root); @@ -399,8 +409,8 @@ export function createFacilityOptionsForm( // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( saveNote, - pipeRow, pipeForm.root, + fordForm.root, ford.widthRow, ford.slopeRow, ford.countRow, @@ -421,15 +431,18 @@ export function createFacilityOptionsForm( if (current === null) return; const isPipe = current === "pipe"; const isBox = current === "box_culvert"; - pipeRow.hidden = current !== "ford_bridge"; + const isBridge = current === "ford_bridge"; pipeForm.root.hidden = !isPipe; + fordForm.root.hidden = !isBridge; boxWrap.hidden = !isBox; - // 날개벽은 BOX암거와 세월교가 같은 옵션 한 벌을 쓴다(2026-08-25 사용자 확정). - const hasWing = isBox || current === "ford_bridge"; + // 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로 + const hasWing = isBox; wingInFields.root.hidden = !hasWing; wingOutFields.root.hidden = !hasWing; - const isFord = current === "ford_pavement" || current === "ford_bridge"; - ford.countRow.hidden = current !== "ford_bridge"; + (isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot); + (isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot); + const isFord = current === "ford_pavement"; + ford.countRow.hidden = true; ford.widthRow.hidden = !isFord; // 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다. ford.slopeRow.hidden = current !== "ford_pavement"; @@ -462,8 +475,6 @@ export function createFacilityOptionsForm( revetOutlet.fields.onChange(emit); revetSide.addEventListener("change", emit); [ - pipeMaterial, - pipeDiameter, boxSize, boxWidth, boxHeight, @@ -500,8 +511,12 @@ export function createFacilityOptionsForm( bindLinks(links) { linkSource = links; pipeForm.paintLinks(); + fordForm.paintLinks(); + }, + paintLinks: () => { + pipeForm.paintLinks(); + fordForm.paintLinks(); }, - paintLinks: () => pipeForm.paintLinks(), setChainage(chainageM) { chainage = chainageM; if (current === "pipe") pipeForm.refresh(); @@ -516,6 +531,7 @@ export function createFacilityOptionsForm( current = facility; hiddenOptions.remember(options); ford.setDesignFlow(designFlow ?? null); + fordFlow = designFlow ?? null; if (facility === null) { syncVisibility(); return; @@ -523,12 +539,11 @@ export function createFacilityOptionsForm( const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; // 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). - pipeMaterial.value = text("pipe_kind"); - pipeDiameter.value = text("pipe_diameter_mm"); - if (facility === "pipe") { - const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); + const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); + if (facility === "pipe") pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups); - } + if (facility === "ford_bridge") + fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups); // 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다. // 값이 없으면 「안 정함」. const savedWidth = Number.parseFloat(text("body_width_m")); @@ -549,7 +564,7 @@ export function createFacilityOptionsForm( boxHeight.value = hasSaved ? String(savedHeight) : ""; wingInFields.write(options); wingOutFields.write(options); - if (facility === "ford_pavement" || facility === "ford_bridge") ford.write(facility, options); + if (facility === "ford_pavement") ford.write(facility, options); revetSide.value = text("side"); const spread = legacyRevetOptions(options); revetInlet.fields.write(spread); @@ -558,15 +573,13 @@ export function createFacilityOptionsForm( }, setDesignFlow(designFlow) { ford.setDesignFlow(designFlow ?? null); + fordFlow = designFlow ?? null; + if (current === "ford_bridge") fordForm.refresh(); // 높이 안내 syncVisibility(); }, readOptions() { const options: Record = {}; // 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈). - const putPipe = (): void => { - if (pipeDiameter.value) options.pipe_diameter_mm = Number(pipeDiameter.value); - if (pipeMaterial.value) options.pipe_kind = pipeMaterial.value; - }; if (current === "pipe") { Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1) } else if (current === "box_culvert") { @@ -578,10 +591,7 @@ export function createFacilityOptionsForm( } else if (current === "ford_pavement") { ford.read(current, options); } else if (current === "ford_bridge") { - putPipe(); - ford.read(current, options); - wingInFields.read(options); - wingOutFields.read(options); + Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4) } else if (current === "revetment") { if (revetSide.value) options.side = revetSide.value; // 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고 diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts index 4e6afd657..51b56df66 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts @@ -42,14 +42,16 @@ export const FORM_OPTION_KEYS: Readonly> = { ], box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")], ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"], + // 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임 ford_bridge: [ - "pipe_kind", "pipe_diameter_mm", "pipe_count", "ford_width_m", "ford_height_m", - ...wingKeys("in"), - ...wingKeys("out"), + "inlet_type", + "outlet_type", + "wing_in", + "wing_out", ], revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")], }; diff --git a/B05_Profile/B05_Profile_UI_Profile_Structures.ts b/B05_Profile/B05_Profile_UI_Profile_Structures.ts index c22646ad4..4eb2380a8 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Structures.ts @@ -17,6 +17,7 @@ import { } from "./B05_Profile_UI_IrregularStations"; import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures"; import { GROUP_LABELS } from "./B05_Profile_UI_Structures_Panel"; +import { HIDDEN_PICK_GROUPS } from "./B05_Profile_UI_Structures_Panel_Types"; /** 우클릭 메뉴용 최소 타입 정보 — 사이드 「구조물 배치」 종류 목록과 같은 원천. */ export interface StructureMenuType { @@ -32,7 +33,9 @@ export function structureGroupMenuItems( types: ReadonlyArray, onPick: (typeId: string) => void, ): MapContextMenuItem[] { - const groups = [...new Set(types.map((type) => type.group))]; + const groups = [...new Set(types.map((type) => type.group))].filter( + (group) => !HIDDEN_PICK_GROUPS.has(group), + ); return groups.map((group) => ({ label: GROUP_LABELS[group] ?? group, children: types diff --git a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts index e3e44d790..7f321904a 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts @@ -78,7 +78,9 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition root .querySelectorAll(".is-rule-hidden") .forEach((node) => node.classList.remove("is-rule-hidden")); - root.querySelectorAll("option[hidden]").forEach((o) => (o.hidden = false)); + root + .querySelectorAll("option[hidden]:not([data-pick-hidden])") + .forEach((o) => (o.hidden = false)); paint(); }, }; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index fc4d6de2a..2a4adbb4f 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -24,7 +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, + hidesTypeSummary, paintStale, renderOptionFields as renderOptionFieldsInto, } from "./B05_Profile_UI_Structures_Panel_Options"; @@ -54,6 +54,7 @@ export type { PipeFacilityItem, StructuresSection } from "./B05_Profile_UI_Struc import { DEFAULT_INTERVAL_LENGTH_M, GROUP_LABELS, + groupOption, hasComputedRange, readBeforeOption, readLengthOption, @@ -227,7 +228,7 @@ export function createStructuresSection( !!type && !managed && type.placement === "interval" && !hasComputedRange(type); positionRow.hidden = !type; positionDivider.hidden = !type; - summary.root.hidden = !type || hasSpecContainers(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1) + summary.root.hidden = !type || hidesTypeSummary(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1) startFields.wrap.hidden = !isInterval; endFields.wrap.hidden = !isInterval; primary.disabled = !type; @@ -544,7 +545,7 @@ export function createStructuresSection( // 첫 항목은 빈칸 — 진입·리셋 상태에서 군·종류를 강제로 고르게 두지 않는다. 이름은 정의(48) 먼저. groupSelect.replaceChildren( new Option("— 선택 —", ""), - ...[...groups].map(([g, name]) => new Option(name ?? GROUP_LABELS[g] ?? g, g)), + ...[...groups].map(([g, name]) => groupOption(g, name ?? GROUP_LABELS[g] ?? g)), ); groupSelect.value = ""; syncTypeOptions(); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index cab419389..c33612c6b 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -23,9 +23,10 @@ import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { solveSplit, specKeys, type SplitRole } from "../A00_Common/design/design_structure_spec"; -/** 컨테이너로 그리는 종류인지(명세 · 정의 그룹 상자) — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1 · 52-7). */ -export const hasSpecContainers = (type: StructureType): boolean => - !!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length; +/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로 + * 그리는 종류(PLAN 52-1 · 52-6b · 52-7) · 종단배수(B군 · 조합 잇기 없음 · 표 07). */ +export const hidesTypeSummary = (type: StructureType): boolean => + !!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length || type.group === "B"; /** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일). */ const SOLVE_SPLIT = new Set(["guardrail"]); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index ff1daad7a..04fa037cd 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -34,6 +34,18 @@ export const GROUP_LABELS: Record = { 호환: "기타", }; +/** 새로 놓을 때 고르기 목록(군 드롭다운 · 우클릭 메뉴)에서 뺄 군 — 종단배수·계류사방(표 07). + * 옛 값 · 도면 · 집계는 그대로이고 데이터도 지우지 않는다. */ +export const HIDDEN_PICK_GROUPS: ReadonlySet = new Set(["B", "D"]); + +/** 군 드롭다운 항목 — 뺄 군은 가림(옛 항목을 열 때는 값으로 그대로 고를 수 있음). */ +export function groupOption(group: string, name: string): HTMLOptionElement { + const option = new Option(name, group); + option.hidden = HIDDEN_PICK_GROUPS.has(group); + if (option.hidden) option.dataset.pickHidden = ""; // 정의 폼의 「가림 풀기」가 건드리지 않게 + return option; +} + /** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */ export const DEFAULT_INTERVAL_LENGTH_M = 15; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index 26899fdc1..d16428ac1 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -26,6 +26,8 @@ import { } from "./M02_SO_Def_Model"; import { buildOptionsBlock } from "./M02_SO_Def_Options"; import { buildRulesBlock } from "./M02_SO_Def_Rules"; +import { buildSpecBlock } from "./M02_SO_Def_Spec"; +import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe"; import { checkBox, textBox } from "./M02_SO_Def_Ui"; import { buildGroupBlock } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; @@ -182,6 +184,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { } const edit = canEdit(); const extra = (): NonNullable => (d.종류[type.type_id] ??= {}); + // 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김 + const spec = STRUCTURE_SPECS[type.type_id]; detail.replaceChildren( el("div", { className: "m02-sod__kindhead", @@ -196,16 +200,20 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { edit, attr: "data-sod-kind-off", }), - checkBox( - to("BaseStationShow"), - baseShown(d, type.type_id), - (v) => { - if (v) delete extra().기준측점; - else extra().기준측점 = false; - changed(); - }, - { edit, attr: "data-sod-base" }, - ), + ...(spec + ? [] + : [ + checkBox( + to("BaseStationShow"), + baseShown(d, type.type_id), + (v) => { + if (v) delete extra().기준측점; + else extra().기준측점 = false; + changed(); + }, + { edit, attr: "data-sod-base" }, + ), + ]), ...(d.종류[type.type_id]?.동작 ?? []).map((a) => { const tag = createTag(a, "neutral"); tag.title = d.동작[a] ?? ""; @@ -213,10 +221,14 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { }), ], }), - buildOptionsBlock({ def: d, type, edit, open, changed }), - buildLayoutBlock({ def: d, type, edit, changed }), - buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), - buildRulesBlock({ def: d, type, edit, changed }), + ...(spec + ? [buildSpecBlock({ def: d, type, spec, edit, changed })] + : [ + buildOptionsBlock({ def: d, type, edit, open, changed }), + buildLayoutBlock({ def: d, type, edit, changed }), + buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), + buildRulesBlock({ def: d, type, edit, changed }), + ]), ); }; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts index d45d9511b..2cb6355bb 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts @@ -57,7 +57,8 @@ export interface StructDef { 구조물군: Record; 동작: Record; types: TypeDef[]; - 종류: Record; + /** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */ + 종류: Record }>; 표시: Record>; 그룹: Record; 배치: Record>; @@ -257,6 +258,10 @@ export function problems(def: StructDef): string[] { if (!o.choices?.length) out.push(`${t.name}.${o.label} 고르기 꼴은 선택지가 있어야 함`); } const grp = badNames((def.그룹[t.type_id] ?? []).map((g) => g.이름)); + const spec = Object.entries(def.종류[t.type_id]?.이름 ?? {}) + .filter(([, v]) => !v.trim() || v.length > NAME_MAX) + .map(([k]) => k); + if (spec.length) out.push(`${t.name} 명세 이름: ${spec.join(", ")}`); if (grp.length) out.push(`${t.name} 그룹 이름: ${grp.join(", ")}`); const lap = overlaps(placeAll(t.options, def.배치[t.type_id])); if (lap.length) out.push(`${t.name} 배치 겹침: ${lap.join(", ")}`); diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts new file mode 100644 index 000000000..63157ee50 --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts @@ -0,0 +1,67 @@ +/* ============================================================================= + * M02_SO_Def_Spec.ts + * 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴 + * 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 · + * 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음. + * 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침. + * ========================================================================== */ + +import { createTag, el } from "@ui/ui_template_elements"; +import { createSpecForm } from "../../A00_Common/design/design_structure_form"; +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef, TypeDef } from "./M02_SO_Def_Model"; +import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model"; +import { head, textBox } from "./M02_SO_Def_Ui"; +import { to, type OptionsTextKey } from "./M02_SO_Text"; + +const TAG: Record, OptionsTextKey> = { + required: "SpecRequired", + optional: "SpecOptional", + select: "FormSelect", + number: "FormNumber", + auto: "FormAuto", +}; + +export function buildSpecBlock(opt: { + def: StructDef; + type: TypeDef; + spec: StructureSpec; + edit: boolean; + changed: () => void; +}): HTMLElement { + const { def, type, spec, edit, changed } = opt; + const rows = specNameRows(def, spec).map((row) => + el("div", { + className: `m02-sod__specrow is-level-${row.level}`, + attrs: { "data-sod-spec": row.nameKey }, + children: [ + textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { + edit, + attr: "data-sod-spec-name", + cls: "m02-sod__name", + }), + el("small", { className: "m02-opts__key", text: row.key }), + ...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []), + ], + }), + ); + // 미리보기 — 값 없는 새 구조물 폼 · 잇기 줄 없음(조합은 프로젝트 몫) + const preview = createSpecForm(null); + const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름])); + preview.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups); + preview.root.inert = true; + return el("div", { + className: "m02-sod__block", + attrs: { "data-sod-spec-block": "" }, + children: [ + ...head(to("SpecHead"), to("SpecNote")), + ...rows, + ...head(to("SpecPreview")), + el("div", { + className: "m02-sod__specpreview", + attrs: { "data-sod-spec-preview": "" }, + children: [preview.root], + }), + ], + }); +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts new file mode 100644 index 000000000..bd6bc693b --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts @@ -0,0 +1,91 @@ +/* ============================================================================= + * M02_SO_Def_Spec_Model.ts + * 구조물 명세(PLAN 52 · `design_structure_spec`) 이름 고치기(52-11)의 자료 손질 — 화면 그리기 없음. + * + * · 명세 = 구조 정본(키 · 컨테이너 · 필수/선택 · 꼴) — 여기서는 **이름만** 고침 + * · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`) + * · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기 + * (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀 + * import 는 꼴만 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef } from "./M02_SO_Def_Model"; + +export interface SpecNameRow { + /** 0 = 컨테이너 · 1 = 항목 · 2 = 값 */ + level: 0 | 1 | 2; + /** 이름 표 키 */ + nameKey: string; + /** 읽기만 표시 — 컨테이너 id · 항목 id · 값 */ + key: string; + /** 지금 이름 */ + label: string; + /** 읽기만 표시 — 필수/선택 · 꼴 */ + tag: "required" | "optional" | "select" | "number" | "auto" | null; + /** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */ + group: string | null; +} + +/** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */ +export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] { + const names = def.종류[spec.typeId]?.이름 ?? {}; + const groups = def.그룹[spec.typeId] ?? []; + const rows: SpecNameRow[] = []; + const seen = new Set(); + const push = (row: SpecNameRow): void => { + if (seen.has(row.nameKey)) return; + seen.add(row.nameKey); + rows.push(row); + }; + for (const c of spec.containers) { + const group = + c.required && names[c.id] === undefined ? groups.find((g) => g.id === c.link) : undefined; + push({ + level: 0, + nameKey: c.id, + key: c.id, + label: names[c.id] ?? group?.이름 ?? c.title, + tag: c.required ? "required" : "optional", + group: group ? group.id : null, + }); + for (const item of c.items) { + const itemKey = `${c.id}.${item.id}`; + push({ + level: 1, + nameKey: itemKey, + key: item.id, + label: names[itemKey] ?? item.label, + tag: item.kind, + group: null, + }); + const choices = [ + ...(item.choices ?? []), + ...Object.values(item.by?.map ?? {}).flatMap((b) => b.choices ?? []), + ]; + for (const ch of choices) { + const choiceKey = `${itemKey}=${ch.value}`; + push({ + level: 2, + nameKey: choiceKey, + key: ch.value, + label: names[choiceKey] ?? ch.label, + tag: null, + group: null, + }); + } + } + } + return rows; +} + +/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */ +export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void { + const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined; + if (group) { + group.이름 = value; + return; + } + const kind = (def.종류[typeId] ??= {}); + (kind.이름 ??= {})[row.nameKey] = value; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Style.css b/M02_MasterTemplete/StructOptions/M02_SO_Style.css index a8570c86a..492eb8c4f 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Style.css +++ b/M02_MasterTemplete/StructOptions/M02_SO_Style.css @@ -332,3 +332,28 @@ margin: 0; color: var(--color-danger); } + +/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */ +.m02-sod__specrow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); +} + +.m02-sod__specrow.is-level-0 { + margin-top: var(--spacing-8); + font-weight: 600; +} + +.m02-sod__specrow.is-level-1 { + padding-left: 1.5rem; +} + +.m02-sod__specrow.is-level-2 { + padding-left: 3rem; +} + +.m02-sod__specpreview { + max-width: 28rem; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 90037edd8..3235e7762 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -45,6 +45,15 @@ const TEXT = { FormNumber: ["숫자", "Number"], FormText: ["글", "Text"], FormBool: ["예/아니오", "Yes/No"], + FormAuto: ["자동", "Auto"], + SpecHead: ["구조물 명세 이름", "Structure spec names"], + SpecNote: [ + "컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림", + "Rename containers › items › values only — keys, required/optional, form and structure come from the spec", + ], + SpecRequired: ["필수", "Required"], + SpecOptional: ["선택", "Optional"], + SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"], Yes: ["예", "Yes"], No: ["아니오", "No"], Unit: ["단위", "Unit"], diff --git a/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md b/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md new file mode 100644 index 000000000..8f544555e --- /dev/null +++ b/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md @@ -0,0 +1,17 @@ +# 구조물 표 08 — 안전부대용지 사토장 · 토취장 · 노면공 콘크리트 포장 · 기타 (사용자 10-5 · 원문 그대로) + +구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목 … 상세 설계 필수 선택 +안전부대용지 유용토운반작업장(사토장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 사토장 부피 수기입력 + +안전부대용지 토사채취장(토취장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 토취장 부피 수기입력 + +노면공 콘크리트 포장 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 포장사양 콘크리트, XXX + 높이 수기입력 + +기타 수기입력 측점 No, + 측점거리 (선택)컨테이너 제목 : 상세 내용 (상세 설계) 도수로(or 돌붙임) + 비고 수기 입력 (상세 설계) 도수로(or 돌붙임) 관보호공, 옹벽, 돌쌓기, 흙막이, 큰톨쌓기 가 같은 형상으로 보임. + +(가드레일은 표 07 덧과 같음) diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 8c77cb1ae..f12c644af 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -1,5 +1,5 @@ { - "판": 7, + "판": 9, "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`가 새 키로 옮긴다.", "구조물군": { @@ -676,28 +676,41 @@ "cross_section", "quantity" ], + "options": [] + }, + { + "type_id": "cross_underdrain", + "group": "A", + "name": "맹암거", + "placement": "point", + "style": { + "color": "#3aa0a8", + "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": "연장", + "key": "culvert_height_m", + "label": "암거 높이", "input": "number", "unit": "m", "default": null, "required": false, - "empty_means": "비우면 표가 안 서고 「연장을 적어야 섬」 사유가 뜸 — 개거는 m당 원단위라 연장이 곧 밑수(2026-09-14 A2)" + "empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)" + }, + { + "key": "culvert_slope_pct", + "label": "암거 경사", + "input": "number", + "unit": "%", + "default": null, + "required": false, + "empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)" } ] }, @@ -3384,7 +3397,25 @@ "계곡통과시설", "월류단면", "날개벽" - ] + ], + "이름": { + "ford.diameter": "관경", + "ford.diameter=800": "800", + "ford.diameter=1000": "1000", + "ford.diameter=1200": "1200", + "ford.diameter=1500": "1500", + "ford.width": "월류 폭 (m)", + "ford.height": "월류 높이 (m)", + "ford.count": "수량 (련)", + "inlet.structure": "구조", + "inlet.structure=없음": "없음", + "inlet.structure=날개벽": "날개벽", + "inlet.structure=흙막이": "흙막이", + "outlet.structure": "구조", + "outlet.structure=없음": "없음", + "outlet.structure=날개벽": "날개벽", + "outlet.structure=흙막이": "흙막이" + } }, "revetment": { "동작": [ @@ -3451,6 +3482,17 @@ ], "형상": {} } + ], + "cross_underdrain": [ + { + "id": "gcrossud", + "이름": "맹암거", + "옵션": [ + "culvert_height_m", + "culvert_slope_pct" + ], + "형상": {} + } ] }, "배치": {}, diff --git a/resources/master_template/구조물정의_이력/판_6.json b/resources/master_template/구조물정의_이력/판_6.json index 1be3c8ef2..5b2e2bf07 100644 --- a/resources/master_template/구조물정의_이력/판_6.json +++ b/resources/master_template/구조물정의_이력/판_6.json @@ -2732,12 +2732,26 @@ "drawing_views": [ "plan" ], - "options": [] + "options": [ + { + "key": "facing", + "label": "방향", + "input": "select", + "choices": [ + "좌", + "우", + "양면" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] }, { "type_id": "guardrail", "group": "E", - "name": "가드레일", + "name": "가드레일·경계석·위험표지", "placement": "interval", "style": { "color": "#1f9e46", @@ -2750,32 +2764,17 @@ ], "options": [ { - "key": "form", - "label": "형태", - "input": "text", + "key": "kind", + "label": "종류", + "input": "select", + "choices": [ + "가드레일", + "경계석", + "위험표지" + ], "default": null, - "required": false - }, - { - "key": "length_m", - "label": "길이", - "input": "number", - "unit": "m", - "default": null - }, - { - "key": "before_m", - "label": "기준측점 전", - "input": "number", - "unit": "m", - "default": null - }, - { - "key": "after_m", - "label": "기준측점 후", - "input": "number", - "unit": "m", - "default": null + "required": true, + "phase": "detail" } ] }, diff --git a/resources/master_template/구조물정의_이력/판_7.json b/resources/master_template/구조물정의_이력/판_7.json new file mode 100644 index 000000000..6694fae47 --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_7.json @@ -0,0 +1,3336 @@ +{ + "판": 7, + "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": [] + }, + { + "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": { + "동작": [ + "계곡통과시설", + "유입유출_묶음", + "기슭막이_한벌" + ], + "이름": { + "pipe.diameter": "관경", + "pipe.diameter=800": "800", + "pipe.diameter=1000": "1000", + "pipe.diameter=1200": "1200", + "pipe.diameter=1500": "1500", + "pipe.length": "관 길이 (m)", + "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.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": { + "동작": [ + "계곡통과시설", + "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/master_template/구조물정의_이력/판_8.json b/resources/master_template/구조물정의_이력/판_8.json new file mode 100644 index 000000000..58479e6f4 --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_8.json @@ -0,0 +1,3360 @@ +{ + "판": 8, + "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": [] + }, + { + "type_id": "guardrail", + "group": "E", + "name": "가드레일", + "placement": "interval", + "style": { + "color": "#1f9e46", + "abbr": "가드" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형태", + "input": "text", + "default": null, + "required": false + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": null + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": null + } + ] + }, + { + "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": { + "동작": [ + "계곡통과시설", + "유입유출_묶음", + "기슭막이_한벌" + ], + "이름": { + "pipe.diameter": "관경", + "pipe.diameter=800": "800", + "pipe.diameter=1000": "1000", + "pipe.diameter=1200": "1200", + "pipe.diameter=1500": "1500", + "pipe.length": "관 길이 (m)", + "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.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": { + "동작": [ + "계곡통과시설", + "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_4_ford.cjs b/resources/tester/helper_52_4_ford.cjs new file mode 100644 index 000000000..b11d13b4c --- /dev/null +++ b/resources/tester/helper_52_4_ford.cjs @@ -0,0 +1,86 @@ +/* 세월교 명세(표 04) · 월류 폭 ↔ 높이 계산 단위검증 헬퍼(PLAN 52-4) — `design_structure_spec.ts` · + * `design_structure_ford.ts` · `design_structure_ford_calc.ts`(+ `fordSection` 이 든 B05 칸 조각)를 그 자리에서 + * 트랜스파일해 Node 로 돌림. 결과 = JSON 한 벌. 인자 `names` 면 세월교 이름 표(정의 `종류.ford_bridge.이름`)만 냄. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +/** TS 한 파일을 CommonJS 로 — 상대 import 는 그 파일 자리 기준 · `@config/` 는 config 폴더 · .json 은 그대로. */ +const load = (file) => { + if (cache.has(file)) return cache.get(file); + if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8")); + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(file, box.exports); + const near = (m) => { + const base = m.startsWith("@config/") + ? path.join(ROOT, "config", m.slice(8)) + : path.join(path.dirname(file), m); + return load(base.endsWith(".json") ? base : `${base}.ts`); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + cache.set(file, box.exports); + return box.exports; +}; +const D = (f) => path.join(ROOT, "A00_Common", "design", f); +const s = load(D("design_structure_spec.ts")); +const { FORD_SPEC, withLegacyGuards } = load(D("design_structure_ford.ts")); +const calc = load(D("design_structure_ford_calc.ts")); + +if (process.argv[2] === "names") { + process.stdout.write(JSON.stringify(s.nameTable(FORD_SPEC))); + process.exit(0); +} + +const old = { + pipe_kind: "파형강관", + pipe_diameter_mm: 1000, + pipe_count: 2, + ford_width_m: 10, + ford_height_m: 0.3, + wing_in: "있음", + wing_in_height_m: 1, + wing_out: "없음", +}; +const legacy = withLegacyGuards(old); +const state = s.readState(FORD_SPEC, legacy); +const back = s.writeOptions(FORD_SPEC, state); +const soil = JSON.parse(JSON.stringify(state)); +soil.inlet[0].structure = "흙막이"; +soil.outlet[0].structure = "날개벽"; +const soilOut = s.writeOptions(FORD_SPEC, soil); + +const flows = [0.5, 2.54, 8]; +const widths = [3, 5, 10, 20]; +const table = flows.flatMap((q) => widths.map((b) => [q, b, calc.fordHeightM(q, b)])); + +const out = { + containers: FORD_SPEC.containers.map((c) => [c.id, c.title, c.required]), + items: FORD_SPEC.containers.map((c) => c.items.map((i) => [i.id, i.kind, i.key ?? null])), + choices: FORD_SPEC.containers.map((c) => + c.items.filter((i) => i.kind === "select").map((i) => s.choicesOf(i, {}).map((ch) => ch.label)), + ), + legacy: { inlet: legacy.inlet_type, outlet: legacy.outlet_type }, + keepSoil: withLegacyGuards({ inlet_type: "흙막이", wing_in: "없음" }).inlet_type, + b06Off: withLegacyGuards({ inlet_type: "날개벽", wing_in: "없음" }).inlet_type, + b06On: withLegacyGuards({ inlet_type: "없음", wing_in: "있음" }).inlet_type, + state, + back, + soilOut, + links: FORD_SPEC.containers.map((c) => s.linkOf(c, 1)), + keys: s.specKeys(FORD_SPEC), + retired: FORD_SPEC.retired, + table, + none: [calc.fordHeightM(null, 10), calc.fordHeightM(2.54, 0)], + hints: [ + calc.fordHeightHint(2.54, 10), + calc.fordHeightHint(2.54, 0), + calc.fordHeightHint(null, 10), + ], +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_52_structure_spec.cjs b/resources/tester/helper_52_structure_spec.cjs index 0ff82723b..168721216 100644 --- a/resources/tester/helper_52_structure_spec.cjs +++ b/resources/tester/helper_52_structure_spec.cjs @@ -13,7 +13,8 @@ const load = (file, deps = {}) => { 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)); + const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m)); // 같은 폴더 명세 + new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? near(m)); return box.exports; }; const s = load("design_structure_spec.ts"); diff --git a/resources/tester/helper_m02_so_def_spec.cjs b/resources/tester/helper_m02_so_def_spec.cjs new file mode 100644 index 000000000..ede10f95b --- /dev/null +++ b/resources/tester/helper_m02_so_def_spec.cjs @@ -0,0 +1,64 @@ +/* M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증 헬퍼 — + * TS 를 그 자리에서 트랜스파일해 Node 로 돌리고 결과를 JSON 으로 냄(pytest 가 판정). + * 정의는 진짜 `구조물정의.json` 을 읽어 메모리 사본만 고침(파일 쓰기 없음). */ +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 = (...parts) => { + const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + new Function("exports", "module", "require", js)(box.exports, box, require); + return box.exports; +}; +const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts"); +const { PIPE_SPEC } = load("A00_Common", "design", "design_structure_pipe.ts"); +const real = () => + JSON.parse( + fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"), + ); + +const out = {}; +const def = real(); +const rows = m.specNameRows(def, PIPE_SPEC); +out.levels = [0, 1, 2].map((lv) => rows.filter((r) => r.level === lv).length); +out.unique = new Set(rows.map((r) => r.nameKey)).size === rows.length; +out.pipeTitle = rows.find((r) => r.nameKey === "pipe"); +out.optional = rows.find((r) => r.nameKey === "soil_add"); +out.basinForm = rows.find((r) => r.nameKey === "inlet.form=I"); + +// 이름 고침 — 그룹 제목 · 항목 · 값 · 선택 컨테이너 +const work = real(); +const pick = (k) => m.specNameRows(work, PIPE_SPEC).find((r) => r.nameKey === k); +m.setSpecName(work, "pipe", pick("pipe"), "관 본체"); +m.setSpecName(work, "pipe", pick("pipe.diameter"), "관 지름"); +m.setSpecName(work, "pipe", pick("pipe.diameter=800"), "Ø800"); +m.setSpecName(work, "pipe", pick("soil_add"), "흙막이 더"); +out.groupName = work.그룹.pipe.find((g) => g.id === "grv6lny").이름; +out.names = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map( + (k) => work.종류.pipe.이름[k] ?? null, +); +out.reread = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map((k) => pick(k).label); +// 구조는 그대로 — types · 동작 · 다른 종류 · 이름 표 키 수(새 키 없음) +const before = real(); +out.sameTypes = JSON.stringify(work.types) === JSON.stringify(before.types); +out.sameKeys = + Object.keys(work.종류.pipe.이름).length === Object.keys(before.종류.pipe.이름).length; + +// 이름 표가 빈 종류 — 명세 글로 읽고 고치면 이름 표가 생김 +const bare = real(); +delete bare.종류.pipe.이름; +const bareRows = m.specNameRows(bare, PIPE_SPEC); +out.bareLabel = bareRows.find((r) => r.nameKey === "inlet.structure=기슭막이").label; +m.setSpecName( + bare, + "pipe", + bareRows.find((r) => r.nameKey === "inlet"), + "유입", +); +out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_52_4_structure_spec_ford.py b/resources/tester/test_52_4_structure_spec_ford.py new file mode 100644 index 000000000..90a973a9b --- /dev/null +++ b/resources/tester/test_52_4_structure_spec_ford.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""횡단배수 › 세월교(표 04) 명세 + 월류 폭 ↔ 높이 연동 — PLAN 52-4. + +순수 규칙은 Node 헬퍼(`helper_52_4_ford.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-4_세월교_컨테이너.js`. +높이 거울 = 이 파일이 Manning(통수단면.md §2 · n 0.017 · S 0.10)을 따로 풀어 같은 값인지 봄. +""" + +import json +import math +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) +N, S = 0.017, 0.10 + + +def _node(*args): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_52_4_ford.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"] == [ + ["ford", "세월교", True], + ["inlet", "유입 구조", True], + ["outlet", "유출 구조", True], + ] + assert out["items"][0] == [ + ["diameter", "select", "pipe_diameter_mm"], + ["width", "number", "ford_width_m"], + ["height", "number", "ford_height_m"], + ["count", "number", "pipe_count"], + ] + assert out["choices"] == [ + [["800", "1000", "1200", "1500"]], + [["없음", "날개벽", "흙막이"]], + [["없음", "날개벽", "흙막이"]], + ] + + +def test_옛_날개벽_칸으로_구조를_세우고_같은_키로_씀(out): + assert out["legacy"] == {"inlet": "날개벽", "outlet": "없음"} + assert out["keepSoil"] == "흙막이" # 흙막이는 설치 칸 없음이어도 그대로 + assert out["b06Off"] == "없음" and out["b06On"] == "날개벽" # B06 조정창 설치 칸을 따름 + back = out["back"] + assert back["pipe_diameter_mm"] == 1000 and back["pipe_count"] == 2 + assert back["ford_width_m"] == 10 and back["ford_height_m"] == 0.3 + assert back["wing_in"] == "있음" and back["wing_out"] == "없음" + # 표 04 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값) + assert "pipe_kind" not in back and "wing_in_height_m" not in back + assert "pipe_kind" in out["retired"] + + +def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out): + s = out["soilOut"] + assert s["inlet_type"] == "흙막이" and s["wing_in"] == "없음" + assert s["outlet_type"] == "날개벽" and s["wing_out"] == "있음" + + +def test_잇기_키_컨테이너마다(out): + assert out["links"] == ["gford", "gfordin", "gfordout"] + + +def _manning_depth(q, b): + """넓은 직사각 수로 Manning 을 h 에 대해 이분법으로 풂 — 헬퍼와 다른 풀이.""" + + def cap(h): + return b * h * (1 / N) * ((b * h) / (b + 2 * h)) ** (2 / 3) * math.sqrt(S) + + lo, hi = 0.0, 5.0 + for _ in range(100): + mid = (lo + hi) / 2 + lo, hi = (mid, hi) if cap(mid) < q else (lo, mid) + return hi + + +def test_월류_높이는_폭과_연동_필요_수심_올림(out): + for q, b, h in out["table"]: + need = _manning_depth(q, b) + assert h == pytest.approx(math.ceil(need * 100 - 1e-9) / 100), (q, b) + # 폭이 넓을수록 높이가 낮아짐 + row = [h for q, b, h in out["table"] if q == 2.54] + assert row == sorted(row, reverse=True) and len(set(row)) == len(row) + assert out["none"] == [None, None] # 설계유량 · 폭이 없으면 손 입력 + assert out["hints"] == ["필요 수심 0.08 이상", "월류 폭을 넣으면 셈", "설계유량 없음 — 손 입력"] + + +def test_정의_파일에_세월교_이름_표가_올라_있음(): + names = _node("names") + with open( + os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8" + ) as f: + doc = json.load(f) + assert doc["종류"]["ford_bridge"]["이름"] == names diff --git a/resources/tester/test_52_6_cross_underdrain.py b/resources/tester/test_52_6_cross_underdrain.py new file mode 100644 index 000000000..b8146e009 --- /dev/null +++ b/resources/tester/test_52_6_cross_underdrain.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""52-6 횡단배수 › 맹암거(표 06 · 새 종류) — 정의 · 컨테이너 · 서버 저장 왕복. + +새 키 `cross_underdrain`(횡단배수 · 점형) — 종단배수 맹암거(`underdrain`)와 따로. +컨테이너 = 정의 그룹 「맹암거」(필수 · 늘 한 벌) — 암거 높이(m) · 암거 경사(%) 손 입력(비워 둘 수 있음) · +그룹 잇기(`group_items`)로 구조물 조합 항목. 저장은 임시 storage 에서만. +""" + +import pytest + +from B05_Profile.B05_Profile_Structures_Schema import ( + StructureInstance, + is_station_planting_type, + load_definition, + structure_type_map, +) + +KEY = "cross_underdrain" +GROUP = "gcrossud" + + +def test_새_종류는_횡단배수_점형_종단배수_맹암거와_따로(): + types = structure_type_map() + new, old = types[KEY], types["underdrain"] + assert (new.group, new.placement, new.name, new.enabled) == ("A", "point", "맹암거", True) + assert (old.group, old.placement) == ("B", "interval") + assert new.style["abbr"] != old.style["abbr"] and new.style["color"] != old.style["color"] + assert is_station_planting_type(new) # 횡단배수 = 그 측점에 횡단도 + + +def test_컨테이너_맹암거_높이_경사_손_입력(): + opts = {o.key: o for o in structure_type_map()[KEY].options} + assert list(opts) == ["culvert_height_m", "culvert_slope_pct"] + for key, label, unit in ( + ("culvert_height_m", "암거 높이", "m"), + ("culvert_slope_pct", "암거 경사", "%"), + ): + o = opts[key] + assert (o.label, o.input, o.unit, o.default, o.required) == ( + label, + "number", + unit, + None, + False, + ) + assert o.empty_means # 손 입력 · 기본값 근거 없음 — 비워 둘 수 있음(B05 단계 필수 금지) + groups = load_definition()["그룹"][KEY] + assert groups == [ + {"id": GROUP, "이름": "맹암거", "옵션": list(opts), "형상": {}}, + ] + + +def test_정의_검사_통과(): + from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition + + assert definition.check(load_definition()) == [] + + +def _item(**options): + return StructureInstance( + type_id=KEY, + placement="point", + chainage_m=63.5, + options=options, + group_items={ + GROUP: {"layer": "system", "owner": None, "key": "SS000001", "name": "맹암거"} + }, + ) + + +def test_저장_다시_읽기_그대로(tmp_path): + from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures + + item = _item(culvert_height_m=0.6, culvert_slope_pct=2.5) + save_structures(str(tmp_path), [item], base_revision=0) + _, loaded = load_structures(str(tmp_path)) + assert [(s.type_id, s.chainage_m, s.options) for s in loaded] == [ + (KEY, 63.5, {"culvert_height_m": 0.6, "culvert_slope_pct": 2.5}) + ] + assert loaded[0].group_items == item.group_items + + +@pytest.mark.parametrize( + "options", + [{"culvert_height_m": -1}, {"culvert_slope_pct": -0.5}], +) +def test_음수는_서버가_막음(tmp_path, options): + from B05_Profile.B05_Profile_Structures_Repository import save_structures + + with pytest.raises(ValueError): + save_structures(str(tmp_path), [_item(**options)], base_revision=0) diff --git a/resources/tester/test_b05_structures_define.py b/resources/tester/test_b05_structures_define.py index 1cb497663..a767f877c 100644 --- a/resources/tester/test_b05_structures_define.py +++ b/resources/tester/test_b05_structures_define.py @@ -74,7 +74,7 @@ def test_정의_덧칸_얹기(out): def test_판1_정의는_지금_화면과_같음(out): - assert out["v1Types"] == 37 + assert out["v1Types"] == 42 # 52-6 맹암거 · 52-7 대피로 · 경계석 · 위험표시 · 취수정 assert out["v1Same"] is True assert out["v1Columns"] == [2] assert out["v1Anchor"] is True diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index 22f41b59e..39a16cc19 100644 --- a/resources/tester/test_b05_structures_registry_policy.py +++ b/resources/tester/test_b05_structures_registry_policy.py @@ -189,7 +189,7 @@ def test_quick_add_types_exclude_pipe_family(): def test_all_enabled_non_managed_types_are_quick_addable(): - """잔존 B05 선택지 = A군 2(노출형·개거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1. + """잔존 B05 선택지 = A군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1. 콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로 받는 정본이 됐다(종단경사 자동 포장 적용을 대체). @@ -204,7 +204,7 @@ def test_all_enabled_non_managed_types_are_quick_addable(): E군이 10에서 14로 늘었다 — 표 07(PLAN 52-7) 대피로 · 경계석 · 위험표시 · 취수정. """ - assert len(_quick_add_types()) == 27 + assert len(_quick_add_types()) == 28 def test_removed_types_are_absent(): @@ -368,6 +368,7 @@ def test_a6_a7_types_exist_for_manual_placement(): item = by_id[type_id] assert item.group == "A" and item.managed_by is None assert item.placement == "point" + assert not item.options, f"{type_id} 옵션 없음 — 기준 측점 + 종류 단위 잇기만(PLAN 52-5)" def test_ford_bridge_shares_pipe_kind_and_diameter(): diff --git a/resources/tester/test_m02_so_def_spec.py b/resources/tester/test_m02_so_def_spec.py new file mode 100644 index 000000000..3677816ff --- /dev/null +++ b/resources/tester/test_m02_so_def_spec.py @@ -0,0 +1,58 @@ +"""M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증. + +Node 헬퍼(`helper_m02_so_def_spec.cjs`)가 TS 를 트랜스파일해 진짜 정의 사본(메모리)으로 돌리고 +pytest 는 그 결과(JSON)를 판정. + +못박는 것 + - 배관 명세 나무: 컨테이너 5 · 항목 · 값 줄 · 이름 키 겹침 없음 · 필수/선택 · 꼴 표시. + - 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀. + - 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음. + - 이름 표가 빈 종류는 명세 글로 읽음. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent + + +@pytest.fixture(scope="module") +def r() -> dict: + proc = subprocess.run( + ["node", str(HERE / "helper_m02_so_def_spec.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_배관_나무(r) -> None: + assert r["levels"][0] == 5 + assert r["levels"][1] > 5 and r["levels"][2] > 10 + assert r["unique"] is True + assert r["pipeTitle"]["tag"] == "required" and r["pipeTitle"]["group"] == "grv6lny" + assert r["optional"]["tag"] == "optional" and r["optional"]["group"] is None + assert r["basinForm"]["label"] == "I형" and r["basinForm"]["key"] == "I" + + +def test_이름_고침_자리(r) -> None: + assert r["groupName"] == "관 본체" + assert r["names"] == [None, "관 지름", "Ø800", "흙막이 더"] + assert r["reread"] == ["관 본체", "관 지름", "Ø800", "흙막이 더"] + + +def test_구조는_그대로(r) -> None: + assert r["sameTypes"] is True and r["sameKeys"] is True + + +def test_이름_표_없는_종류(r) -> None: + assert r["bareLabel"] == "흙막이" + assert r["bareGroup"] == "유입" diff --git a/resources/tester/ui_checks/52-11_정의_이름고치기.js b/resources/tester/ui_checks/52-11_정의_이름고치기.js new file mode 100644 index 000000000..6d4679afe --- /dev/null +++ b/resources/tester/ui_checks/52-11_정의_이름고치기.js @@ -0,0 +1,155 @@ +/* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여 + * M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자: + * orca eval --expression "window.__m5211='m02'; $(cat 이 파일)" + * B05 종단설계(#/b05-profile) · 목록에 「배관 1」: + * orca eval --expression "window.__m5211='b05'; $(cat 이 파일)" + * 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 · + * B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림 + * 기대 수치(m02): + * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기 + * - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님) + * - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐 + * - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로 + * 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true + */ +(async () => { + const MODE = window.__m5211 || "m02"; + const W = (ms) => new Promise((r) => setTimeout(r, ms)); + const $ = (s, root = document) => root.querySelector(s); + const $$ = (s, root = document) => [...root.querySelectorAll(s)]; + const until = async (fn, ms = 10000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await W(150); + } + return null; + }; + const put = (box, value) => { + box.value = value; + box.dispatchEvent(new Event("input", { bubbles: true })); + box.dispatchEvent(new Event("change", { bubbles: true })); + }; + const NEW = { pipe: "관 본체X", "pipe.diameter": "관 지름X", "pipe.diameter=800": "Ø800X" }; + const out = { mode: MODE }; + + if (MODE === "m02") { + const getDef = async () => + (await fetch("/api/m02/structure-definition", { credentials: "include" })).json(); + const first = await getDef(); + out.판 = first.판; + const row = await until(() => $("[data-options-row]")); + if (!row) return JSON.stringify({ error: "정의 줄 없음" }); + row.click(); + if (!(await until(() => $$("[data-sod-kind]").length))) + return JSON.stringify({ error: "종류 목록 없음" }); + // 명세 없는 종류 — 옛 편집기 그대로 + $('[data-sod-kind="retaining_wall"]').click(); + await W(200); + out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") }; + // 배관 — 명세 이름 나무 + $('[data-sod-kind="pipe"]').click(); + await W(200); + const block = $("[data-sod-spec-block]"); + const detail = $("[data-sod-detail]"); + const rows = $$("[data-sod-spec]", block); + out.spec = { + rows: rows.length, + containers: $$(".is-level-0", block).length, + inputs: $$("input, select, textarea", block).filter( + (n) => !n.closest("[data-sod-spec-preview]"), + ).length, + nameInputs: $$("[data-sod-spec-name]", block).length, + keysReadOnly: rows.every((r) => r.querySelector("small.m02-opts__key")), + }; + out.old = { + base: !!$("[data-sod-base]", detail), + blocks: detail.children.length, // 머리 칸 + 명세 칸 = 2 + }; + const save = $("[data-sod-save]"); + out.saveBefore = save.disabled; + const box = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`); + const orig = Object.fromEntries(Object.keys(NEW).map((k) => [k, box(k).value])); + for (const [k, v] of Object.entries(NEW)) { + put(box(k), v); + await W(150); + } + const preview = $("[data-sod-spec-preview]"); + const pipeBox = $('[data-spec-container="pipe"]', preview); + out.preview = { + title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe), + item: pipeBox + .querySelector('[data-spec-item="pipe.diameter"]') + .textContent.includes(NEW["pipe.diameter"]), + choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some( + (o) => o.textContent === NEW["pipe.diameter=800"], + ), + }; + out.saveAfterEdit = $("[data-sod-save]").disabled; + // 빈 이름 → 저장 막힘 + put(box("pipe.diameter"), ""); + await W(150); + out.blank = { + issue: $("[data-sod-issues]").textContent.includes("명세 이름"), + save: $("[data-sod-save]").disabled, + }; + // 되돌림 + for (const [k, v] of Object.entries(orig)) { + put(box(k), v); + await W(150); + } + out.restored = Object.entries(orig).every(([k, v]) => box(k).value === v); + out.판After = (await getDef()).판; + out.ok = + out.other.base && + !out.other.spec && + out.spec.containers === 5 && + out.spec.inputs === out.spec.rows && + out.spec.nameInputs === out.spec.rows && + out.spec.keysReadOnly && + !out.old.base && + out.old.blocks === 2 && + out.preview.title && + out.preview.item && + out.preview.choice && + !out.saveAfterEdit && + out.blank.issue && + out.blank.save && + out.restored && + out.판After === out.판; + } else { + const sec = $(".b05-structure-section"); + if (!sec) return JSON.stringify({ error: "구조물 배치 칸 없음" }); + const url = performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes("/B05_Profile/B05_Profile_Api_Structures.ts")) + .pop(); + const api = await import(url); + const pipe = (await api.fetchStructureTypes(true)).find((t) => t.type_id === "pipe"); + pipe.spec_names = { ...(pipe.spec_names ?? {}), ...NEW }; + await until(() => $$(".b05-route__irregular-list li").length); + const li = $$(".b05-route__irregular-list li").find((l) => + /배관 1$/.test(l.textContent.trim()), + ); + const spec = () => $(".b05-structure__spec", sec); + (li.querySelector("button") ?? li).click(); + await W(800); + if (!(spec() && spec().offsetParent !== null)) (li.querySelector("button") ?? li).click(); + await until(() => spec() && spec().offsetParent !== null); + const pipeBox = $('[data-spec-container="pipe"]', spec()); + out.named = { + title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe), + item: pipeBox + .querySelector('[data-spec-item="pipe.diameter"]') + .textContent.includes(NEW["pipe.diameter"]), + choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some( + (o) => o.textContent === NEW["pipe.diameter=800"], + ), + }; + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + out.ok = Object.values(out.named).every(Boolean); + } + return JSON.stringify(out); +})(); diff --git a/resources/tester/ui_checks/52-4_세월교_컨테이너.js b/resources/tester/ui_checks/52-4_세월교_컨테이너.js new file mode 100644 index 000000000..c5c9143e0 --- /dev/null +++ b/resources/tester/ui_checks/52-4_세월교_컨테이너.js @@ -0,0 +1,181 @@ +/* 52-4 — B05 · B06 좌측 「구조물 배치」 세월교 = 사용자 구조물 표 04 명세(컨테이너 셋) + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 목록에 「세월교 1」 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m524 = "단계";` 를 붙여 + * orca eval --expression "window.__m524='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠) + * (orca reload 로 다시 열기) + * orca eval --expression "window.__m524='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기 + * orca eval --expression "window.__m524='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움 + * 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀) + * 기대 수치(edit): 컨테이너 3(세월교 · 유입 구조 · 유출 구조 · 모두 필수) · 관경 800/1000/1200/1500 · + * 구조 없음/날개벽/흙막이 · 옛 관종 · 날개벽 칸 안 보임 · 폭 8 → 높이 = 필요 수심(설계유량 있으면) · + * 낮게 적으면 필요 수심으로 되돌림 · 수량 3 · 잇기 세월교 · 유입 지정 · 유출 지정 뒤 풂 + * 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) + */ +(async () => { + const MODE = window.__m524 || "edit"; + const NAME = /세월교 1$/; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts")); + const spec = () => + [...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null); + const box = (c) => spec().querySelector(`[data-spec-container="${c}"]`); + const ctl = (c, item) => + box(c)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input"); + const set = async (c, item, v) => { + const el = ctl(c, item); + el.value = v; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(250); + }; + const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text); + const pickFord = async () => { + const find = () => + [...document.querySelectorAll(".b05-route__irregular-list li")].find( + (l) => l.offsetParent !== null && NAME.test(l.textContent.trim()), + ); + await until(() => !!find()); + const li = find(); + (li.querySelector("button") ?? li).click(); + await w(800); + if (!spec()) (li.querySelector("button") ?? li).click(); + await until(() => !!spec()); + await w(500); + }; + const shape = () => + [...spec().querySelectorAll("[data-spec-container]")].map((b) => ({ + c: b.dataset.specContainer, + legend: b.querySelector("legend").firstChild.textContent, + items: [...b.querySelectorAll("[data-spec-item]")].map((n) => [ + n.dataset.specItem, + n.querySelector("select, input").value, + ]), + link: b.querySelector(".b05-structure__summary-name")?.textContent, + })); + const assigned = (c) => + box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned"); + const pickLink = async (c) => { + box(c).querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const visibleKeys = () => + [...document.querySelectorAll(".b05-drainage__facility [data-option-keys]")] + .filter((n) => n.offsetParent !== null) + .map((n) => n.dataset.optionKeys); + const out = { mode: MODE }; + + if (MODE === "edit") { + const before = await fetchApi.fetchDetailPipePoints(projectId); + sessionStorage.setItem("__524_original", JSON.stringify(before.pipe_points)); + await pickFord(); + const s0 = shape(); + out.containers = s0.map((b) => [b.c, b.legend]); + out.oldFieldsVisible = visibleKeys(); // 옛 관종 · 월류 · 날개벽 칸 — 빈 목록이어야 + out.diameters = labels("ford", "diameter"); + out.inletChoices = labels("inlet", "structure"); + out.outletChoices = labels("outlet", "structure"); + out.start = s0.map((b) => b.items); + await set("ford", "diameter", "1200"); + await set("ford", "width", "8"); + out.heightAfterWidth8 = ctl("ford", "height").value; + out.heightHint = ctl("ford", "height").placeholder; + await set("ford", "width", "4"); + out.heightAfterWidth4 = ctl("ford", "height").value; + await set("ford", "height", "0.01"); + out.heightLowBounced = ctl("ford", "height").value; + const raised = (Number(out.heightAfterWidth4 || 0) + 0.2).toFixed(2); + await set("ford", "height", raised); + out.heightRaisedKept = ctl("ford", "height").value === raised; + await set("ford", "count", "3"); + await set("inlet", "structure", "흙막이"); + await set("outlet", "structure", "날개벽"); + await pickLink("ford"); + await pickLink("inlet"); + await pickLink("outlet"); + out.linksPicked = ["ford", "inlet", "outlet"].map(assigned); + box("outlet").querySelector('[data-b05-summary="clear"]').click(); + await w(300); + out.linkOutletCleared = !assigned("outlet"); + out.edited = shape().map((b) => b.items); + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000); + const after = await fetchApi.fetchDetailPipePoints(projectId); + const p = after.pipe_points.find((x) => x.chainage_m > 170 && x.chainage_m < 176); // 세월교 1 = 8+13 + out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) }; + } + + if (MODE === "verify") { + await pickFord(); + const b05 = shape(); + out.reopened = b05.map((b) => b.items); + out.links = ["ford", "inlet", "outlet"].map(assigned); + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await pickFord(); + const b06 = shape(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + if (!out.sameDrawing) out.diff = { b05, b06 }; + out.b06WingSlots = ["inlet", "outlet"].map( + (c) => box(c).querySelector(".b05-structure__adjust-slot")?.childElementCount ?? -1, + ); + } + + if (MODE === "restore") { + const original = JSON.parse(sessionStorage.getItem("__524_original") || "null"); + if (!original) return JSON.stringify({ error: "원래 값 없음" }); + const keys = [ + "chainage_m", + "source", + "facility", + "start_m", + "end_m", + "options", + "summary_item", + "group_items", + ]; + const points = original.map((p) => + Object.fromEntries( + keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]), + ), + ); + await fetchApi.saveDetailPipePoints(projectId, points); + for (const k of Object.keys(sessionStorage)) + if (k.startsWith("aislo:draft:pipes:") || k === "__524_original") + sessionStorage.removeItem(k); + const now = await fetchApi.fetchDetailPipePoints(projectId); + const pick = (list) => + JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}])); + out.restored = pick(now.pipe_points) === pick(original); + } + return JSON.stringify(out); +})(); diff --git a/resources/tester/ui_checks/52-5_횡단수로_개거.js b/resources/tester/ui_checks/52-5_횡단수로_개거.js new file mode 100644 index 000000000..6745c6ed2 --- /dev/null +++ b/resources/tester/ui_checks/52-5_횡단수로_개거.js @@ -0,0 +1,158 @@ +/* 52-5 — B05 · B06 좌측 「구조물 배치」 노출형 횡단수로 · 개거(겉도랑) = 사용자 구조물 표 05 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 폼 비어 있음 · 로그인 상태 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m525 = "단계";` 를 붙여 + * orca eval --expression "window.__m525='edit'; $(cat 이 파일)" → 두 종류 모양 · 추가 · 잇기 · [저장] + * (orca reload 로 다시 열기) + * orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → 저장 값 그대로 · 다시 고름 + * (B06 으로 옮겨 — 같은 모양) + * orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → B06 에서도 같은 값 · 같은 모양 + * orca eval --expression "window.__m525='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장] + * 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움) + * 기대 수치: + * shape(종류마다) — optionInputs 0 · containers 0 · summaryRows 1 · summaryVisible true · summaryRightAfterPosition true · + * anchor true · stale false + * edit — added 둘 · 잇기 이름 값 있음 · 저장 뒤 목록 2 늘어남 + * verify — 같은 모양 · 잇기 이름 그대로 · 옵션 값 없음 + */ +(async () => { + const MODE = window.__m525 || "edit"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 15000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const vis = (n) => !!n && n.offsetParent !== null; + const sec = () => document.querySelector(".b05-structure-section"); + const body = () => sec().querySelector(".b05-route__panel-body"); + const selects = () => sec().querySelectorAll(".b05-structure__grid select"); + const fire = (el, value) => { + el.value = value; + el.dispatchEvent(new Event("change", { bubbles: true })); + }; + const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; + const NAMES = { cross_drain_exposed: "노출형 횡단수로", open_ditch: "개거(겉도랑)" }; + const AT = { cross_drain_exposed: [2, 7], open_ditch: [6, 3] }; + + const shape = (typeId) => { + const kids = [...body().children]; + const position = kids.find((c) => c.classList.contains("b05-structure__position-row")); + const summary = kids.find((c) => c.classList.contains("b05-structure__summary")); + const optionRow = kids.filter((c) => c.classList.contains("b05-structure__grid"))[1]; + const after = kids.slice(kids.indexOf(position) + 1).find((c) => vis(c)); + const anchorWrap = position.querySelectorAll(":scope > label")[2]; + return { + typeId, + optionInputs: vis(optionRow) ? optionRow.querySelectorAll("input, select").length : 0, + containers: [...sec().querySelectorAll("[data-spec-container]")].filter(vis).length, + summaryRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length, + summaryVisible: vis(summary), + summaryRightAfterPosition: after === summary, + anchor: vis(anchorWrap), + stale: vis(sec().querySelector(".b05-structure__stale")), + linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "", + }; + }; + const pickType = async (typeId) => { + const [group, type] = selects(); + fire(group, "A"); + await w(300); + fire(type, typeId); + await w(500); + }; + const linkFirstLeaf = async () => { + sec().querySelector("[data-b05-summary=pick]").click(); + await until(() => document.querySelector(".ui-item-picker")); + await w(600); + const modal = document.querySelector(".ui-item-picker"); + // 끝 항목 = ▸ · ▾ 로 안 시작하고 모달 조작 단추가 아닌 첫 단추 + const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"]; + const leaf = [...modal.querySelectorAll("button")].find((b) => { + const t = b.textContent.trim(); + return t && !/^[▸▾]/.test(t) && !ctl.includes(t); + }); + leaf?.click(); + await w(300); + [...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click(); + await w(500); + return !!leaf; + }; + const clickBtn = async (text) => { + [...document.querySelectorAll("button")] + .find((b) => b.textContent.trim() === text && vis(b)) + ?.click(); + await w(1500); + }; + const out = { mode: MODE, items: [] }; + + if (MODE === "edit") { + await until(() => listItems().length > 0); + out.before = listItems().length; + for (const typeId of Object.keys(NAMES)) { + await pickType(typeId); + const row = { ...shape(typeId), phase: "edit" }; + const [no, dist] = sec() + .querySelectorAll(".b05-structure__position-row > label")[2] + .querySelectorAll("input"); + fire(no, String(AT[typeId][0])); + fire(dist, String(AT[typeId][1])); + await w(200); + row.picked = await linkFirstLeaf(); + row.linked = shape(typeId).linked; + sec().querySelector(".is-primary").click(); // 추가 + await w(800); + out.items.push(row); + } + out.after = listItems().length; + await clickBtn("저장"); + out.listed = listItems() + .map((l) => l.textContent.trim()) + .filter((t) => /횡단수로|개거/.test(t)); + } else if (MODE === "verify") { + await until(() => listItems().length > 0); + for (const typeId of Object.keys(NAMES)) { + const li = listItems().find((l) => l.textContent.includes(NAMES[typeId])); + if (!li) { + out.items.push({ typeId, missing: true }); + continue; + } + (li.querySelector("button") ?? li).click(); + await w(800); + // B06 은 다른 항목이 골라져 있으면 첫 눌림이 풀기만 함 — 안 불러와졌으면 한 번 더 + if (selects()[1].value !== typeId) { + (li.querySelector("button") ?? li).click(); + await w(800); + } + out.items.push({ ...shape(typeId), phase: "verify", title: li.title || "" }); + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + await w(300); + } + } else if (MODE === "restore") { + await until(() => listItems().length > 0); + for (const typeId of Object.keys(NAMES)) { + const li = listItems().find((l) => l.textContent.includes(NAMES[typeId])); + if (!li) continue; + (li.querySelector("button") ?? li).click(); + await w(600); + await clickBtn("삭제"); + } + await clickBtn("저장"); + out.left = listItems().length; + } + const rows = out.items.filter((r) => !r.missing); + out.shapeOk = + rows.length === 2 && + rows.every( + (r) => + r.optionInputs === 0 && + r.containers === 0 && + r.summaryRows === 1 && + r.summaryVisible && + r.summaryRightAfterPosition && + r.anchor && + !r.stale, + ); + return JSON.stringify(out); +})(); diff --git a/resources/tester/ui_checks/52-6_횡단맹암거.js b/resources/tester/ui_checks/52-6_횡단맹암거.js new file mode 100644 index 000000000..3519947f5 --- /dev/null +++ b/resources/tester/ui_checks/52-6_횡단맹암거.js @@ -0,0 +1,217 @@ +/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」 + * 시작 화면: B05 종단설계(#/b05-profile) · 아무것도 안 고름 · 측점 3+7 이 비어 있음 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m526 = "단계";` 를 붙여 + * orca eval --expression "window.__m526='edit'; $(cat 이 파일)" → 종류 목록 · 컨테이너 · 값 · 잇기 · [추가] · 다시 열기 + * orca eval --expression "window.__m526='b06'; $(cat 이 파일)" → B06 에서 같은 항목 다시 열기 · B05 와 같은 그리기 + * orca eval --expression "window.__m526='save'; $(cat 이 파일)" → [저장] 요청 가로챔(몸통만 봄) + * orca eval --expression "window.__m526='restore'; $(cat 이 파일)" → sessionStorage 를 처음 그대로 · 뒤에 orca reload + * 쓰는 것: 없음 — [저장] 은 fetch 를 가로채 서버로 안 보냄(공유 storage) · 서버 저장 왕복은 test_52_6_cross_underdrain + * 기대 수치(edit): b05Types 에 cross_underdrain:맹암거 · underdrain 없음 · box 맹암거 · keys 높이 · 경사 · + * 잇기 줄 1(맹암거 상자 안만 · 종류 단위 줄 가림 · 52-6b) · pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음 + * 기대 수치(b06): b06Types A 에 맹암거 · B 에 종단배수 맹암거 따로 · sameDrawing · 값 같음 + * 기대 수치(save): saved = PUT 몸통에 같은 값(67 m · 0.6 · 2.5 · gcrossud) + */ +(async () => { + const MODE = window.__m526 || "edit"; + const KEY = "cross_underdrain"; + const GROUP = "gcrossud"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const vis = (n) => n && n.offsetParent !== null; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts")); + const sec = () => document.querySelector(".b05-structure-section"); + const sels = () => sec().querySelectorAll(".b05-structure__grid select"); + const choose = async (sel, value) => { + sel.value = value; + sel.dispatchEvent(new Event("change", { bubbles: true })); + await w(300); + }; + const typesOf = async (group) => { + await choose(sels()[0], group); + return [...sels()[1].options].filter((o) => o.value).map((o) => `${o.value}:${o.text}`); + }; + const box = () => sec().querySelector(`[data-b05-group="${GROUP}"]`); + const input = (k) => box()?.querySelector(`[data-option-keys="${k}"] input`); + const shape = () => { + const b = box(); + return { + legend: b?.querySelector("legend").textContent, + keys: [...(b?.querySelectorAll("[data-option-keys]") ?? [])].map( + (n) => `${n.dataset.optionKeys}|${n.querySelector("span").textContent}`, + ), + values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value), + link: b?.querySelector(".b05-structure__summary-name").textContent, + linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"), + linkRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length, + }; + }; + const item = () => + [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) => + /^3\+7.*맹암거/.test(l.textContent.trim()), + ); + const openItem = async () => { + await until(() => item()); + const li = item(); + (li.querySelector("button") ?? li).click(); + await w(600); + // 이미 고른 채로 열렸으면 누르면 풀림 — 풀렸으면 한 번 더(재선택 = 해제) + if (sels()[1].value !== KEY) (li.querySelector("button") ?? li).click(); + await until(() => vis(box())); + await w(300); + }; + const pickLink = async () => { + box().querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const pendingMine = () => + (api.readPendingStructures(projectId) ?? []).filter((s) => s.type_id === KEY); + const out = { mode: MODE }; + + if (MODE === "edit") { + sessionStorage.setItem("__526_session", JSON.stringify({ ...sessionStorage })); + out.b05Types = await typesOf("A"); + out.b05HasLongUnderdrain = (await typesOf("B")).some((t) => t.startsWith("underdrain:")); + await typesOf("A"); + await choose(sels()[1], KEY); + out.boxBeforeStation = { shown: vis(box()), locked: !!box()?.querySelector(".is-locked") }; + const pos = [...sec().querySelectorAll(".b05-structure__position-row input")].filter(vis); + for (const [el, v] of [ + [pos[0], "3"], + [pos[1], "7"], + ]) { + el.value = v; + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + } + await w(300); + for (const [k, v] of [ + ["culvert_height_m", "0.6"], + ["culvert_slope_pct", "2.5"], + ]) { + input(k).value = v; + input(k).dispatchEvent(new Event("input", { bubbles: true })); + input(k).dispatchEvent(new Event("change", { bubbles: true })); + } + await w(200); + await pickLink(); + out.form = shape(); + sec().querySelector(".is-primary").click(); // [추가] + await w(800); + const mine = pendingMine(); + out.pending = mine.map((s) => ({ + chainage_m: s.chainage_m, + options: s.options, + groups: Object.keys(s.group_items ?? {}), + })); + // 종단 알약 — 약자 · 색이 종단배수 맹암거(맹암거 · #1f7fb8)와 따로 + const types = await api.fetchStructureTypes(true); + const ids = new Set(mine.map((s) => s.structure_id)); + if (!document.querySelector(".b05-structure__mark")) { + [...document.querySelectorAll("button")] + .find((b) => b.textContent.trim() === "▲종단") + ?.click(); + await until(() => document.querySelector(".b05-structure__mark"), 8000); + } + out.marks = [...document.querySelectorAll(".b05-structure__mark")] + .filter((m) => ids.has(m.dataset.structureId)) + .map((m) => `${m.textContent}|${m.style.color}`); + out.longStyle = types.find((t) => t.type_id === "underdrain")?.style; + // 다시 열기 — 폼을 비우고 목록에서 다시 고름 + const li = item(); + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + await w(400); + await openItem(); + out.reopened = shape(); + out.reopenSame = JSON.stringify(out.reopened) === JSON.stringify(out.form); + } + + if (MODE === "save") { + // [저장] — 요청을 가로채 몸통만 봄(서버 · storage 안 감) · 뒤에 화면이 서버 목록을 다시 읽어 항목이 사라짐 + const real = window.fetch; + const sent = []; + window.fetch = async (url, init = {}) => { + const method = (init.method || "GET").toUpperCase(); + if (method !== "GET" && String(url).includes(`/projects/${projectId}/`)) { + sent.push({ url: String(url), method, body: init.body ? String(init.body) : "" }); + return new Response(JSON.stringify({ status: "success", revision: 0, count: 0 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return real(url, init); + }; + try { + document.querySelector('[data-dock-action="save"]').click(); + await w(3000); + await until(() => !document.querySelector(".ui-overlay.is-active"), 20000); + } finally { + window.fetch = real; + } + const put = sent.find((r) => r.url.includes("/route/structures") && r.method === "PUT"); + const body = put ? JSON.parse(put.body) : null; + const saved = body?.structures.filter((s) => s.type_id === KEY) ?? []; + out.saved = saved.map((s) => ({ + chainage_m: s.chainage_m, + options: s.options, + groups: Object.keys(s.group_items ?? {}), + })); + out.sent = sent.map((r) => `${r.method} ${r.url.split("/api/")[1] ?? r.url}`); + } + + if (MODE === "b06") { + await openItem(); + const b05 = shape(); + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await openItem(); + const b06 = shape(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + out.b06 = b06; + sels()[1].value = ""; + const li = item(); + (li.querySelector("button") ?? li).click(); // 고름 풂 + await w(300); + out.b06TypesA = await typesOf("A"); + out.b06TypesB = await typesOf("B"); + } + + if (MODE === "restore") { + const raw = sessionStorage.getItem("__526_session"); + if (!raw) return JSON.stringify({ error: "처음 sessionStorage 없음" }); + const before = JSON.parse(raw); + sessionStorage.clear(); + for (const [k, v] of Object.entries(before)) sessionStorage.setItem(k, v); + out.restored = JSON.stringify({ ...sessionStorage }) === JSON.stringify(before); + out.mine = pendingMine().length; + } + return JSON.stringify(out); +})(); diff --git a/resources/tester/ui_checks/52-8_종단_계류_숨김.js b/resources/tester/ui_checks/52-8_종단_계류_숨김.js new file mode 100644 index 000000000..d77b951a7 --- /dev/null +++ b/resources/tester/ui_checks/52-8_종단_계류_숨김.js @@ -0,0 +1,99 @@ +/* 52-8 — B05 · B06 좌측 「구조물 배치」 종단배수(B) · 계류사방(D) 고르기 목록에서 숨김 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub2_검증) + * 부르는 법: 앞에 `window.__m528 = "단계";` 를 붙여 + * orca eval --expression "window.__m528='seed'; $(cat 이 파일)" → 옛 값 둘(골막이 D · 측구 B)을 세션 초안에만 둠 + * (orca reload) + * orca eval --expression "window.__m528='verify'; $(cat 이 파일)" → 군 목록 · 우클릭 메뉴 · 옛 값 열기 (B05) + * (#/b06-section 으로 가서 reload 후 verify 한 번 더) + * orca eval --expression "window.__m528='clear'; $(cat 이 파일)" → 세션 초안 비움(서버 저장 안 함) + * 기대 수치(verify): visibleGroups 에 B · D 없음(0) · hiddenGroups 가림만 · menuHasBD 0 · + * 옛 값 목록 둘 그대로 · 옛 값을 열면 군 = D / B 로 열림(editGroup) · 잇기 칸 pickLinks 골막이(D) 1 · 측구(B) 0 + */ +(async () => { + const MODE = window.__m528 || "verify"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts")); + const out = { mode: MODE, hash: location.hash }; + if (MODE === "seed") { + const base = { + placement_source: "manual", + status: "draft", + revision: 0, + geometry: null, + memo: "", + options: {}, + }; + api.writePendingStructures(projectId, [ + { + ...base, + structure_id: "m528d", + type_id: "erosion_check", + placement: "point", + chainage_m: 40, + }, + { + ...base, + structure_id: "m528b", + type_id: "ditch_side", + placement: "interval", + start_m: 60, + end_m: 80, + }, + ]); + out.seeded = api.readPendingStructures(projectId).length; + return JSON.stringify(out); + } + if (MODE === "clear") { + api.writePendingStructures(projectId, null); + out.cleared = api.readPendingStructures(projectId) === null; + return JSON.stringify(out); + } + const groupSel = [...document.querySelectorAll("select")].find((s) => + [...s.options].some((o) => o.text.includes("A 횡단배수")), + ); + const isBD = (o) => /^(B|D) /.test(o.text); + out.visibleGroups = [...groupSel.options].filter((o) => !o.hidden && o.value).map((o) => o.text); + out.visibleBD = [...groupSel.options].filter((o) => !o.hidden && isBD(o)).length; + out.hiddenGroups = [...groupSel.options].filter((o) => o.hidden).map((o) => o.text); + // 우클릭 메뉴 목록(공용 만들기) — 종류 전부를 줘도 B · D 군이 안 나옴 + const menuMod = await import(modUrl("/B05_Profile/B05_Profile_UI_Profile_Structures.ts")); + const types = (await api.fetchStructureTypes(true)).map((t) => ({ + type_id: t.type_id, + group: t.group, + name: t.name, + })); + const menu = menuMod.structureGroupMenuItems(types, () => {}); + out.menuGroups = menu.map((m) => m.label); + out.menuHasBD = menu.filter((m) => /^(B|D) /.test(m.label)).length; + // 옛 값 — 목록에 그대로 · 열면 그 군으로 열림 + const rows = [...document.querySelectorAll(".b05-route__irregular-list li")]; + out.rows = rows.map((r) => r.textContent.trim().replace(/\s+/g, " ").slice(0, 40)); + const old = rows.find((r) => + (window.__m528row ? new RegExp(window.__m528row) : /골막이/).test(r.textContent), + ); + if (old) { + (old.querySelector("button") ?? old).click(); + await w(1500); + const sels = [...document.querySelectorAll("select")].filter((s) => + [...s.options].some((o) => o.text.includes("A 횡단배수")), + ); + // 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더 + if (!sels[0]?.value) { + (old.querySelector("button") ?? old).click(); + await w(1500); + } + out.editGroup = sels[0]?.value; + // 종단배수(B)는 조합 잇기 칸이 없음(0) · 계류사방(D)은 옛 그대로(1) + out.pickLinks = [...document.querySelectorAll('[data-b05-summary="pick"]')].filter( + (n) => n.offsetParent, + ).length; + } + return JSON.stringify(out); +})();