From 5a85e0b32137b52349704599770a71eea13c4c79 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:48:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(B05):=20=EC=95=88=EC=A0=84=EB=B6=80?= =?UTF-8?q?=EB=8C=80=EC=9A=A9=EC=A7=80=20=3D=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=20=ED=91=9C=2007=20=E2=80=94=20=EB=8C=80=ED=94=BC=EB=A1=9C=20?= =?UTF-8?q?=C2=B7=20=EA=B0=80=EB=93=9C=EB=A0=88=EC=9D=BC=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=C2=B7=20=EC=A0=90=ED=98=95=20?= =?UTF-8?q?=EC=9D=BC=EA=B3=B1=20(PLAN=2052-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정의 판 7(판 6 이력) — 대피로(evac_road · 점형 · 상세 = 대피소 · 정차/작업장 · 차돌림곳) · 경계석 · 위험표시 · 취수정 새 키 · 가드레일 = 형태(손 입력) · 길이 · 기준측점 전 · 후 · 대피로 · 가드레일 「상세 내용」 컨테이너(정의 그룹 · 잇기 1) - 반사경 방향 · 가드레일 옛 종류(kind)는 화면에서 빼고 지난 정의 값으로 남김 - 옛 대피소 · 정차·작업장 · 차돌림곳 = 고르기 목록에서 빼고 대피로 + 상세로 읽음 · 고치기 전까지 옛 종류 그대로 · 고쳐도 옛 칸 값 남김 - 가드레일 길이 = 전 + 후 = 배관 맞물림 solveSplit 그대로 - 컨테이너 종류(정의 그룹 있음)는 종류 단위 잇기 줄 가림 · 가린 시설 폼 칸이 그룹 상자로 딸려 오던 흠 막음 - 집계 알림 「가드레일·경계석·위험표지 종류 미정」 걷음 - 시험 test_52_7_safety · 정책 시험 B05 선택지 27 · ORCA ui_checks/52-7_안전부대용지 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../B05_Profile_UI_Structures_Define.ts | 22 + .../B05_Profile_UI_Structures_Groups.ts | 5 +- .../B05_Profile_UI_Structures_Panel.ts | 4 +- .../B05_Profile_UI_Structures_Panel_Commit.ts | 12 +- ...B05_Profile_UI_Structures_Panel_Options.ts | 35 +- M02_MasterTemplete/M02_Table_Fill.py | 5 - resources/master_template/구조물정의.json | 153 +- .../master_template/구조물정의_이력/판_6.json | 3360 +++++++++++++++++ resources/tester/test_52_7_safety.py | 101 + .../test_b05_structures_registry_policy.py | 6 +- .../tester/ui_checks/52-7_안전부대용지.js | 218 ++ 11 files changed, 3875 insertions(+), 46 deletions(-) create mode 100644 resources/master_template/구조물정의_이력/판_6.json create mode 100644 resources/tester/test_52_7_safety.py create mode 100644 resources/tester/ui_checks/52-7_안전부대용지.js diff --git a/B05_Profile/B05_Profile_UI_Structures_Define.ts b/B05_Profile/B05_Profile_UI_Structures_Define.ts index 4c5ecf599..b3ef3f123 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define.ts @@ -215,3 +215,25 @@ export function withDefinition< }; }); } + +/** 대피로(표 07 · PLAN 52-7) — 옛 대피 종류는 고르기 목록에서 빼고 대피로 + 상세로 읽음. + * 옛 기록은 고치기 전까지 옛 종류 그대로(도면 · 집계 그대로) · 고치면 대피로로 저장 · 옛 칸 값은 남음. */ +export const ROAD_TYPE = "evac_road"; +export const ROAD_DETAILS: Readonly> = { + refuge: "대피소", + work_yard: "정차/작업장", + turnaround: "차돌림곳", +}; + +/** 고르기 목록에 낼 종류인지 — 옛 대피 종류만 뺌. */ +export const pickable = (type: { type_id: string }): boolean => !ROAD_DETAILS[type.type_id]; + +/** 옛 대피 종류 기록 → 대피로 + 상세(옛 값 그대로 · 상세만 채움). 다른 기록은 그대로. */ +export function asRoad }>( + record: T, +): T { + const detail = ROAD_DETAILS[record.type_id]; + return detail + ? { ...record, type_id: ROAD_TYPE, options: { detail, ...record.options } } + : record; +} diff --git a/B05_Profile/B05_Profile_UI_Structures_Groups.ts b/B05_Profile/B05_Profile_UI_Structures_Groups.ts index f41119d99..63b1afc19 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Groups.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Groups.ts @@ -136,9 +136,12 @@ export function createGroupBoxes(): GroupBoxes { for (const scope of scopes) { const marked = [...scope.querySelectorAll("[data-option-keys]")]; // 지금 숨은 칸(다른 시설 몫 · 안 고른 갈래)은 그 자리에 둠 — 같은 키 칸이 두 벌 옮겨 오지 않게 + // 자리 자체가 가려졌으면(안 쓰는 시설 폼) 그 칸도 숨은 것 — 같은 키 칸이 딸려 오지 않게(52-7) const shown = (node: HTMLElement): boolean => { - for (let at: HTMLElement | null = node; at && at !== scope; at = at.parentElement) + for (let at: HTMLElement | null = node; at; at = at.parentElement) { if (at.hidden) return false; + if (at === scope) break; + } return true; }; const fits = marked.filter( diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 7f1c2ffe0..fc4d6de2a 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -42,7 +42,7 @@ import { loadPipeForm as loadPipeFormInto, } from "./B05_Profile_UI_Structures_Panel_Commit"; import { renderStructureList } from "./B05_Profile_UI_Structures_List"; -import { showsAnchor } from "./B05_Profile_UI_Structures_Define"; +import { pickable, showsAnchor } from "./B05_Profile_UI_Structures_Define"; import { hiddenOptionKeys } from "./B05_Profile_UI_Structures_Define_Form"; import { stationLabel as formatStation } from "../common_util/common_util_station"; import { PIPE_MATCH_M, resolveSelection } from "../A00_Common/design/design_selection"; @@ -344,7 +344,7 @@ export function createStructuresSection( * 초기·리셋 상태를 "안 고름"으로 드러낸다. */ function syncTypeOptions(keepTypeId?: string): void { const group = groupSelect.value; - const candidates = group ? types.filter((type) => type.group === group) : []; + const candidates = group ? types.filter((t) => t.group === group && pickable(t)) : []; typeSelect.replaceChildren( new Option("— 선택 —", ""), ...candidates.map((type) => new Option(type.name, type.type_id)), diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts index e24155203..3bf459a6a 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Commit.ts @@ -18,6 +18,7 @@ import type { FacilityAttributes, FacilityOptionsForm } from "./B05_Profile_UI_D import type { StationFields } from "./B05_Profile_UI_Structures_Fields"; import { splitPavementRange } from "./B05_Profile_UI_Structures_Pavement"; import { mergeStructureOptions } from "./B05_Profile_UI_Structures_Summary"; +import { asRoad } from "./B05_Profile_UI_Structures_Define"; import type { GroupBoxes } from "./B05_Profile_UI_Structures_Groups"; import { readGroupItems, @@ -95,16 +96,17 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance ctx.setPositionConfirmed(target !== null); const step = ctx.interval(); if (target) { - const type = ctx.typeMap().get(target.type_id); + const shown = asRoad(target); // 옛 대피 종류 = 대피로 + 상세(52-7) + const type = ctx.typeMap().get(shown.type_id); if (type) { ctx.groupSelect.value = type.group; - ctx.syncTypeOptions(target.type_id); + ctx.syncTypeOptions(shown.type_id); } ctx.anchorFields.write(structureAnchorM(target), step); ctx.startFields.write(target.start_m ?? null, step); ctx.endFields.write(target.end_m ?? null, step); ctx.memoField.value = target.memo ?? ""; - ctx.renderOptionFields(target.options); + ctx.renderOptionFields(shown.options); ctx.setSummary(readSummaryItem(target.summary_item)); ctx.groupBoxes.set(readGroupItems(target.group_items)); ctx.groupBoxes.setShapes(target.options); @@ -128,7 +130,7 @@ export function loadForm(ctx: StructuresCommitContext, target: StructureInstance afterEntry.input.value = after.toFixed(1); } } - ctx.syncFacilityForm(target.options); + ctx.syncFacilityForm(shown.options); } else { ctx.anchorFields.write(null, step); ctx.startFields.write(null, step); @@ -367,7 +369,7 @@ export async function commit(ctx: StructuresCommitContext, live = false): Promis ...previous, ...records[0], options: mergeStructureOptions( - previous, + asRoad(previous), // 옛 대피 종류를 대피로로 고쳐도 옛 칸 값은 남김(52-7) type.type_id, [...ctx.optionInputs.map((entry) => entry.key), ...ctx.groupBoxes.shapeKeys()], records[0].options, diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index 97032d397..cab419389 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -21,11 +21,19 @@ import { } from "./B05_Profile_UI_Structures_Define"; import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; -import { specKeys } from "../A00_Common/design/design_structure_spec"; +import { solveSplit, specKeys, type SplitRole } from "../A00_Common/design/design_structure_spec"; -/** 구조물 명세 컨테이너로 그리는 종류인지 — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1). */ +/** 컨테이너로 그리는 종류인지(명세 · 정의 그룹 상자) — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1 · 52-7). */ export const hasSpecContainers = (type: StructureType): boolean => - !!STRUCTURE_SPECS[type.type_id]?.containers.length; + !!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length; + +/** 길이 = 전 + 후 를 배관 맞물림(`solveSplit` · 마지막에 고친 둘을 지킴)으로 푸는 종류(표 07 가드레일). */ +const SOLVE_SPLIT = new Set(["guardrail"]); +const SPLIT_ROLE: Record = { + length_m: "length", + before_m: "before", + after_m: "after", +}; /** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */ export interface OptionRenderContext { @@ -127,12 +135,31 @@ export function renderOptionFields( "length" | "before_m" | "after_m"; const refresh = (normalize: boolean): void => { input.classList.remove("is-invalid"); - ctx.syncSplitInputs(source, normalize); + if (SOLVE_SPLIT.has(type!.type_id)) { + solveInto(option.key); // 타이핑 중에도 — change 의 실시간 반영이 푼 값을 읽게 + ctx.syncRangeDisplay(); + } else ctx.syncSplitInputs(source, normalize); }; input.addEventListener("input", () => refresh(false)); input.addEventListener("change", () => refresh(true)); } }); + /** 길이 · 전 · 후 맞물림 — 고친 차례(최근이 앞)를 쥐고 나머지 하나를 셈. */ + const recent: SplitRole[] = []; + function solveInto(key: string): void { + const role = SPLIT_ROLE[key]; + recent.splice(0, recent.length, role, ...recent.filter((r) => r !== role)); + const inputOf = (r: SplitRole) => + all.find(({ entry }) => SPLIT_ROLE[entry.key] === r)?.entry.input; + const values = Object.fromEntries( + (["length", "before", "after"] as SplitRole[]).map((r) => [r, inputOf(r)?.value ?? ""]), + ); + const solved = solveSplit(values, recent); + for (const r of ["length", "before", "after"] as SplitRole[]) { + const input = inputOf(r); + if (input) input.value = solved[r] ?? ""; + } + } /** 규칙 걸기 — `parent` 가 바뀐 칸이면 그 칸에 매인 기본값부터 넣음. */ function applyRules(parent: string | null): void { const now = (): Record => diff --git a/M02_MasterTemplete/M02_Table_Fill.py b/M02_MasterTemplete/M02_Table_Fill.py index a55dc4341..214c355c0 100644 --- a/M02_MasterTemplete/M02_Table_Fill.py +++ b/M02_MasterTemplete/M02_Table_Fill.py @@ -511,11 +511,6 @@ def fill_document( missing = sum(1 for item in items if item["type_id"] == "pipe" and not item.get("관연장")) if missing: notes.append(f"관 연장 없음 {missing}곳 — B06 횡단 설계 전이면 빈칸") - loose = sum( - 1 for item in items if item["type_id"] == "guardrail" and not item["options"].get("kind") - ) - if loose: - notes.append(f"가드레일·경계석·위험표지 종류 미정 {loose}건 — 표에 안 셈") # 손 값 지키기 — 같은 줄 id 의 손 열 · 전구간 줄 · 사용자가 더한 줄 hand = _hand_ids(columns) diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 5b2e2bf07..8c77cb1ae 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -1,5 +1,5 @@ { - "판": 6, + "판": 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`가 새 키로 옮긴다.", "구조물군": { @@ -2633,6 +2633,36 @@ } ] }, + { + "type_id": "evac_road", + "group": "E", + "name": "대피로", + "placement": "point", + "style": { + "color": "#2fa84f", + "abbr": "대피로" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "detail", + "label": "상세", + "input": "select", + "choices": [ + "대피소", + "정차/작업장", + "차돌림곳" + ], + "default": null, + "required": false, + "phase": "b05", + "empty_means": "비우면 상세 미정 — 대피로 자리만 잡음(대피소 · 정차/작업장 · 차돌림곳은 뒤에 고름)" + } + ] + }, { "type_id": "refuge", "group": "E", @@ -2732,26 +2762,40 @@ "drawing_views": [ "plan" ], - "options": [ - { - "key": "facing", - "label": "방향", - "input": "select", - "choices": [ - "좌", - "우", - "양면" - ], - "default": null, - "required": true, - "phase": "detail" - } - ] + "options": [] + }, + { + "type_id": "curb", + "group": "E", + "name": "경계석", + "placement": "point", + "style": { + "color": "#4fae5c", + "abbr": "경계석" + }, + "drawing_views": [ + "plan" + ], + "options": [] + }, + { + "type_id": "hazard_sign", + "group": "E", + "name": "위험표시", + "placement": "point", + "style": { + "color": "#5fb83a", + "abbr": "위험" + }, + "drawing_views": [ + "plan" + ], + "options": [] }, { "type_id": "guardrail", "group": "E", - "name": "가드레일·경계석·위험표지", + "name": "가드레일", "placement": "interval", "style": { "color": "#1f9e46", @@ -2764,17 +2808,35 @@ ], "options": [ { - "key": "kind", - "label": "종류", - "input": "select", - "choices": [ - "가드레일", - "경계석", - "위험표지" - ], + "key": "form", + "label": "형태", + "input": "text", "default": null, - "required": true, - "phase": "detail" + "required": false + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "empty_means": "비우면 범위를 못 셈 — 길이를 적어야 기준 측점 앞뒤로 구간이 섬" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": null, + "empty_means": "비우면 0 — 기준 측점이 곧 시작(길이 = 전 + 후 맞물림)" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": null, + "empty_means": "비우면 길이 − 전 으로 셈(길이 = 전 + 후 맞물림)" } ] }, @@ -2793,6 +2855,20 @@ ], "options": [] }, + { + "type_id": "water_intake", + "group": "E", + "name": "취수정", + "placement": "point", + "style": { + "color": "#3a9e78", + "abbr": "취수정" + }, + "drawing_views": [ + "plan" + ], + "options": [] + }, { "type_id": "barrier_gate", "group": "E", @@ -3352,6 +3428,29 @@ "옵션": [], "형상": {} } + ], + "evac_road": [ + { + "id": "gevacdetail", + "이름": "상세 내용", + "옵션": [ + "detail" + ], + "형상": {} + } + ], + "guardrail": [ + { + "id": "gguarddetail", + "이름": "상세 내용", + "옵션": [ + "form", + "length_m", + "before_m", + "after_m" + ], + "형상": {} + } ] }, "배치": {}, diff --git a/resources/master_template/구조물정의_이력/판_6.json b/resources/master_template/구조물정의_이력/판_6.json new file mode 100644 index 000000000..1be3c8ef2 --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_6.json @@ -0,0 +1,3360 @@ +{ + "판": 6, + "schema_version": 1, + "comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.", + "구조물군": { + "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/test_52_7_safety.py b/resources/tester/test_52_7_safety.py new file mode 100644 index 000000000..824382bef --- /dev/null +++ b/resources/tester/test_52_7_safety.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +"""안전부대용지(표 07) — PLAN 52-7. + +정의(대피로 · 가드레일 컨테이너 · 점형 일곱) · 옛 대피 종류 읽기(`asRoad` · `pickable`) · +화면은 ORCA `ui_checks/52-7_안전부대용지.js`. +""" + +import json +import os +import subprocess + +from B05_Profile.B05_Profile_Structures_Schema import structure_type_map +from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) +POINTS = [ + "mirror", + "curb", + "hazard_sign", + "position_sign", + "water_intake", + "completion_sign", + "barrier_gate", +] + + +def test_정의_검사_통과(): + assert definition.check(definition.read()) == [] + + +def test_컨테이너_없는_일곱은_점형_옵션_없음(): + types = structure_type_map() + for tid in POINTS: + t = types[tid] + assert (t.group, t.placement, t.options) == ("E", "point", []), tid + assert [types[t].name for t in ("curb", "hazard_sign", "water_intake")] == [ + "경계석", + "위험표시", + "취수정", + ] + + +def test_대피로_가드레일_상세_내용_컨테이너(): + doc = definition.read() + groups = doc["그룹"] + assert groups["evac_road"] == [ + {"id": "gevacdetail", "이름": "상세 내용", "옵션": ["detail"], "형상": {}} + ] + assert groups["guardrail"][0]["옵션"] == ["form", "length_m", "before_m", "after_m"] + types = structure_type_map() + road = types["evac_road"] + assert road.placement == "point" + assert road.options[0].choices == ["대피소", "정차/작업장", "차돌림곳"] + guard = types["guardrail"] + assert guard.name == "가드레일" and guard.placement == "interval" + assert [(o.key, o.input) for o in guard.options] == [ + ("form", "text"), + ("length_m", "number"), + ("before_m", "number"), + ("after_m", "number"), + ] + # 옛 대피 종류는 정의에 그대로(옛 기록 도면 · 집계) + assert {"refuge", "work_yard", "turnaround"} <= set(types) + + +NODE = r""" +const fs = require("fs"), path = require("path"); +const ts = require(path.join(process.argv[1], "config", "node_modules", "typescript")); +const src = fs.readFileSync( + path.join(process.argv[1], "B05_Profile", "B05_Profile_UI_Structures_Define.ts"), "utf8"); +const js = ts.transpileModule(src, { + compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText; +const m = { exports: {} }; +new Function("exports", "module", js)(m.exports, m); +const d = m.exports; +process.stdout.write(JSON.stringify({ + old: d.asRoad({ type_id: "turnaround", options: { width_m: 10 } }), + other: d.asRoad({ type_id: "mirror", options: { facing: "좌" } }), + pick: ["refuge", "work_yard", "turnaround", "evac_road", "guardrail"].map( + (type_id) => d.pickable({ type_id })), +})); +""" + + +def test_옛_대피_종류는_대피로_상세로_읽고_목록에서_뺌(): + proc = subprocess.run( + ["node", "-e", NODE, ROOT], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + out = json.loads(proc.stdout) + assert out["old"] == { + "type_id": "evac_road", + "options": {"detail": "차돌림곳", "width_m": 10}, + } + assert out["other"] == {"type_id": "mirror", "options": {"facing": "좌"}} + assert out["pick"] == [False, False, False, True, True] diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index 2f2c006c7..22f41b59e 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군 10 + G군 1(콘크리트 포장) + 기타 1. + """잔존 B05 선택지 = A군 2(노출형·개거) + C군 5 + D군 3 + E군 14 + G군 1(콘크리트 포장) + 기타 1. 콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로 받는 정본이 됐다(종단경사 자동 포장 적용을 대체). @@ -201,8 +201,10 @@ def test_all_enabled_non_managed_types_are_quick_addable(): 소단 규격은 법령·교본마다 갈려 프로그램이 판정하지 않고 **사용자가 구간에 놓는다** — 그래서 C군 구간형 시설과 같은 자리에 선다. (측구쪽 `ditch_berm` 은 우클릭 추가 대상이 아니라 여기 수에 안 잡힌다.) + + E군이 10에서 14로 늘었다 — 표 07(PLAN 52-7) 대피로 · 경계석 · 위험표시 · 취수정. """ - assert len(_quick_add_types()) == 23 + assert len(_quick_add_types()) == 27 def test_removed_types_are_absent(): diff --git a/resources/tester/ui_checks/52-7_안전부대용지.js b/resources/tester/ui_checks/52-7_안전부대용지.js new file mode 100644 index 000000000..bfe28c65b --- /dev/null +++ b/resources/tester/ui_checks/52-7_안전부대용지.js @@ -0,0 +1,218 @@ +/* 52-7 — B05 · B06 좌측 「구조물 배치」 안전부대용지 = 사용자 구조물 표 07 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m527 = "단계";` 를 붙여 + * seed → 원래 구조물을 sessionStorage 에 두고 옛 「대피소」 한 건(refuge · 너비 4 · 유효길이 15)을 더해 PUT + * (orca reload) + * edit → 종류 목록 · 일곱 종류 컨테이너 0 / 잇기 1 · 대피로 상세 3 · 옛 대피소 = 대피로 + 대피소 · + * 가드레일 길이 10 전 3 → 후 7 · 가드레일 · 대피로 [추가] → [저장] + * (orca reload) + * verify → 다시 연 값 · 옛 대피소 기록 그대로 · B05 = B06 같은 그리기 + * restore → 원래 구조물로 되돌림(PUT) · 초안 비움 + * 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 원래 목록으로 다시 씀) + */ +(async () => { + const MODE = window.__m527 || "edit"; + 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 api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts")); + const section = () => document.querySelector(".b05-structure-section"); + const fieldOf = (label) => + [...section().querySelectorAll(".b05-route__field")].find( + (f) => f.querySelector("span")?.textContent.trim() === label, + ); + const groupSelect = () => fieldOf("구조물군").querySelector("select"); + const typeSelect = () => fieldOf("종류").querySelector("select"); + const choose = async (el, value) => { + el.value = value; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(300); + }; + const pickType = async (typeId) => { + await choose(groupSelect(), "E"); + await choose(typeSelect(), typeId); + }; + const anchor = () => fieldOf("기준 측점")?.querySelectorAll("input"); + const place = async (station, plus) => { + const [s, r] = anchor(); + s.value = String(station); + s.dispatchEvent(new Event("change", { bubbles: true })); + r.value = String(plus); + r.dispatchEvent(new Event("change", { bubbles: true })); + await w(400); + }; + // 보이는 칸만 — 안 쓰는 시설 폼에도 같은 키 칸(포장 길이 등)이 숨어 있음 + const opt = (key) => + [...section().querySelectorAll(`[data-option-keys="${key}"]`)] + .find((n) => n.offsetParent !== null) + ?.querySelector("input, select"); + const setOpt = async (key, value) => { + const el = opt(key); + el.value = value; + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(300); + }; + const shown = (el) => !!el && el.offsetParent !== null; + // 종류 단위 잇기 = 기준 측점 아래 줄(컨테이너 밖) · 컨테이너 = 정의 그룹 상자 + const typeLinks = () => + [...section().querySelectorAll('[data-b05-summary="row"]')].filter( + (r) => !r.closest("[data-b05-group]") && shown(r), + ).length; + const containers = () => + [...section().querySelectorAll("[data-b05-group]")].filter(shown).map((box) => ({ + title: box.querySelector("legend").textContent, + keys: [...box.querySelectorAll("[data-option-keys]")].map((n) => n.dataset.optionKeys), + links: box.querySelectorAll('[data-b05-summary="row"]').length, + })); + const listItem = (name) => + [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) => + l.textContent.includes(name), + ); + const openItem = async (name) => { + await until(() => listItem(name)); + const li = listItem(name); + (li.querySelector("button") ?? li).click(); + await w(600); + if (!typeSelect().value) { + (listItem(name).querySelector("button") ?? listItem(name)).click(); // 재선택 = 해제였으면 다시 + await w(600); + } + }; + const stale = () => + [...section().querySelectorAll(".b05-structure__stale .b05-route__field")].map((f) => [ + f.querySelector("span").textContent, + f.querySelector("input").value, + ]); + const form = () => ({ + type: typeSelect().selectedOptions[0]?.text, + containers: containers(), + typeLinks: typeLinks(), + values: Object.fromEntries( + [...section().querySelectorAll("[data-option-keys]")] + .filter(shown) + .map((n) => [n.dataset.optionKeys, n.querySelector("input, select").value]), + ), + stale: stale(), + }); + const out = { mode: MODE }; + + if (MODE === "seed") { + const now = await api.fetchStructures(projectId); + sessionStorage.setItem("__527_original", JSON.stringify(now.structures)); + const legacy = { + structure_id: null, + type_id: "refuge", + placement: "interval", + chainage_m: 150, + start_m: 145, + end_m: 160, + options: { width_m: 4, length_m: 15 }, + memo: "52-7 옛 대피소", + placement_source: "manual", + status: "draft", + revision: 0, + geometry: null, + }; + await api.saveStructures(projectId, now.revision, [...now.structures, legacy]); + api.writePendingStructures(projectId, null); + const saved = await api.fetchStructures(projectId); + out.seeded = saved.structures.filter((s) => s.type_id === "refuge").length; + out.total = saved.structures.length; + } + + if (MODE === "edit") { + await until(() => section() && groupSelect().options.length > 1); + await choose(groupSelect(), "E"); + out.picker = [...typeSelect().options].filter((o) => o.value).map((o) => o.text); + // 컨테이너 없는 일곱 — 기준 측점 아래 잇기 1 · 컨테이너 0 + out.seven = {}; + for (const id of [ + "mirror", + "curb", + "hazard_sign", + "position_sign", + "water_intake", + "completion_sign", + "barrier_gate", + ]) { + await choose(typeSelect(), id); + out.seven[typeSelect().selectedOptions[0].text] = [containers().length, typeLinks()]; + } + // 대피로 — 상세 내용 컨테이너 · 상세 셋 + await pickType("evac_road"); + await place(7, 0); + out.road = form(); + out.roadDetails = [...opt("detail").options].filter((o) => o.value).map((o) => o.text); + await setOpt("detail", "정차/작업장"); + section().querySelector(".b05-route__irregular-btn.is-primary").click(); + await w(600); + // 가드레일 — 형태 · 길이 10 · 전 3 → 후 7 + await pickType("guardrail"); + await place(5, 0); + await setOpt("form", "W빔 가드레일"); + await setOpt("length_m", "10"); + await setOpt("before_m", "3"); + out.guard = form(); + out.range = section().querySelector(".b05-structure__range-value").textContent; + section().querySelector(".b05-route__irregular-btn.is-primary").click(); + await w(600); + // 옛 대피소 기록 = 대피로 + 상세 대피소 · 옛 칸 값은 지난 정의 값 + await openItem("대피소"); + out.legacy = form(); + section().querySelectorAll(".b05-route__irregular-btn")[2].click(); // [리셋] — 안 고치고 닫음 + await w(300); + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000); + const file = await api.fetchStructures(projectId); + out.file = file.structures + .filter((s) => ["evac_road", "guardrail", "refuge"].includes(s.type_id)) + .map((s) => [s.type_id, s.placement, s.chainage_m, s.start_m, s.end_m, s.options]); + } + + if (MODE === "verify") { + await openItem("가드레일"); + const b05 = form(); + out.guard = b05; + await openItem("대피로"); + out.road = form(); + await openItem("대피소"); + out.legacy = form(); + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await openItem("가드레일"); + const b06 = form(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + if (!out.sameDrawing) out.diff = { b05, b06 }; + location.hash = "#/b05-profile"; + await w(3000); + } + + if (MODE === "restore") { + const original = JSON.parse(sessionStorage.getItem("__527_original") || "null"); + if (!original) return JSON.stringify({ error: "원래 값 없음" }); + const now = await api.fetchStructures(projectId); + await api.saveStructures(projectId, now.revision, original); + api.writePendingStructures(projectId, null); + sessionStorage.removeItem("__527_original"); + const after = await api.fetchStructures(projectId); + const pick = (list) => JSON.stringify(list.map((s) => [s.type_id, s.chainage_m, s.options])); + out.restored = pick(after.structures) === pick(original); + out.count = after.structures.length; + } + return JSON.stringify(out); +})(); From aac85be021840b67f6ea0fcc15c3ad26c945d628 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:51:05 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore(plan):=2010-5=20=EB=B0=A4=20=EB=B2=94?= =?UTF-8?q?=EC=9C=84=20=EC=A7=80=EC=8B=9C=20=EC=9B=90=EB=AC=B8=20(?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- docs/raw/사용자요구/2026-10-05_수정요구.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index bebbfa64f..81999c1c9 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -119,3 +119,6 @@ 추가로 구조물 조합의 일위대가 상세페이지의 로직 테이블의 순서 열에서 존재하는 버튼을 2행 구조로 변경 단가 승격에도 옵션 버튼이있어야할것같고. 일위조합의 경우에는 바로 옵션으로 지정할수 없으니 수정버튼(모달)도 잇어할것 같은데? + +맹암거처럼 cad연결이나 횡단도에 그릴지 말지는 이번 수정 후에 작업할거야. 너는 B05/B06 좌측 패널의 생성과 기존 로직 연결에만 신경쓸것. +나는 이제 자러 갈거니 너가 다른 워크트리들을 잘 제어해서 계획서의 내용을 반영하고 최종검증을 진행 후 아티팩트로 결과를 보여주는데 만약 개선이 필요하다고 판단되는 부분이 있다면 계획서에 내용을 추가해서 개선할것. From 2471a14a6253c5eb79010ccfc81a22bc28cecf1a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:59:41 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EC=A0=95=EC=9D=98=20=3D=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20=EA=B7=B8=EB=A3=B9=20=EA=BC=B4=20=EC=A2=85=EB=A5=98?= =?UTF-8?q?=EB=8F=84=20=EC=9D=B4=EB=A6=84=20=EB=82=98=EB=AC=B4=20=C2=B7=20?= =?UTF-8?q?=EC=98=9B=20=ED=8E=B8=EC=A7=91=EA=B8=B0=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=EC=97=90=EC=84=9C=20=EC=88=A8=EA=B9=80(52-12?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명세 없는 종류 = 그룹 제목(그룹.이름) › 칸 이름(옵션 label) › 선택지 이름(종류.이름 `{옵션}={값}` · 값 그대로) · 그룹 밖 칸은 머리 줄 아래 - 칸 더하기 · 빼기 · 꼴 · 필수는 읽기만 · 칸 없는 종류(개거 · 노출형 횡단수로)는 「고칠 이름 없음」 · 종류 이름은 머리 칸 - 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 OLD_EDITOR 거짓으로 숨김(코드 그대로) - 미리보기 = B05 칸 부품(field · optionControl · 그룹 상자)으로 같은 차례 - B05 · B06 고르기 칸 보기 글 = 정의 이름 표(값 그대로 · 규칙 선택지 · 시설 폼 덧보기 포함) - 시험: test_m02_so_def_spec 정의 그룹 꼴 더함 · 헬퍼가 이웃 TS 를 읽게 고침 · ui_checks/52-11 에 맹암거 · 바닥막이 · 개거 더함 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s --- .../B05_Profile_UI_Structures_Define_Form.ts | 3 +- .../B05_Profile_UI_Structures_Fields.ts | 4 +- ...B05_Profile_UI_Structures_Panel_Options.ts | 17 ++- .../StructOptions/M02_SO_Def.ts | 9 +- .../StructOptions/M02_SO_Def_Spec.ts | 80 +++++++++-- .../StructOptions/M02_SO_Def_Spec_Model.ts | 74 +++++++++- .../StructOptions/M02_SO_Text.ts | 6 +- resources/tester/helper_m02_so_def_spec.cjs | 37 ++++- resources/tester/test_m02_so_def_spec.py | 16 +++ .../tester/ui_checks/52-11_정의_이름고치기.js | 127 +++++++++++++++++- 10 files changed, 335 insertions(+), 38 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts index 7f321904a..bce554803 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts @@ -48,7 +48,8 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition // 정의에 새로 든 선택지는 보기를 더하고, 정의 · 규칙 밖 보기는 가림(빈 보기 · 지금 값은 그대로) const have = new Set([...control.options].map((item) => item.value)); for (const choice of state.choices) - if (!have.has(choice)) control.add(new Option(choice, choice)); + if (!have.has(choice)) + control.add(new Option(type.spec_names?.[`${option.key}=${choice}`] ?? choice, choice)); for (const item of control.options) { item.hidden = !!item.value && !state.choices.includes(item.value) && item.value !== control.value; diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index ed31f4ce4..eaf354609 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -68,6 +68,8 @@ interface OptionShape { export function optionControl( option: OptionShape, stored: string | number | undefined, + /** 선택지 보일 글(정의 이름 표 · 52-12) — 값은 그대로 */ + label: (choice: string) => string = (c) => c, ): HTMLInputElement | HTMLSelectElement { const value = stored === undefined || stored === null ? "" : String(stored); // 툴팁 — 표가 아직 안 읽는 칸이면 그 사유가 먼저(넣어도 안 바뀜을 알아야 함), 아니면 비울 때의 뜻. @@ -78,7 +80,7 @@ export function optionControl( const kept = value && !option.choices.includes(value) ? [value] : []; const element = select([ ["", blank], - ...[...option.choices, ...kept].map((c) => [c, c] as [string, string]), + ...[...option.choices, ...kept].map((c) => [c, label(c)] as [string, string]), ]); element.value = value; if (tip) element.title = tip; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index af789a5dd..c30ad9f16 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -80,7 +80,8 @@ export function renderOptionFields( shown.forEach((option) => { // 받은 값만 칸에 — 새 구조물은 부르는 쪽(패널)이 등록부 기본값을 `values` 로 실어 줌(PLAN 46-7). // 보기는 정의 전부로 세워 옛 값을 잃지 않고, 숨김 · 규칙 선택지는 `applyRules` 가 좁힘. - const input = optionControl(option, values[option.key]); + const name = (c: string): string => type.spec_names?.[`${option.key}=${c}`] ?? c; + const input = optionControl(option, values[option.key], name); const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`; const wrap = field(label, input, option.default_basis); wrap.dataset.optionKeys = option.key; // 옵션 그룹 상자가 이 표식으로 칸을 옮김(PLAN 46-3) @@ -152,7 +153,11 @@ export function renderOptionFields( wrap.classList.toggle("is-rule-hidden", off); // 그룹 상자로 옮겨 가도 같은 표식 if (!off) optionInputs.push(entry); if (state && entry.input.tagName.toLowerCase() === "select") - syncChoices(entry.input as HTMLSelectElement, state.choices); + syncChoices( + entry.input as HTMLSelectElement, + state.choices, + (c) => type!.spec_names?.[`${entry.key}=${c}`] ?? c, + ); } } applyRules(null); @@ -161,14 +166,18 @@ export function renderOptionFields( } /** 고르기 칸 보기를 규칙 선택지로 — 빈 보기는 남기고, 지금 값이 목록 밖이면 그 값만 남겨 보임. */ -function syncChoices(element: HTMLSelectElement, choices: string[]): void { +function syncChoices( + element: HTMLSelectElement, + choices: string[], + label: (choice: string) => string, +): void { const now = [...element.children].slice(1).map((o) => (o as HTMLOptionElement).value); const keep = element.value && !choices.includes(element.value) ? [element.value] : []; const want = [...choices, ...keep]; if (now.length === want.length && now.every((v, i) => v === want[i])) return; const value = element.value; const blank = element.children[0]; - element.replaceChildren(blank, ...want.map((c) => new Option(c, c))); + element.replaceChildren(blank, ...want.map((c) => new Option(label(c), c))); element.value = value; } diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index d16428ac1..1529bacc3 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -32,6 +32,9 @@ import { checkBox, textBox } from "./M02_SO_Def_Ui"; import { buildGroupBlock } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; +/** 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) — 모든 종류에서 숨김(52-12 · 정의 화면 = 이름만) */ +const OLD_EDITOR = false; + export interface DefScreenOptions { /** 시스템 관리자 — 아니면 읽기만 */ isAdmin: boolean; @@ -184,7 +187,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { } const edit = canEdit(); const extra = (): NonNullable => (d.종류[type.type_id] ??= {}); - // 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김 + // 이름만 고침(PLAN 52-11 · 52-12) — 명세 종류 = 명세 나무 · 나머지 = 정의 그룹 나무 const spec = STRUCTURE_SPECS[type.type_id]; detail.replaceChildren( el("div", { @@ -200,7 +203,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { edit, attr: "data-sod-kind-off", }), - ...(spec + ...(!OLD_EDITOR ? [] : [ checkBox( @@ -221,7 +224,7 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { }), ], }), - ...(spec + ...(!OLD_EDITOR ? [buildSpecBlock({ def: d, type, spec, edit, changed })] : [ buildOptionsBlock({ def: d, type, edit, open, changed }), diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts index 63157ee50..1728f1662 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts @@ -3,14 +3,23 @@ * 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴 * 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 · * 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음. + * 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목 › 칸 이름 › 선택지 이름 · 미리보기 = B05 칸 부품 + * (`field` · `optionControl` · 그룹 상자)으로 같은 차례. * 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침. * ========================================================================== */ 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 { group } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields"; +import { field, optionControl } from "../../B05_Profile/B05_Profile_UI_Structures_Fields"; import type { StructDef, TypeDef } from "./M02_SO_Def_Model"; -import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model"; +import { + groupNameRows, + 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"; @@ -20,47 +29,90 @@ const TAG: Record, OptionsTextKey> = { select: "FormSelect", number: "FormNumber", auto: "FormAuto", + text: "FormText", + bool: "FormBool", }; +/** 정의 그룹 꼴 미리보기 — 그룹 밖 칸 · 그룹 상자(제목 = 그룹 이름) · 선택지 = 이름 표 글(값 그대로). */ +function groupPreview(def: StructDef, type: TypeDef): HTMLElement { + const names = def.종류[type.type_id]?.이름 ?? {}; + const options = type.options.filter((o) => o.enabled !== false); + const cell = (o: (typeof options)[number]): HTMLElement => + field( + o.unit ? `${o.label} (${o.unit})` : o.label, + optionControl( + { + input: o.input === "select" || o.input === "number" ? o.input : "text", + choices: o.choices ?? [], + default: null, + required: !!o.required, + }, + undefined, + (c) => names[`${o.key}=${c}`] ?? c, + ), + ); + const groups = def.그룹[type.type_id] ?? []; + const inGroup = new Set(groups.flatMap((g) => g.옵션)); + const grid = (cells: HTMLElement[]): HTMLElement => + el("div", { className: "b05-structure__grid", children: cells }); + return el("div", { + children: [ + grid(options.filter((o) => !inGroup.has(o.key)).map(cell)), + ...groups.map((g) => { + const box = group(g.이름); + box.body.append(grid(options.filter((o) => g.옵션.includes(o.key)).map(cell))); + return box.root; + }), + ], + }); +} + export function buildSpecBlock(opt: { def: StructDef; type: TypeDef; - spec: StructureSpec; + /** 없으면 정의 그룹 꼴(52-12) */ + spec: StructureSpec | undefined; edit: boolean; changed: () => void; }): HTMLElement { const { def, type, spec, edit, changed } = opt; - const rows = specNameRows(def, spec).map((row) => + const rows = (spec ? specNameRows(def, spec) : groupNameRows(def, type)).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", - }), + row.nameKey + ? textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { + edit, + attr: "data-sod-spec-name", + cls: "m02-sod__name", + }) + : el("span", { text: to("SpecLoose") }), 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; + let preview: HTMLElement; + if (spec) { + const form = createSpecForm(null); + const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름])); + form.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups); + preview = form.root; + } else preview = groupPreview(def, type); + preview.inert = true; return el("div", { className: "m02-sod__block", attrs: { "data-sod-spec-block": "" }, children: [ ...head(to("SpecHead"), to("SpecNote")), - ...rows, + ...(rows.length ? rows : [el("p", { className: "m02-side__empty", text: to("SpecNone") })]), ...head(to("SpecPreview")), el("div", { className: "m02-sod__specpreview", attrs: { "data-sod-spec-preview": "" }, - children: [preview.root], + children: [preview], }), ], }); diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts index bd6bc693b..52634ecbc 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts @@ -6,25 +6,29 @@ * · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`) * · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기 * (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀 + * · 명세 없는 종류(정의 그룹 꼴 · 52-12) = 그룹 제목(`그룹.이름`) › 칸 이름(옵션 `label`) › + * 선택지 이름(이름 표 키 `{옵션}={값}` · 값은 그대로) — 그룹 밖 칸은 맨 앞 머리 줄 아래 * import 는 꼴만 — 시험이 이 파일만 옮겨 돌림. * ========================================================================== */ import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; -import type { StructDef } from "./M02_SO_Def_Model"; +import type { StructDef, TypeDef } 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; + tag: "required" | "optional" | "select" | "number" | "auto" | "text" | "bool" | null; /** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */ group: string | null; + /** 정의 옵션 이름(`label`)에서 옴 — 고치면 그 옵션 이름에 씀 */ + option: string | null; } /** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */ @@ -48,6 +52,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[c.id] ?? group?.이름 ?? c.title, tag: c.required ? "required" : "optional", group: group ? group.id : null, + option: null, }); for (const item of c.items) { const itemKey = `${c.id}.${item.id}`; @@ -58,6 +63,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[itemKey] ?? item.label, tag: item.kind, group: null, + option: null, }); const choices = [ ...(item.choices ?? []), @@ -72,6 +78,7 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] label: names[choiceKey] ?? ch.label, tag: null, group: null, + option: null, }); } } @@ -79,8 +86,67 @@ export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] return rows; } -/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */ +/** 정의 그룹 꼴 종류(명세 없음) 이름 줄 — 그룹 밖 칸 · 그룹마다 제목 › 칸 › 선택지(B05 그리기 차례). */ +export function groupNameRows(def: StructDef, type: TypeDef): SpecNameRow[] { + const names = def.종류[type.type_id]?.이름 ?? {}; + const options = type.options.filter((o) => o.enabled !== false); + const groups = def.그룹[type.type_id] ?? []; + const inGroup = new Set(groups.flatMap((g) => g.옵션)); + const rows: SpecNameRow[] = []; + const optionRows = (keys: readonly string[]): void => { + for (const o of options.filter((x) => keys.includes(x.key))) { + rows.push({ + level: 1, + nameKey: o.key, + key: o.key, + label: o.label, + tag: o.input, + group: null, + option: o.key, + }); + for (const ch of o.input === "select" ? (o.choices ?? []) : []) { + const nameKey = `${o.key}=${ch}`; + rows.push({ + level: 2, + nameKey, + key: ch, + label: names[nameKey] ?? ch, + tag: null, + group: null, + option: null, + }); + } + } + }; + const loose = options.filter((o) => !inGroup.has(o.key)).map((o) => o.key); + if (loose.length) { + rows.push({ level: 0, nameKey: "", key: "", label: "", tag: null, group: null, option: null }); + optionRows(loose); + } + for (const g of groups) { + rows.push({ + level: 0, + nameKey: `#${g.id}`, + key: g.id, + label: g.이름, + tag: null, + group: g.id, + option: null, + }); + optionRows(g.옵션); + } + return rows; +} + +/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 옵션 칸은 옵션 이름 · 나머지는 이름 표. */ export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void { + const option = row.option + ? def.types.find((t) => t.type_id === typeId)?.options.find((o) => o.key === row.option) + : undefined; + if (option) { + option.label = value; + return; + } const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined; if (group) { group.이름 = value; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 3235e7762..a2c9f8c46 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -48,9 +48,11 @@ const TEXT = { FormAuto: ["자동", "Auto"], SpecHead: ["구조물 명세 이름", "Structure spec names"], SpecNote: [ - "컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림", - "Rename containers › items › values only — keys, required/optional, form and structure come from the spec", + "제목 › 칸 이름 › 선택지 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 칸 더하기 · 빼기는 읽기만 · [저장]하면 B05 · B06 이 이 이름으로 그림", + "Rename titles › fields › choices only — keys, required/optional, form and adding/removing fields are read-only", ], + SpecLoose: ["그룹 밖 칸", "Fields outside groups"], + SpecNone: ["고칠 이름 없음 — 칸이 없는 종류", "No names to edit — this kind has no fields"], SpecRequired: ["필수", "Required"], SpecOptional: ["선택", "Optional"], SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"], diff --git a/resources/tester/helper_m02_so_def_spec.cjs b/resources/tester/helper_m02_so_def_spec.cjs index ede10f95b..6634b447e 100644 --- a/resources/tester/helper_m02_so_def_spec.cjs +++ b/resources/tester/helper_m02_so_def_spec.cjs @@ -6,12 +6,20 @@ const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); +// 이웃 TS(`./…`)는 같은 길로 읽음 — 명세 파일이 서로 import 함 +const cache = {}; const load = (...parts) => { - const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), { + const file = path.join(ROOT, ...parts); + if (cache[file]) return cache[file].exports; + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; - const box = { exports: {} }; - new Function("exports", "module", "require", js)(box.exports, box, require); + const box = (cache[file] = { exports: {} }); + const near = (id) => + id.startsWith(".") + ? load(path.relative(ROOT, path.resolve(path.dirname(file), `${id}.ts`))) + : require(id); + new Function("exports", "module", "require", js)(box.exports, box, near); return box.exports; }; const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts"); @@ -61,4 +69,27 @@ m.setSpecName( ); out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름; +// 정의 그룹 꼴(52-12) — 맹암거 = 그룹 제목 › 칸 · 옆도랑 = 그룹 밖 칸 › 선택지 +const g = real(); +const ud = g.types.find((t) => t.type_id === "cross_underdrain"); +const udRows = m.groupNameRows(g, ud); +out.udRows = udRows.map((r) => [r.level, r.nameKey, r.label]); +m.setSpecName(g, ud.type_id, udRows[0], "맹암거 본체"); +m.setSpecName(g, ud.type_id, udRows[1], "암거 높이 칸"); +out.udGroup = g.그룹.cross_underdrain[0].이름; +out.udLabel = ud.options[0].label; +const side = g.types.find((t) => t.type_id === "ditch_side"); +const sideRows = m.groupNameRows(g, side); +out.sideHead = sideRows[0]; +const lshape = sideRows.find((r) => r.nameKey === "form=L형"); +out.sideChoice = lshape && [lshape.level, lshape.label]; +m.setSpecName(g, side.type_id, lshape, "L 모양"); +out.sideName = g.종류.ditch_side.이름["form=L형"]; +out.sideChoices = side.options.find((o) => o.key === "form").choices; +out.sideReread = m.groupNameRows(g, side).find((r) => r.nameKey === "form=L형").label; +out.bareKind = m.groupNameRows( + g, + g.types.find((t) => t.type_id === "open_ditch"), +).length; + process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_so_def_spec.py b/resources/tester/test_m02_so_def_spec.py index 3677816ff..e575dad1a 100644 --- a/resources/tester/test_m02_so_def_spec.py +++ b/resources/tester/test_m02_so_def_spec.py @@ -8,6 +8,8 @@ pytest 는 그 결과(JSON)를 판정. - 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀. - 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음. - 이름 표가 빈 종류는 명세 글로 읽음. + - 정의 그룹 꼴(52-12): 그룹 제목 › 칸 › 선택지 · 제목 = 그룹 이름 · 칸 = 옵션 이름 · + 선택지 = 이름 표(값 그대로) · 칸 없는 종류는 줄 없음. """ from __future__ import annotations @@ -56,3 +58,17 @@ def test_구조는_그대로(r) -> None: def test_이름_표_없는_종류(r) -> None: assert r["bareLabel"] == "흙막이" assert r["bareGroup"] == "유입" + + +def test_정의_그룹_꼴(r) -> None: + assert r["udRows"] == [ + [0, "#gcrossud", "맹암거"], + [1, "culvert_height_m", "암거 높이"], + [1, "culvert_slope_pct", "암거 경사"], + ] + assert r["udGroup"] == "맹암거 본체" and r["udLabel"] == "암거 높이 칸" + assert r["sideHead"]["nameKey"] == "" and r["sideHead"]["level"] == 0 + assert r["sideChoice"] == [2, "L형"] + assert r["sideName"] == "L 모양" and r["sideReread"] == "L 모양" + assert "L형" in r["sideChoices"] + assert r["bareKind"] == 0 diff --git a/resources/tester/ui_checks/52-11_정의_이름고치기.js b/resources/tester/ui_checks/52-11_정의_이름고치기.js index 6d4679afe..8cffe5b2b 100644 --- a/resources/tester/ui_checks/52-11_정의_이름고치기.js +++ b/resources/tester/ui_checks/52-11_정의_이름고치기.js @@ -1,4 +1,5 @@ /* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림 + * 52-12 — 정의 그룹 꼴 종류(맹암거 · 개거 · 바닥막이 …)도 같은 이름 나무 · 옛 편집기는 모든 종류에서 숨김 * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여 * M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자: * orca eval --expression "window.__m5211='m02'; $(cat 이 파일)" @@ -7,11 +8,14 @@ * 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 · * B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림 * 기대 수치(m02): - * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기 + * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류도 이름 나무 + * - 맹암거 = 그룹 제목 › 칸 2 · 고치면 미리보기 그룹 상자 제목 · 칸 이름 따라감 + * - 바닥막이 = 선택지 이름 고치면 미리보기 보기 글 따라감(값 그대로) · 개거 = 고칠 이름 없음 · 종류 이름만(왼쪽 목록 따라감) * - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님) * - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐 * - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로 * 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true + * group.* = 맹암거 그룹 제목 · 칸 이름 · 바닥막이 선택지 글이 새 구조물 폼에 그대로(값 그대로) */ (async () => { const MODE = window.__m5211 || "m02"; @@ -44,10 +48,74 @@ row.click(); if (!(await until(() => $$("[data-sod-kind]").length))) return JSON.stringify({ error: "종류 목록 없음" }); - // 명세 없는 종류 — 옛 편집기 그대로 + // 명세 없는 종류 — 옛 편집기 숨김 · 이름 나무(52-12) $('[data-sod-kind="retaining_wall"]').click(); await W(200); - out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") }; + out.other = { + base: !!$("[data-sod-base]"), + spec: !!$("[data-sod-spec-block]"), + blocks: $("[data-sod-detail]").children.length, + }; + const nameBox = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`); + const groupPreview = () => $("[data-sod-spec-preview]"); + // 맹암거 — 그룹 제목 › 칸 + $('[data-sod-kind="cross_underdrain"]').click(); + await W(200); + { + const keys = $$("[data-sod-spec]").map((r) => r.dataset.sodSpec); + const g0 = nameBox("#gcrossud").value; + const o0 = nameBox("culvert_height_m").value; + put(nameBox("#gcrossud"), "맹암거X"); + await W(150); + put(nameBox("culvert_height_m"), "암거 높이X"); + await W(150); + const pv = groupPreview(); + out.ud = { + keys, + legend: [...pv.querySelectorAll("legend")].some((l) => l.textContent === "맹암거X"), + label: pv.textContent.includes("암거 높이X (m)"), + old: !!$("[data-sod-option]") || !!$("[data-sod-base]"), + }; + put(nameBox("#gcrossud"), g0); + await W(150); + put(nameBox("culvert_height_m"), o0); + await W(150); + out.ud.restored = + nameBox("#gcrossud").value === g0 && nameBox("culvert_height_m").value === o0; + } + // 바닥막이 — 선택지 이름(값 그대로) + $('[data-sod-kind="bed_sill"]').click(); + await W(200); + { + const k = "form=돌붙임(메)"; + const c0 = nameBox(k).value; + put(nameBox(k), "돌붙임 메X"); + await W(150); + const opt = [...groupPreview().querySelectorAll("option")].find( + (o) => o.textContent === "돌붙임 메X", + ); + out.bed = { choice: !!opt, value: opt?.value }; + put(nameBox(k), c0); + await W(150); + out.bed.restored = nameBox(k).value === c0; + } + // 개거 — 칸 없음 · 종류 이름만 + $('[data-sod-kind="open_ditch"]').click(); + await W(200); + { + const kind = $("[data-sod-kind-name]"); + const n0 = kind.value; + out.ditch = { + rows: $$("[data-sod-spec]").length, + none: !!$("[data-sod-spec-block] p.m02-side__empty"), + }; + put(kind, "개거X"); + await W(200); + out.ditch.list = $('[data-sod-kind="open_ditch"]').textContent.includes("개거X"); + put($("[data-sod-kind-name]"), n0); + await W(200); + out.ditch.restored = $('[data-sod-kind="open_ditch"]').textContent.includes(n0); + } // 배관 — 명세 이름 나무 $('[data-sod-kind="pipe"]').click(); await W(200); @@ -102,8 +170,21 @@ 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.other.base && + out.other.spec && + out.other.blocks === 2 && + out.ud.keys.join() === "#gcrossud,culvert_height_m,culvert_slope_pct" && + out.ud.legend && + out.ud.label && + !out.ud.old && + out.ud.restored && + out.bed.choice && + out.bed.value === "돌붙임(메)" && + out.bed.restored && + out.ditch.rows === 0 && + out.ditch.none && + out.ditch.list && + out.ditch.restored && out.spec.containers === 5 && out.spec.inputs === out.spec.rows && out.spec.nameInputs === out.spec.rows && @@ -149,7 +230,41 @@ ), }; (li.querySelector("button") ?? li).click(); // 재선택 = 해제 - out.ok = Object.values(out.named).every(Boolean); + await W(500); + // 정의 그룹 꼴(52-12) — 받아 둔 종류 객체만 고치고 새 구조물 폼(군 · 종류 고르기 · 놓지 않음)으로 봄 + const types = await api.fetchStructureTypes(true); + const ud = types.find((t) => t.type_id === "cross_underdrain"); + ud.groups[0].name = "맹암거X"; + ud.options[0].label = "암거 높이X"; + const bed = types.find((t) => t.type_id === "bed_sill"); + bed.spec_names = { ...(bed.spec_names ?? {}), "form=돌붙임(메)": "돌붙임 메X" }; + const count = $$(".b05-route__irregular-list li").length; + const [famSel, kindSel] = $$("select", sec); + const choose = async (fam, kind) => { + famSel.value = fam; + famSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(300); + kindSel.value = kind; + kindSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(500); + }; + await choose("A", "cross_underdrain"); + out.group = { + legend: $$("legend", sec).some( + (l) => l.offsetParent !== null && l.textContent.includes("맹암거X"), + ), + label: $$(".b05-route__field > span", sec).some( + (n) => n.offsetParent !== null && n.textContent === "암거 높이X (m)", + ), + }; + await choose("D", "bed_sill"); + const opt = $$("option", sec).find((o) => o.textContent === "돌붙임 메X"); + out.group.choice = !!opt && opt.value === "돌붙임(메)"; + famSel.value = ""; + famSel.dispatchEvent(new Event("change", { bubbles: true })); + await W(300); + out.group.noWrite = $$(".b05-route__irregular-list li").length === count; + out.ok = Object.values(out.named).every(Boolean) && Object.values(out.group).every(Boolean); } return JSON.stringify(out); })(); From b02cd0bb992b56c3bfb6dd67ff31d1116e362744 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 6 Oct 2026 00:05:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test(m02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EC=8B=9C=ED=97=98=EC=9D=98=20=ED=8C=90=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=20=EA=B3=A0=EC=A0=95=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=E2=80=94=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=A9=B4=20=ED=8C=90=20?= =?UTF-8?q?+=201=20=C2=B7=20=EC=9D=B4=EB=A0=A5=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EA=B9=80=20=EA=BC=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 판 1 = 옛 레지스트리 검사는 이력 판_1 로 · 지금 정의는 검사 통과 · 종류·옵션 수 일치 - 쓰기 시험은 사본 정의의 지금 판 기준 · 진짜 구조물정의.json 안 씀 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- .../tester/test_m02_structure_definition.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/resources/tester/test_m02_structure_definition.py b/resources/tester/test_m02_structure_definition.py index d4df44197..675dd2af3 100644 --- a/resources/tester/test_m02_structure_definition.py +++ b/resources/tester/test_m02_structure_definition.py @@ -3,7 +3,7 @@ 못박는 것 - 판 1 = 옛 레지스트리 그대로 옮김(37종 · 옵션 235 · 키 그대로) · 검사 통과. - 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자만(403). - - 저장마다 판 +1 · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀). + - 저장마다 판 +1(지금 판에서) · 앞 판은 `구조물정의_이력/판_N.json` · 낡은 판 409 · 틀린 정의 422(안 씀). - 검사 = 키 겹침 · 꼴 · 규칙 대상 있음 · 그룹 형상. - 저장한 정의를 `/structure-types` 가 바로 읽음 · 프로젝트 저장에 정의 판 · 없는 키 값 보존. """ @@ -49,12 +49,16 @@ def _client(session: dict) -> TestClient: def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None: - doc = definition.read() + doc = definition.read_version(1) assert doc["판"] == 1 and len(doc["types"]) == 37 assert sum(len(t["options"]) for t in doc["types"]) == 235 - assert len(load_structure_types()) == 37 - assert sum(len(t.options) for t in load_structure_types()) == 235 assert definition.check(doc) == [] + now = definition.read() + assert now["판"] >= 1 and definition.check(now) == [] + assert len(load_structure_types()) == len(now["types"]) + assert sum(len(t.options) for t in load_structure_types()) == sum( + len(t["options"]) for t in now["types"] + ) assert [g["이름"] for g in doc["그룹"]["pipe"]] == ["관", "관 보호공(유입)", "관 보호공(유출)"] assert {t["group"] for t in doc["types"]} <= set(doc["구조물군"]) @@ -62,26 +66,28 @@ def test_판1_은_옛_레지스트리_그대로_검사_통과() -> None: def test_읽기는_누구나_쓰기는_시스템_관리자만(copy) -> None: user = _client(USER) got = user.get(URL) - assert got.status_code == 200 and got.json()["판"] == 1 - assert user.put(URL, json={"판": 1, "정의": got.json()}).status_code == 403 - assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 안 씀 + v = json.loads(copy.read_text("utf-8"))["판"] + assert got.status_code == 200 and got.json()["판"] == v + assert user.put(URL, json={"판": v, "정의": got.json()}).status_code == 403 + assert json.loads(copy.read_text("utf-8"))["판"] == v # 안 씀 def test_저장마다_판_더하기_1_앞_판은_이력_structure_types_가_바로_읽음(copy) -> None: admin = _client(ADMIN) doc = admin.get(URL).json() + v = doc["판"] tid = doc["types"][0]["type_id"] doc["types"][0]["name"] = "이름 바꿈" - res = admin.put(URL, json={"판": 1, "정의": doc}) + res = admin.put(URL, json={"판": v, "정의": doc}) assert res.status_code == 200, res.text - assert res.json()["판"] == 2 - old = json.loads((copy.parent / "구조물정의_이력" / "판_1.json").read_text("utf-8")) - assert old["판"] == 1 and old["types"][0]["name"] != "이름 바꿈" + assert res.json()["판"] == v + 1 + old = json.loads((copy.parent / "구조물정의_이력" / f"판_{v}.json").read_text("utf-8")) + assert old["판"] == v and old["types"][0]["name"] != "이름 바꿈" seen = {t["type_id"]: t for t in admin.get(TYPES).json()["types"]} - assert seen[tid]["name"] == "이름 바꿈" and len(seen) == 37 - assert admin.put(URL, json={"판": 1, "정의": doc}).status_code == 409 # 낡은 판 - assert admin.put(URL, json={"판": 2, "정의": res.json()}).json()["판"] == 3 - assert (copy.parent / "구조물정의_이력" / "판_2.json").is_file() + assert seen[tid]["name"] == "이름 바꿈" and len(seen) == len(doc["types"]) + assert admin.put(URL, json={"판": v, "정의": doc}).status_code == 409 # 낡은 판 + assert admin.put(URL, json={"판": v + 1, "정의": res.json()}).json()["판"] == v + 2 + assert (copy.parent / "구조물정의_이력" / f"판_{v + 1}.json").is_file() def _rule(doc: dict) -> tuple[str, dict, dict]: @@ -96,6 +102,7 @@ def _rule(doc: dict) -> tuple[str, dict, dict]: def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None: admin = _client(ADMIN) base = admin.get(URL).json() + v = base["판"] tid, up, down = _rule(base) good = { "종류": tid, @@ -133,13 +140,13 @@ def test_검사_키_겹침_꼴_규칙_대상_형상(copy) -> None: bad(모르는칸=1), ] for i, doc in enumerate(cases): - res = admin.put(URL, json={"판": 1, "정의": doc}) + res = admin.put(URL, json={"판": v, "정의": doc}) assert res.status_code == 422, (i, res.text) - assert json.loads(copy.read_text("utf-8"))["판"] == 1 # 거절되면 안 씀 + assert json.loads(copy.read_text("utf-8"))["판"] == v # 거절되면 안 씀 assert not (copy.parent / "구조물정의_이력").exists() shaped = bad(종류={tid: {"기준측점": False}}) shaped["그룹"]["pipe"][0]["형상"] = {"길이": {"받기": True, "뜻": "관 길이"}} - assert admin.put(URL, json={"판": 1, "정의": shaped}).status_code == 200 + assert admin.put(URL, json={"판": v, "정의": shaped}).status_code == 200 def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_path) -> None: @@ -154,5 +161,5 @@ def test_프로젝트_저장에_정의_판_없는_키_값은_남김(copy, tmp_pa ) assert save_structures(root, [item], base_revision=0) == 1 raw = json.loads(Path(root, "B05_Profile", "route", "structures.json").read_text("utf-8")) - assert raw["definition_version"] == 1 + assert raw["definition_version"] == definition.read()["판"] assert load_structures(root)[1][0].options["지난정의키"] == "옛값"