From 9e4b4abdfb92e527031d856d719fd798c5098dba Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:33:24 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat(design):=20=EB=85=B8=EC=B6=9C=ED=98=95?= =?UTF-8?q?=20=ED=9A=A1=EB=8B=A8=EC=88=98=EB=A1=9C=20=C2=B7=20=EA=B0=9C?= =?UTF-8?q?=EA=B1=B0(=EA=B2=89=EB=8F=84=EB=9E=91)=20=3D=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=20=EC=B8=A1=EC=A0=90=20+=20=EC=A2=85=EB=A5=98=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=EC=9E=87=EA=B8=B0=EB=A7=8C(=ED=91=9C=2005?= =?UTF-8?q?=20=C2=B7=2052-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 개거 옛 옵션(규격 · 연장)을 정의에서 뺌 — 저장된 값은 지난 정의 값으로 남음 · 정의 판 7 · 이력 판_6 - 두 종류 모두 컨테이너 0 · 옵션 0 · 기준 측점 바로 아래 구조물 조합 잇기 1(52-1 공용 틀) - 시험: 레지스트리 정책에 옵션 없음 한 줄 · ui_checks/52-5_횡단수로_개거 (B05 · B06 같음) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s --- resources/master_template/구조물정의.json | 27 +- .../master_template/구조물정의_이력/판_6.json | 3359 +++++++++++++++++ .../test_b05_structures_registry_policy.py | 1 + .../tester/ui_checks/52-5_횡단수로_개거.js | 158 + 4 files changed, 3520 insertions(+), 25 deletions(-) create mode 100644 resources/master_template/구조물정의_이력/판_6.json create mode 100644 resources/tester/ui_checks/52-5_횡단수로_개거.js diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 5b2e2bf07..6694fae47 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`가 새 키로 옮긴다.", "구조물군": { @@ -676,30 +676,7 @@ "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)" - } - ] + "options": [] }, { "type_id": "ditch_side", diff --git a/resources/master_template/구조물정의_이력/판_6.json b/resources/master_template/구조물정의_이력/판_6.json new file mode 100644 index 000000000..5b2e2bf07 --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_6.json @@ -0,0 +1,3359 @@ +{ + "판": 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": [ + { + "key": "facing", + "label": "방향", + "input": "select", + "choices": [ + "좌", + "우", + "양면" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "guardrail", + "group": "E", + "name": "가드레일·경계석·위험표지", + "placement": "interval", + "style": { + "color": "#1f9e46", + "abbr": "가드" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "kind", + "label": "종류", + "input": "select", + "choices": [ + "가드레일", + "경계석", + "위험표지" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "position_sign", + "group": "E", + "name": "국가지점번호판", + "placement": "point", + "style": { + "color": "#53a832", + "abbr": "번호판" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "barrier_gate", + "group": "E", + "name": "차단기", + "placement": "point", + "style": { + "color": "#3f9e2e", + "abbr": "차단기" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "completion_sign", + "group": "E", + "name": "준공표지판", + "placement": "point", + "style": { + "color": "#66b344", + "abbr": "준공" + }, + "drawing_views": [ + "plan" + ], + "options": [] + }, + { + "type_id": "spoil_bank", + "group": "E", + "name": "유용토운반작업장(구 사토장)", + "placement": "interval", + "style": { + "color": "#8c9a2e", + "abbr": "사토장" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "side", + "label": "쌓는 쪽", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "required": false, + "phase": "b05", + "default_basis": "「자동(성토 쪽)」 — 흙은 성토 쪽에 쌓는다는 지금까지의 동작을 이어받는 표식. 측점마다 좌·우로 바꿈" + }, + { + "key": "fill_slope_ratio", + "label": "비탈 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 그 측점의 노선 성토 기울기를 그대로 씀 — 지식DB 가 「기본 비탈기울기는 근거에 없다」고 못 박은 자리" + }, + { + "key": "extra_distance_m", + "label": "추가 운반거리", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "detail", + "default_basis": "0 = 「추가 없음」 — 노선 안 사토장은 측점 누가거리로 거리가 나오므로 이 칸은 외부 사토장 대비 여벌" + }, + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "borrow_pit", + "group": "E", + "name": "토사채취장(토취장)", + "placement": "site", + "style": { + "color": "#7d8a2a", + "abbr": "토취장" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "wildlife_path", + "enabled": false, + "group": "F", + "name": "야생동물 이동통로", + "placement": "point", + "style": { + "color": "#3aa88f", + "abbr": "동물길" + }, + "drawing_views": [ + "plan", + "cross_section" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "경사로", + "자연형계단" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "revegetation", + "enabled": false, + "group": "F", + "name": "비탈면 녹화", + "placement": "interval", + "style": { + "color": "#4cb84c", + "abbr": "녹화" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "method", + "label": "공법", + "input": "select", + "choices": [ + "씨뿌리기(줄파종)", + "새심기", + "선떼붙이기", + "줄떼", + "평떼", + "비탈덮기", + "녹생토 뿜어붙이기", + "조공", + "편책", + "나무심기" + ], + "default": null, + "default_basis": "「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)", + "required": true, + "phase": "detail" + }, + { + "key": "face", + "label": "대상 면", + "input": "select", + "choices": [ + "절토면", + "성토면", + "양쪽" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "cover_kind", + "label": "덮개(비탈덮기)", + "input": "select", + "choices": [ + "거적", + "짚망", + "방초·야자섬유매트" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비탈덮기일 때만 고름 — 비면 비탈덮기 줄이 안 섬(품셈 5-25 거적덮기 ㎡ · 5-28-1 짚망 · 5-28-2 매트 · 재료가 공종을 가름)" + }, + { + "key": "step_height_m", + "label": "단 직고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음 · 비면 선떼 연장(품셈 5-16-1 [주]② 대상지 직고 ÷ 단 직고 × 연장)이 안 섬" + }, + { + "key": "sod_grade", + "label": "선떼 급수", + "input": "select", + "choices": [ + "1급", + "2급", + "3급", + "4급", + "5급", + "6급", + "7급", + "8급", + "9급" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음 · 비면 m당 떼 매수(사방기술교본 표 2-2-6)가 안 섬" + }, + { + "key": "sod_size", + "label": "선떼 떼 크기", + "input": "select", + "choices": [ + "대형 40×40", + "소형 18×18" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "사방기술교본 표 2-2-6 이 대형(40×40)·소형(18×18) 두 줄을 나란히 줌 — 우리가 고를 자리가 아니라 기본값 없음 · 비면 선떼 떼 매수가 안 섬" + }, + { + "key": "cutting_soil", + "label": "단끊기 토질", + "input": "select", + "choices": [ + "보통토사", + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "품셈 5-16-1 단끊기 표의 토질 셋 — B06 지반(토사/암)과 다른 축이라 자동으로 안 고름 · 비면 단끊기 조각만 막힘" + } + ] + }, + { + "type_id": "pavement_concrete", + "group": "G", + "name": "콘크리트 포장", + "placement": "interval", + "style": { + "color": "#8a8f98", + "abbr": "포장" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10 + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 포장층 두께(표준 횡단면 설정 · 기본 0.2m = 임도기술교본 3-2 「두께 0.2m」)를 이음 — 적으면 적은 값이 이김" + }, + { + "key": "width_m", + "label": "포장 폭", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 노폭(구간 측점의 차도 표준 폭 · 교본 3-2 「포장 폭 3.0m」 · 실무 관측 3.5~4.0)을 이음 — 적으면 적은 값이 이김" + }, + { + "key": "widening_area_m2", + "label": "확폭 면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 곡선부 확폭을 측점 사이 평균 × 거리로 더함(교본 3-2 「곡선부에는 확폭」) — 확폭이 없으면 0 을 적을 것" + }, + { + "key": "joint_spacing_m", + "label": "수축줄눈 간격", + "input": "number", + "unit": "m", + "default": 6, + "default_basis": "제안값 · 임도기술교본 부록 4-7 7-1 3.5.1 「수축줄눈의 간격은 4-6m를 기준」 · 실무 소광 6m 관측 · 바꿀 수 있음", + "phase": "detail" + }, + { + "key": "separation_sheet", + "label": "비닐(깔기·양생)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 비닐 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「양생 재료비(비닐 등)는 별도 계상」이라 설계자가 고름(기본값 없음)" + }, + { + "key": "wire_mesh", + "label": "철망", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 철망 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「철망재료비는 별도 계상」 · 교본 부록 7-1 3.4 「설계도서에 따라」 · 실무 소광 「철망포함」 관측" + } + ] + }, + { + "type_id": "gravel_surfacing", + "enabled": false, + "group": "G", + "name": "쇄석(자갈) 포설", + "placement": "interval", + "style": { + "color": "#9aa0a8", + "abbr": "쇄석" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "etc", + "group": "호환", + "name": "기타", + "placement": "point", + "style": { + "color": "#98a2b3", + "abbr": "기타" + }, + "drawing_views": [ + "profile" + ], + "options": [ + { + "key": "name", + "label": "이름", + "input": "text", + "default": "기타 구조물" + } + ] + } + ], + "종류": { + "pipe": { + "동작": [ + "계곡통과시설", + "유입유출_묶음", + "기슭막이_한벌" + ], + "이름": { + "pipe.diameter": "관경", + "pipe.diameter=800": "800", + "pipe.diameter=1000": "1000", + "pipe.diameter=1200": "1200", + "pipe.diameter=1500": "1500", + "pipe.length": "관 길이 (m)", + "inlet.structure": "구조", + "inlet.structure=집수정": "집수정", + "inlet.structure=기슭막이": "흙막이", + "inlet.form": "형태", + "inlet.form=돌쌓기(찰)": "돌 쌓기(찰)", + "inlet.form=돌쌓기(메)": "돌 쌓기(메)", + "inlet.form=콘크리트": "콘크리트", + "inlet.form=돌망태": "돌망태", + "inlet.form=통나무·목재틀": "통나무/목재틀", + "inlet.form=바자": "바자", + "inlet.form=I": "I형", + "inlet.form=L": "L형", + "inlet.form=U": "ㄷ형", + "inlet.length": "길이 (m)", + "inlet.height": "높이 (m)", + "inlet.before": "기준측점 전 (m)", + "inlet.after": "기준측점 후 (m)", + "outlet.structure": "구조", + "outlet.structure=기슭막이": "흙막이", + "outlet.form": "형태", + "outlet.form=돌쌓기(찰)": "돌 쌓기(찰)", + "outlet.form=돌쌓기(메)": "돌 쌓기(메)", + "outlet.form=콘크리트": "콘크리트", + "outlet.form=돌망태": "돌망태", + "outlet.form=통나무·목재틀": "통나무/목재틀", + "outlet.form=바자": "바자", + "outlet.length": "길이 (m)", + "outlet.height": "높이 (m)", + "outlet.before": "기준측점 전 (m)", + "outlet.after": "기준측점 후 (m)", + "soil_add": "흙막이 추가", + "soil_add.structure": "구조", + "soil_add.structure=기슭막이": "흙막이", + "soil_add.form": "형태", + "soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)", + "soil_add.form=돌쌓기(메)": "돌 쌓기(메)", + "soil_add.form=콘크리트": "콘크리트", + "soil_add.form=돌망태": "돌망태", + "soil_add.form=통나무·목재틀": "통나무/목재틀", + "soil_add.form=바자": "바자", + "soil_add.length": "길이 (m)", + "soil_add.height": "높이 (m)", + "soil_add.before": "기준측점 전 (m)", + "soil_add.after": "기준측점 후 (m)", + "chute_add": "도수로/돌붙임", + "chute_add.structure": "구조", + "chute_add.structure=도수로": "도수로", + "chute_add.structure=돌붙임": "돌붙임" + } + }, + "box_culvert": { + "동작": [ + "계곡통과시설", + "BOX_본체규격", + "날개벽" + ] + }, + "ford_pavement": { + "동작": [ + "계곡통과시설", + "월류단면" + ] + }, + "ford_bridge": { + "동작": [ + "계곡통과시설", + "월류단면", + "날개벽" + ] + }, + "revetment": { + "동작": [ + "계곡통과시설", + "기슭막이_한벌" + ] + }, + "pavement_concrete": { + "동작": [ + "포장_물넘이_겹침" + ] + } + }, + "표시": { + "pipe": { + "pipe_kind": false + } + }, + "그룹": { + "pipe": [ + { + "id": "grv6lny", + "이름": "관", + "옵션": [ + "pipe_kind", + "pipe_diameter_mm" + ], + "형상": {} + }, + { + "id": "gnbf9m9", + "이름": "관 보호공(유입)", + "옵션": [ + "inlet_type" + ], + "형상": {} + }, + { + "id": "g9lq9uk", + "이름": "관 보호공(유출)", + "옵션": [], + "형상": {} + } + ] + }, + "배치": {}, + "규칙": [] +} diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index 2f2c006c7..d17cd6d55 100644 --- a/resources/tester/test_b05_structures_registry_policy.py +++ b/resources/tester/test_b05_structures_registry_policy.py @@ -366,6 +366,7 @@ def test_a6_a7_types_exist_for_manual_placement(): item = by_id[type_id] assert item.group == "A" and item.managed_by is None assert item.placement == "point" + assert not item.options, f"{type_id} 옵션 없음 — 기준 측점 + 종류 단위 잇기만(PLAN 52-5)" def test_ford_bridge_shares_pipe_kind_and_diameter(): diff --git a/resources/tester/ui_checks/52-5_횡단수로_개거.js b/resources/tester/ui_checks/52-5_횡단수로_개거.js new file mode 100644 index 000000000..6745c6ed2 --- /dev/null +++ b/resources/tester/ui_checks/52-5_횡단수로_개거.js @@ -0,0 +1,158 @@ +/* 52-5 — B05 · B06 좌측 「구조물 배치」 노출형 횡단수로 · 개거(겉도랑) = 사용자 구조물 표 05 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(랩탑_보조) · 폼 비어 있음 · 로그인 상태 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m525 = "단계";` 를 붙여 + * orca eval --expression "window.__m525='edit'; $(cat 이 파일)" → 두 종류 모양 · 추가 · 잇기 · [저장] + * (orca reload 로 다시 열기) + * orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → 저장 값 그대로 · 다시 고름 + * (B06 으로 옮겨 — 같은 모양) + * orca eval --expression "window.__m525='verify'; $(cat 이 파일)" → B06 에서도 같은 값 · 같은 모양 + * orca eval --expression "window.__m525='restore'; $(cat 이 파일)" → 추가한 둘 삭제 · [저장] + * 쓰는 것: 검증 프로젝트 structures.json(되돌림 단계가 지움) + * 기대 수치: + * shape(종류마다) — optionInputs 0 · containers 0 · summaryRows 1 · summaryVisible true · summaryRightAfterPosition true · + * anchor true · stale false + * edit — added 둘 · 잇기 이름 값 있음 · 저장 뒤 목록 2 늘어남 + * verify — 같은 모양 · 잇기 이름 그대로 · 옵션 값 없음 + */ +(async () => { + const MODE = window.__m525 || "edit"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 15000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const vis = (n) => !!n && n.offsetParent !== null; + const sec = () => document.querySelector(".b05-structure-section"); + const body = () => sec().querySelector(".b05-route__panel-body"); + const selects = () => sec().querySelectorAll(".b05-structure__grid select"); + const fire = (el, value) => { + el.value = value; + el.dispatchEvent(new Event("change", { bubbles: true })); + }; + const listItems = () => [...document.querySelectorAll(".b05-route__irregular-list li")]; + const NAMES = { cross_drain_exposed: "노출형 횡단수로", open_ditch: "개거(겉도랑)" }; + const AT = { cross_drain_exposed: [2, 7], open_ditch: [6, 3] }; + + const shape = (typeId) => { + const kids = [...body().children]; + const position = kids.find((c) => c.classList.contains("b05-structure__position-row")); + const summary = kids.find((c) => c.classList.contains("b05-structure__summary")); + const optionRow = kids.filter((c) => c.classList.contains("b05-structure__grid"))[1]; + const after = kids.slice(kids.indexOf(position) + 1).find((c) => vis(c)); + const anchorWrap = position.querySelectorAll(":scope > label")[2]; + return { + typeId, + optionInputs: vis(optionRow) ? optionRow.querySelectorAll("input, select").length : 0, + containers: [...sec().querySelectorAll("[data-spec-container]")].filter(vis).length, + summaryRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length, + summaryVisible: vis(summary), + summaryRightAfterPosition: after === summary, + anchor: vis(anchorWrap), + stale: vis(sec().querySelector(".b05-structure__stale")), + linked: summary.querySelector(".b05-structure__summary-name")?.textContent ?? "", + }; + }; + const pickType = async (typeId) => { + const [group, type] = selects(); + fire(group, "A"); + await w(300); + fire(type, typeId); + await w(500); + }; + const linkFirstLeaf = async () => { + sec().querySelector("[data-b05-summary=pick]").click(); + await until(() => document.querySelector(".ui-item-picker")); + await w(600); + const modal = document.querySelector(".ui-item-picker"); + // 끝 항목 = ▸ · ▾ 로 안 시작하고 모달 조작 단추가 아닌 첫 단추 + const ctl = ["닫기", "개인용으로 복사", "관리 화면에서 고치기", "해제", "적용"]; + const leaf = [...modal.querySelectorAll("button")].find((b) => { + const t = b.textContent.trim(); + return t && !/^[▸▾]/.test(t) && !ctl.includes(t); + }); + leaf?.click(); + await w(300); + [...modal.querySelectorAll("button")].find((b) => b.textContent.trim() === "적용")?.click(); + await w(500); + return !!leaf; + }; + const clickBtn = async (text) => { + [...document.querySelectorAll("button")] + .find((b) => b.textContent.trim() === text && vis(b)) + ?.click(); + await w(1500); + }; + const out = { mode: MODE, items: [] }; + + if (MODE === "edit") { + await until(() => listItems().length > 0); + out.before = listItems().length; + for (const typeId of Object.keys(NAMES)) { + await pickType(typeId); + const row = { ...shape(typeId), phase: "edit" }; + const [no, dist] = sec() + .querySelectorAll(".b05-structure__position-row > label")[2] + .querySelectorAll("input"); + fire(no, String(AT[typeId][0])); + fire(dist, String(AT[typeId][1])); + await w(200); + row.picked = await linkFirstLeaf(); + row.linked = shape(typeId).linked; + sec().querySelector(".is-primary").click(); // 추가 + await w(800); + out.items.push(row); + } + out.after = listItems().length; + await clickBtn("저장"); + out.listed = listItems() + .map((l) => l.textContent.trim()) + .filter((t) => /횡단수로|개거/.test(t)); + } else if (MODE === "verify") { + await until(() => listItems().length > 0); + for (const typeId of Object.keys(NAMES)) { + const li = listItems().find((l) => l.textContent.includes(NAMES[typeId])); + if (!li) { + out.items.push({ typeId, missing: true }); + continue; + } + (li.querySelector("button") ?? li).click(); + await w(800); + // B06 은 다른 항목이 골라져 있으면 첫 눌림이 풀기만 함 — 안 불러와졌으면 한 번 더 + if (selects()[1].value !== typeId) { + (li.querySelector("button") ?? li).click(); + await w(800); + } + out.items.push({ ...shape(typeId), phase: "verify", title: li.title || "" }); + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + await w(300); + } + } else if (MODE === "restore") { + await until(() => listItems().length > 0); + for (const typeId of Object.keys(NAMES)) { + const li = listItems().find((l) => l.textContent.includes(NAMES[typeId])); + if (!li) continue; + (li.querySelector("button") ?? li).click(); + await w(600); + await clickBtn("삭제"); + } + await clickBtn("저장"); + out.left = listItems().length; + } + const rows = out.items.filter((r) => !r.missing); + out.shapeOk = + rows.length === 2 && + rows.every( + (r) => + r.optionInputs === 0 && + r.containers === 0 && + r.summaryRows === 1 && + r.summaryVisible && + r.summaryRightAfterPosition && + r.anchor && + !r.stale, + ); + return JSON.stringify(out); +})(); From af1e7d2328c7318a0cf64314386e77cfdbf730da Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:33:34 +0900 Subject: [PATCH 2/9] =?UTF-8?q?chore(plan):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=20=ED=91=9C=2008=20=EC=82=AC=ED=86=A0=EC=9E=A5=20=C2=B7=20?= =?UTF-8?q?=ED=86=A0=EC=B7=A8=EC=9E=A5=20=C2=B7=20=EB=85=B8=EB=A9=B4?= =?UTF-8?q?=EA=B3=B5=20=C2=B7=20=EA=B8=B0=ED=83=80=20=EC=9B=90=EB=AC=B8(10?= =?UTF-8?q?-5)?= 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_01FJqWQpoB1RE7hiaQ2wH8qV --- .../08_사토장_토취장_노면공_기타.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md diff --git a/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md b/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md new file mode 100644 index 000000000..8f544555e --- /dev/null +++ b/docs/raw/구조물정의_표/08_사토장_토취장_노면공_기타.md @@ -0,0 +1,17 @@ +# 구조물 표 08 — 안전부대용지 사토장 · 토취장 · 노면공 콘크리트 포장 · 기타 (사용자 10-5 · 원문 그대로) + +구조물군 상세 종류 기본 옵션 컨테이너 1 컨테이너 1 옵셩 항목 … 상세 설계 필수 선택 +안전부대용지 유용토운반작업장(사토장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 사토장 부피 수기입력 + +안전부대용지 토사채취장(토취장) 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 토취장 부피 수기입력 + +노면공 콘크리트 포장 측점 No, + 측점거리 (필수)컨테이너 제목 : 상세 내용 + 포장사양 콘크리트, XXX + 높이 수기입력 + +기타 수기입력 측점 No, + 측점거리 (선택)컨테이너 제목 : 상세 내용 (상세 설계) 도수로(or 돌붙임) + 비고 수기 입력 (상세 설계) 도수로(or 돌붙임) 관보호공, 옹벽, 돌쌓기, 흙막이, 큰톨쌓기 가 같은 형상으로 보임. + +(가드레일은 표 07 덧과 같음) From 541c4642b043a8399a08fdd83b90c2e79d091873 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:35:20 +0900 Subject: [PATCH 3/9] =?UTF-8?q?chore(sync):=20=EA=B3=B5=EC=9C=A0=20docs?= =?UTF-8?q?=C2=B7.gitignore=20=EB=8B=B4=EC=9D=8C=20=E2=80=94=20origin/main?= =?UTF-8?q?=5Flaptop=5F1=20=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_01MPcuokNm1ZR2kqgGGDfaJA --- .gitignore | 7 ++- docs/raw/사용자요구/2026-10-05_수정요구.md | 71 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 84f53985e..918db4041 100644 --- a/.gitignore +++ b/.gitignore @@ -73,10 +73,11 @@ config/spreadsheet_node/ # graphify 위키 산출물 — 창끼리 같은 위키를 찾게 **통째로** git 으로 나름 # (2026-09-09 사용자 확정 「출력물 전체를 공유해도 됨」). # 다만 아래 둘은 PC 마다 다른 값이라 나르면 안 됨 — 나르면 상대 PC 에서 어긋남. -docs/wiki/graphify-out/.graphify_python -docs/wiki/graphify-out/.graphify_incremental.json +# docs/wiki/graphify-out/.graphify_python +# docs/wiki/graphify-out/.graphify_incremental.json # 캐시는 **PC 마다 다시 만들어지는 파생물**이라 git 으로 나르지 않는다(2026-09-10 사용자 확정). # 까닭 — 코드 창에서 graphify 가 돌 때마다 이 폴더가 바뀌어, 위키 브랜치를 받을 때 # 「덮으면 네 것이 날아간다」로 **병합이 막혔다**. 4.3MB·443파일도 함께 가벼워진다. -docs/wiki/graphify-out/cache/ +#docs/wiki/graphify-out/cache/ +docs/ ~$* diff --git a/docs/raw/사용자요구/2026-10-05_수정요구.md b/docs/raw/사용자요구/2026-10-05_수정요구.md index eeadef7f7..bebbfa64f 100644 --- a/docs/raw/사용자요구/2026-10-05_수정요구.md +++ b/docs/raw/사용자요구/2026-10-05_수정요구.md @@ -48,3 +48,74 @@ 4. 구조물 조합의 단가산출 상세화면에도 원문보기 우측 패널이 있어야함. 우측 패널은 단가산출 로직을 선택할 경우에 원문을 보여주는 기능이며 패널을 접었다 펼수 있어야함. 5. 원문 패널 추가에 따라 현재 단가산출 로직 항목을 선택하면 옵션창이 보이나 선택하면 하이라이트를 하고 원문을 보일수 있게 함. 그리고 옵션설정에 대한 조정은 올리기, 내리기, 빼기 버튼의 이름을 위로, 아래로, 삭제로 변경하고 아래로 와 삭제 사이에 신규 버튼(이름 : 옵션)을 추가하여 선택할 수 있게 변경. 버튼 사이간격에 대해 템플릿화하여 적용. 6. 구조물 조합의 면적산출 우측패널에 별도의 도면 패널 추가(기능은 4번 요청 확인) + +## 구조물 옵션 정의 페이지 (10-5 · 원문 그대로) + +이제부터 다시 계획서를 세울 예정임. +1. 목표 : 현재 구조물 옵션 보이기의 컨테이너 항목을 기준으로 프로젝트 내부에 사용하는 구조물의 옵션 항목 및 제어를 설정하는 페이지를 궁성 +2. 상세 내용 + - 현재 M02의 구조물 옵션보이기 항목으로 구조물군과 구조물 종류를 설정하고 이름을 변경할 수 있게 반영 + - 구조물 종류를 선택시 우측에 해당항목의 일위대가 그룹을 지정하고 변수를 편입할수 있게 반영(기반영), 추가로 배치 순서를 조절할 수 있게, 행열의 크기 제어도 같이 + - 구조물 종류의 특정 옵션의 옵션 값 항목을 지정할 수 있게 반영 + - 구조물 종류의 최상위에 기본값인 기준 측점을 표현할건지에 대한 기능 + - 구조물의 길이, 높이, 기준측점 전/후 값에 대한 옵션 사용여부 및 어떤걸 제어하는지에 대한 결정 + - 구조물 옵션과 값들의 이름 변경기능 + - 옵션들은 상위에 따라 하위 옵션을 변경할 수 있게 반영 +3. 현재 B05와 B06의 방대한 변경이 필요하니 순차적으로 진행, 가자먼저 작업할 내용은 B05와 B06에 좌측 패널의 구조물 배치 컨테이너에 보일 항목을 반만들수 있게 반영하고 선택에 따라 변경되는 변수 값과 제어는 별도로 작업 할 예정. 일단 방향성에 대해 검토해보고 대화부터 하자. 내가 원하는 방향은 결국 내가 프로그래머가 아니라서 나중에 항목을 추가하거나 옵션을 변경하였을때 프론트엔드에 기능을 구현하고 변경하고 싶은것임. + +## 원문 보기 패널 (10-5 · 원문 그대로) + +계획서는 작성해주고 빠르게 작업해줄 내용이 M01과 M02에서 원문보기가 패널 양식을 사용하지 않고 있음. 추가로 M01페이지의 원문보기에서 md파일을 보기가 깨짐이 있음. M02는 정상. M01의 경우 흐름도와 원분은 우측 패널로 구현되어있음. 좌측 패널과 동일하게 접었다 펴는 양식 제대로 확인할 ㄱ서. + +그리고 우측 패널의 내용은 메인 화면의 스크롤과 별개로 동작하게 반영 + +추가로 원문에서 표는 스크롤로 매우 낮은 공간만 보이고 스크롤 표현되어서 보기가 힘듬. 그냥 표가 길더라도 보여줄것. 어차피 상세페이지와 원문의 우측 패널의 영역은 별도의 스크롤 영역이라 무관함. + +원문은 M01과 M02에 표현되고 있는데 일원화 할것. M02가 더 가독성이 좋음. + +## 일위대가 조합 · 면적산출 알약 (10-5 · 원문 그대로) + +다시 돌아가서 M02 페이지에 +1-1. 구조물 조합의 면적산출에서 도면으로 보내진 셀의 영역을 표현이 되게하기 +1-2. 구조물 조합의 면적산출에서 알약을 선택을 했을때 셀이 반짝임은 있지만 반대로 셀을 선택했을때는 잘 안보임. +1-3. 구조물 조합의 면적산출에서 일위대가의 로직을 삭제했을때 기 지정된 알약이 잔존하고 있음. 삭제가 힘들다면 느낌표라도 보여지게 하고 삭제가능한지 검토 +2. 원문 표기의 '산림품셈 3-4-2 1' 항목 확인 +3. 구조물 조합의 일위대가 상세 화면에서 단가산출 로직에 들어가는 로직항목들은 단가산출 로직 뿐만아니라 단일 단가산출 로직을 일위대가로 승격시킨 선택지와 다른 일위대가로 만들어진 조합도 선택이 되어야함. +4. 방법 : 구조물 조합 컨테이너 하단에 일위대가 조합 컨테이너를 추가하고 여기에 마스터, 회사, 개인의 권한을 구현, 이때는 두가지의 선택지가 있음. 하나는 일위대가로 승격시킨 단가산출로직의 리스트와 일위대가에 사용하기 전 사전 조합으로 만들어진 조합으로 구분함. +5. 단가산출 승격은 단가산출 로직의 항목들을 검색하여 선택하면 일위대가 형태로 저장이 됨. (도면 X, 면적산출 X) +6. 일위대가 사전 조합은 단가산출 승격 + 다른 일위대가의 조합으로 그룹(도면 X, 면적산출 X) +7. 일위대가 조합은 구조물 조합의 일위대가 상세화면에서 선택할 수 있어야함. +8. 구조물 조합의 일위대가 상세화면에서의 로직테이블은 일위대가 목록표임. 여기서 행을 눌렀을때 하단에 상세 내역이 보였으면 함. 상세내역은 M01페이지의 단가산출 로직을 보면 시험계산 컨테이너 내부에 기준단위에 대한 테이블 리스트가 있음. 이걸 B02 구조물 조합의 일위대가 조합에 맞게 넣어주면 됨. 행을 선택했을때만 보여주면 되고 다른 행을 선택하면 닫히면 됨. +9. 구조물 조합의 일위대가 상세화면에 단가산출 로직 테이블의 이름 좌측 열을 추가하고 구분을 추가함. 구분은 출처로 단가산출인지, 일위대가 조합인지, 단가산출을 승격한 정보인지 표기 항목은 4음절로 표기, +일단 상세히 반영 가능성에 대해 상세히 검토후 계획서에 반영 + +## 오른쪽 패널 높이 (10-5 · 원문 그대로) + +하나씩 수정 내용에 대해 요청할께. M01 단가산출 로직 상세와 M02의 구조물 조합의 일위대가와 면적산출에는 우측 패널이 존재함. 이 우측 패널의 높이는 화면에 맞춰서 높이를 결정하게 했으면 함. 스크롤 동작시 움직임. + +추가로 너가 오해한게 있는데 구조물 조합의 일위대가 상세페이지에서 단가산출 로직 행을 선택했을때 나오는 상세정보를 해당행이 아래에 별도의 행으로 구분하여 접었다 폈다하면서 내용을 보줬으면 해. 별도의 공간에 M01처럼 보여달라는게 아니라 테이블에 맞춰서 정보를 넣어달라는 의미였어. + +## 서고 · 고르기 창 (10-5 · 원문 그대로) + +1. 로직고르기 모달창에서 찾기 버튼은 필요없어 보임. 다른 찾기 기능을 보면 없어도 될것같아. 그리고 단가 승격과 사전 조합을 별도의 버튼으로 활성화하는데 이건 그냥 구분정보를 보여줘서 선택을 유도해도 되지 않을까? +2. 일위대가 조합 컨테이너에는 새로, 본떠 만들기, 지우기 버튼은 필요없고 단가산출 승격 이라는 항목과 일위대가 사전조합이라는 항목 두게만 있으면 될것같음. 그리고 해당 항목을 누르면 상세에 테이블형태로 리스트 들을 보여주고 거기에 생성, 복사, 삭제 버튼이 있으면 됨. +3. 사전 조합도 동일함 + +단위를 선택하게 하는 항목들 중 선택 후 나오는 단위 표기 버튼이 너무 작음 저장같은 버튼과 같은 크기로 변경 + +사전조합에 상세에서 보여지는 단가산출 로직의 선택 기능이 없음 + +구조물 조합의 일위대가에서 조합 테이블에 넣어준 상세 항목 펼쳐짐 화살표는 필요없어. + +그리고 로직 고르기 모달창에서 구분과 상세 구분은 2열 구조로 변경하고 찾을때 나온 항목들의 이름들은 좌측 맞춤으로 글자를 맞춰야지. 찾기의 공용화 양식이 없는거야?템플릿 있을것같은데? + +단가산출과 일위대가 사전조합에서 테이블의 양식도 M01에 공통테이블 양식이 있을것같고 버튼들도 M01에 템플릿이 있을거야. 확인해봐 그리고 사전조합의 경우에 검색은 별도의 창이 아니라 구조물 조합의 일위대가 테이블의 조건 추가와 같은 기능이어야. + +일위대가 사전조합에서 나오는 모당창의 형식 뿐만아니라 내용도 구조물조합의 일위대가의 상세 화면에서 로직 추가와 같아야된다고 선택되는 항목도 + +어차피 신규로 키값을 만들지 않았음? 그럼 당연히 해당 연결정보와 구분, 상세구분을 만들어야하지 않음? + +다음으로 구조물 조합에서 일위대가의 상세페이지에서 테이블 행 선택에서 단가승격이야 단가산출과 같은 항목이라 문제가 없는데 일위대가 사전조합의 경우에는 전체를 다 표현하는데 문제가 있어. 일위조합인 경우에는 조합의 세부항목을 기준으로 노무비, 재료비, 경비를 값을 가져와서 사용하면 가능하지 않을까? + +추가로 구조물 조합의 일위대가 상세페이지의 로직 테이블의 순서 열에서 존재하는 버튼을 2행 구조로 변경 +단가 승격에도 옵션 버튼이있어야할것같고. 일위조합의 경우에는 바로 옵션으로 지정할수 없으니 수정버튼(모달)도 잇어할것 같은데? From 4c0174bf27924ded0de77096455bfce5792ed844 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:38:40 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat(structure):=20=ED=9A=A1=EB=8B=A8?= =?UTF-8?q?=EB=B0=B0=EC=88=98=20=EB=A7=B9=EC=95=94=EA=B1=B0=20=EC=83=88=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98(=ED=91=9C=2006=20=C2=B7=2052-6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정의 판 8 — 새 종류 cross_underdrain(A 횡단배수 · 점형 · 이름 맹암거 · 약자 횡맹 · 색 따로) · 종단배수 underdrain 그대로 - 컨테이너 = 정의 그룹 gcrossud 「맹암거」 — 암거 높이(m) · 암거 경사(%) 손 입력 · 기본값 없음 · 그룹 잇기로 구조물 조합 항목 - 시험 test_52_6_cross_underdrain · 종류 수 기대값(B05 선택지 24 · 정의 38) · ORCA 시나리오 52-6_횡단맹암거 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- resources/master_template/구조물정의.json | 49 +- .../master_template/구조물정의_이력/판_7.json | 3336 +++++++++++++++++ .../tester/test_52_6_cross_underdrain.py | 91 + .../tester/test_b05_structures_define.py | 2 +- .../test_b05_structures_registry_policy.py | 4 +- resources/tester/ui_checks/52-6_횡단맹암거.js | 216 ++ 6 files changed, 3694 insertions(+), 4 deletions(-) create mode 100644 resources/master_template/구조물정의_이력/판_7.json create mode 100644 resources/tester/test_52_6_cross_underdrain.py create mode 100644 resources/tester/ui_checks/52-6_횡단맹암거.js diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 6694fae47..a62bd0a6e 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -1,5 +1,5 @@ { - "판": 7, + "판": 8, "schema_version": 1, "comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.", "구조물군": { @@ -678,6 +678,42 @@ ], "options": [] }, + { + "type_id": "cross_underdrain", + "group": "A", + "name": "맹암거", + "placement": "point", + "style": { + "color": "#3aa0a8", + "abbr": "횡맹" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "culvert_height_m", + "label": "암거 높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)" + }, + { + "key": "culvert_slope_pct", + "label": "암거 경사", + "input": "number", + "unit": "%", + "default": null, + "required": false, + "empty_means": "비우면 값 없음 — 사용자 표 06 손 입력 칸(기본값 근거 없음)" + } + ] + }, { "type_id": "ditch_side", "enabled": false, @@ -3329,6 +3365,17 @@ "옵션": [], "형상": {} } + ], + "cross_underdrain": [ + { + "id": "gcrossud", + "이름": "맹암거", + "옵션": [ + "culvert_height_m", + "culvert_slope_pct" + ], + "형상": {} + } ] }, "배치": {}, diff --git a/resources/master_template/구조물정의_이력/판_7.json b/resources/master_template/구조물정의_이력/판_7.json new file mode 100644 index 000000000..6694fae47 --- /dev/null +++ b/resources/master_template/구조물정의_이력/판_7.json @@ -0,0 +1,3336 @@ +{ + "판": 7, + "schema_version": 1, + "comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 PLAN.md 구조물 리스트와 resources/knowledge/technical_info/01_임도 참조. 2026-08-17 선택지 재편: B군(종단배수)·F군(생태/녹화)·G군(노면공)은 enabled:false 보존 — B05 선택지에서 빠지고 B06 개별 횡단도 옵션으로 재사용한다. 바닥막이(bed_sill)·생태연못(eco_pond)은 임도 미사용 확정으로 삭제. 배관·BOX암거·물넘이·세월교는 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않으며, 배관 부속(유형/집수정/기슭막이/돌붙임)·날개벽 옵션 정의는 B06/B07 필수 승격의 선언 근거다. ★기본값 원칙(2026-08-16 크로스체크 2차): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (pipe_diameter_mm 1000 = 별표2 원칙값+사용자 확정 — 화이트리스트 등재). BOX암거 본체 2.0×2.0·날개벽 유입/유출 있음·짧은쪽 높이 1m·길이 2m·각도 45°는 2026-08-17 사용자 확정값이다 — 본체 규격은 실무 관측 규격(2.0×2.0·3.0×3.0)을 폼에서 프리셋 select로 받고 필요 시 사용자 지정으로 자유 입력하되, 저장 키는 body_width_m·body_height_m 숫자 그대로다(프리셋 전용 키를 만들지 않는다). 이 값들은 B05 서브폼이 실제로 받으므로 phase를 detail로 두지 않는다. ★보호공 개편(2026-08-17 사용자 지시): 구 `*_pitching`(있음/없음) + `*_pitching_finish`(찰/메) 두 축을 `*_protection` 한 축(돌붙임(찰)·돌붙임(메)·도수로)으로 합쳤다 — 도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이 아니라 보호공 선택지로 받는다. \"없음\"은 삭제(물이 흐르는 자리라 보호공 필수). 치수는 배타 — 돌붙임이면 `*_protection_area_m2`(10㎡), 도수로면 `*_protection_width_m`(1m, 지식DB 수치 근거 없음·사용자 확정, 실무 확인 후 개선). 구 저장분은 `common_util_drainage_pipes.parse_pipe_points`가 새 키로 옮긴다.", + "구조물군": { + "A": "A 횡단배수", + "B": "B 종단배수", + "C": "C 사면안정", + "D": "D 계류·사방", + "E": "E 안전·부대·용지", + "F": "F 생태·녹화", + "G": "G 노면공", + "호환": "기타" + }, + "동작": { + "계곡통과시설": "관 지점 정본(pipe_points.json) 시설 — 부속 옵션 서브폼을 손코딩으로 그림 · 저장은 폼이 아는 칸만 갈아 끼움", + "유입유출_묶음": "관 유입구(집수정 · 기슭막이 택일) · 유출구(기슭막이) 소그룹", + "기슭막이_한벌": "기슭막이 형태 · 길이 · 높이 · 바닥 보호공 한 벌 — 관 유입 · 유출 · 독립 기슭막이가 같은 조각", + "BOX_본체규격": "BOX암거 본체 규격 프리셋 고르기(폭 × 높이 한 칸)", + "날개벽": "날개벽 유입 · 유출 · 짧은쪽 높이 · 길이 · 각도 조각", + "월류단면": "월류 폭 · 높이 · 바닥 경사 · 포장 두께 + 개략 단면 줄 · 필요 수심 미만은 되돌림", + "포장_물넘이_겹침": "포장 구간이 물넘이포장을 품으면 앞 · 뒤로 나눔 · 끝만 걸치면 저장 멈춤" + }, + "types": [ + { + "type_id": "pipe", + "group": "A", + "name": "배관", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#4f8ef7", + "abbr": "배수관" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "pipe_kind", + "label": "관종", + "input": "select", + "choices": [ + "흄관", + "VR관", + "파형강관" + ], + "default": "파형강관", + "required": false, + "enabled": false + }, + { + "key": "pipe_diameter_mm", + "label": "관경", + "input": "select", + "choices": [ + "800", + "1000", + "1200", + "1500" + ], + "unit": "mm", + "default": "1000", + "required": false + }, + { + "key": "inlet_type", + "label": "유입구 구조", + "input": "select", + "choices": [ + "기슭막이", + "집수정" + ], + "default": "기슭막이", + "required": false + }, + { + "key": "wing_wall_type", + "label": "관보호공 날개벽", + "input": "select", + "choices": [ + "A-TYPE", + "C-TYPE", + "A-TYPE+집수정" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 날개벽을 안 셈 — 고르면 그 형식의 개소당 원단위로 한 줄이 섬(Ø800·Ø1000 만 있음)" + }, + { + "key": "inlet_basin_form", + "label": "집수정 형식", + "input": "select", + "choices": [ + "동물이동형", + "□형(기본형)", + "돌집수정 ㄴ형", + "돌집수정 ㄷ형" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_material", + "label": "집수정 형태", + "input": "select", + "choices": [ + "콘크리트", + "조립식 주철맨홀", + "돌쌓기(찰)", + "돌쌓기(메)" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_length_m", + "label": "집수정 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_before_m", + "label": "집수정 기준측점 전", + "input": "number", + "unit": "m", + "default": 1, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_basin_after_m", + "label": "집수정 기준측점 후", + "input": "number", + "unit": "m", + "default": 1, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_form", + "label": "유입 기슭막이 형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(찰)", + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_length_m", + "label": "유입 기슭막이 길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false + }, + { + "key": "inlet_revet_before_m", + "label": "유입 기슭막이 기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_after_m", + "label": "유입 기슭막이 기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "inlet_revet_height_m", + "label": "유입 기슭막이 높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" + }, + { + "key": "outlet_type", + "label": "유출구 구조", + "input": "select", + "choices": [ + "기슭막이" + ], + "default": "기슭막이", + "required": false + }, + { + "key": "outlet_revet_form", + "label": "유출 기슭막이 형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(메)", + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_length_m", + "label": "유출 기슭막이 길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false + }, + { + "key": "outlet_revet_before_m", + "label": "유출 기슭막이 기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_after_m", + "label": "유출 기슭막이 기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail" + }, + { + "key": "outlet_revet_height_m", + "label": "유출 기슭막이 높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)" + }, + { + "key": "revet_foundation", + "label": "기슭막이 기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": false, + "phase": "detail" + } + ] + }, + { + "type_id": "box_culvert", + "group": "A", + "name": "BOX암거", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#3b6fd4", + "abbr": "암거" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "body_width_m", + "label": "본체 폭", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "revet_foundation", + "label": "기슭막이 기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": false, + "phase": "detail" + }, + { + "key": "body_height_m", + "label": "본체 높이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "length_m", + "label": "연장(계류 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "empty_means": "비우면 수량이 안 섬 — 구체 길이는 설계자 입력(기본값 없음 · 2026-09-15 브레인 판정 ③)" + }, + { + "key": "wall_thickness_m", + "label": "측벽 두께", + "input": "number", + "unit": "m", + "default": 0.3, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 벽 0.3 · KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "slab_thickness_m", + "label": "상·저판 두께", + "input": "number", + "unit": "m", + "default": 0.25, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 판 0.25 — ⚠ KDS 44 90 00 4.5.2 도로암거 부재 최소두께 300㎜ 에 못 미침(임도 예제 0.25 · KDS 도로 최소 300㎜) · 설계자가 고름", + "required": false, + "phase": "detail" + }, + { + "key": "haunch_m", + "label": "헌치(모따기) 크기", + "input": "number", + "unit": "m", + "default": 0.2, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 0.2×0.2 · 실무 울진 기번3 구조도 2×2 도 0.2 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "blinding_thickness_m", + "label": "기초(버림) 두께", + "input": "number", + "unit": "m", + "default": 0.1, + "default_basis": "제안값 · 산림과임업기술(임도) 5장 수량산출 예제 기초 0.1(양쪽 0.1 여유) · 실무 울진 기번3 구조도 2×2 버림 0.1 · 바꿀 수 있음", + "required": false, + "phase": "detail" + }, + { + "key": "trench_depth_m", + "label": "평균 터파기고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 구체 터파기·되메우기가 안 섬 — 원지반에서 터파기 바닥(기초잡석 밑)까지 · 토공집계로 감(2026-09-15 브레인 판정 ③)" + }, + { + "key": "wing_in", + "label": "날개벽(유입)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_in_height_m", + "label": "유입 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_in_length_m", + "label": "유입 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_in_angle_deg", + "label": "유입 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + }, + { + "key": "wing_out", + "label": "날개벽(유출)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_out_height_m", + "label": "유출 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_out_length_m", + "label": "유출 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_out_angle_deg", + "label": "유출 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + } + ] + }, + { + "type_id": "ford_pavement", + "group": "A", + "name": "물넘이포장", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#2f9e8f", + "abbr": "물넘이" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "ford_width_m", + "label": "월류 폭", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "detail", + "default_basis": "물넘이 월류 폭 기본 5m — 사용자 확정값(엔진 FORD_PAVEMENT_DEFAULT_WIDTH_M 과 같음)" + }, + { + "key": "ford_slope_pct", + "label": "바닥 경사(유입→유출)", + "input": "number", + "unit": "%", + "default": null, + "required": false + }, + { + "key": "thickness_cm", + "label": "포장 두께", + "input": "number", + "unit": "㎝", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 표가 안 서고 「두께를 적어야 섬」 사유가 뜸 — 두께로 관측 원단위(울진 20㎝)를 고름(2026-09-14 A3)" + }, + { + "key": "length_m", + "label": "포장 길이(노폭 방향)", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 면적(월류 폭 × 길이)이 0 이라 표가 안 서고 사유가 뜸 — 노견까지 전폭을 적음(2026-09-14 A3)" + } + ] + }, + { + "type_id": "ford_bridge", + "group": "A", + "name": "세월교", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#28857a", + "abbr": "세월교" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "pipe_kind", + "label": "관종", + "input": "select", + "choices": [ + "흄관", + "VR관", + "파형강관" + ], + "default": "파형강관", + "required": false + }, + { + "key": "pipe_diameter_mm", + "label": "관경", + "input": "select", + "choices": [ + "800", + "1000", + "1200", + "1500" + ], + "unit": "mm", + "default": "1000", + "required": false + }, + { + "key": "pipe_count", + "label": "배관 수량", + "input": "number", + "unit": "련", + "default": null, + "required": false + }, + { + "key": "ford_width_m", + "label": "월류 폭", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "detail", + "default_basis": "세월교 월류 폭 기본 10m — 사용자 확정값(엔진 FORD_DEFAULT_WIDTH_M 과 같음)" + }, + { + "key": "wing_in", + "label": "날개벽(유입)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_in_height_m", + "label": "유입 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_in_length_m", + "label": "유입 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_in_angle_deg", + "label": "유입 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + }, + { + "key": "wing_out", + "label": "날개벽(유출)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": "있음", + "required": false + }, + { + "key": "wing_out_height_m", + "label": "유출 날개벽 짧은쪽 높이", + "input": "number", + "unit": "m", + "default": 1, + "required": false + }, + { + "key": "wing_out_length_m", + "label": "유출 날개벽 길이", + "input": "number", + "unit": "m", + "default": 2, + "required": false + }, + { + "key": "wing_out_angle_deg", + "label": "유출 날개벽 각도", + "input": "number", + "unit": "°", + "default": 45, + "required": false + } + ] + }, + { + "type_id": "cross_drain_exposed", + "group": "A", + "name": "노출형 횡단수로", + "placement": "point", + "style": { + "color": "#5aa7e8", + "abbr": "횡수로" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "quantity" + ], + "options": [] + }, + { + "type_id": "open_ditch", + "group": "A", + "name": "개거(겉도랑)", + "placement": "point", + "style": { + "color": "#6d9bd1", + "abbr": "개거" + }, + "drawing_views": [ + "plan", + "profile", + "cross_section", + "quantity" + ], + "options": [] + }, + { + "type_id": "ditch_side", + "enabled": false, + "group": "B", + "name": "측구(옆도랑)", + "design_owner": "횡단 설계", + "placement": "interval", + "style": { + "color": "#2eaadc", + "abbr": "측구" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "일반형(제형)", + "L형", + "U형", + "J형" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "depth_cm", + "label": "깊이", + "input": "number", + "unit": "cm", + "default": 30 + } + ] + }, + { + "type_id": "ditch_ridge", + "enabled": false, + "group": "B", + "name": "산마루측구", + "placement": "interval", + "style": { + "color": "#2e8bdc", + "abbr": "산마루" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "콘크리트", + "돌", + "떼" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "ditch_berm", + "enabled": false, + "group": "B", + "name": "소단측구", + "placement": "interval", + "style": { + "color": "#2ec5dc", + "abbr": "소단구" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "width_m", + "label": "폭", + "input": "number", + "unit": "m", + "default": 0.5, + "required": false, + "phase": "b05" + }, + { + "key": "interval_m", + "label": "간격(사면길이)", + "input": "number", + "unit": "m", + "default": 3, + "required": false, + "phase": "b05" + }, + { + "key": "slope_deg", + "label": "사면 쪽 기울기", + "input": "number", + "unit": "°", + "default": 0, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "chute", + "enabled": false, + "group": "B", + "name": "도수로·산비탈수로", + "placement": "interval", + "style": { + "color": "#1f9bb8", + "abbr": "도수로" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "떼", + "돌", + "콘크리트", + "블록판", + "벤치플륨관", + "반원관" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "section", + "label": "단면형", + "input": "select", + "choices": [ + "반달꼴", + "U자형", + "사다리꼴" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "slope_drain", + "enabled": false, + "group": "B", + "name": "절토사면 배수로", + "placement": "interval", + "style": { + "color": "#28b1a1", + "abbr": "사면로" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "material", + "label": "재료", + "input": "select", + "choices": [ + "떼", + "돌" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "underdrain", + "enabled": false, + "group": "B", + "name": "맹암거", + "placement": "interval", + "style": { + "color": "#1f7fb8", + "abbr": "맹암거" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "note", + "label": "규격", + "input": "text", + "default": "" + } + ] + }, + { + "type_id": "retaining_wall", + "group": "C", + "name": "옹벽", + "placement": "interval", + "style": { + "color": "#e07a2e", + "abbr": "옹벽" + }, + "drawing_views": [ + "profile", + "cross_section", + "detail", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2, + "default_basis": "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음", + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "중력식", + "반중력식", + "캔틸레버식", + "부벽식", + "식생옹벽블럭" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + } + ] + }, + { + "type_id": "masonry_wet", + "group": "C", + "name": "돌쌓기(찰)", + "placement": "interval", + "style": { + "color": "#d0692a", + "abbr": "찰쌓기" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이(≤3.0)", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "masonry_dry", + "group": "C", + "name": "돌쌓기(메)", + "placement": "interval", + "style": { + "color": "#c25d2c", + "abbr": "메쌓기" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이(≤2.0)", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)", + "warn_above": 2, + "warn_message": "메쌓기 높이 기준을 넘음 — 임도기술교본 7-3:27 「메쌓기는 2.0m 이하」 · 품셈 13-4-4 [주]⑪(원문 L7192) 「높이 3m 이상이면 전부 또는 하부를 찰쌓기」 · 막지 않음(설계자 확인)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "soil_guard", + "group": "C", + "name": "흙막이", + "placement": "interval", + "style": { + "color": "#b8742e", + "abbr": "흙막이" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "콘크리트", + "돌(찰)", + "돌(메)", + "블록쌓기", + "돌망태", + "흙포대", + "통나무쌓기", + "떼" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "tiers", + "label": "단 수(다단)", + "input": "number", + "unit": "단", + "default": 1, + "required": false, + "phase": "b05", + "default_basis": "1 = 다단 없음 — 도메인 수치가 아니라 「단이 하나」라는 표식(기슭막이와 같은 벌)" + }, + { + "key": "lift_m", + "label": "기준 올림(사면 위로)", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로 — 올리지 않음을 뜻함(기슭막이와 같은 벌)" + }, + { + "key": "shift_m", + "label": "기준 좌우 이동", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로 — 옮기지 않음을 뜻함(기슭막이와 같은 벌)" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 돌쌓기 기본값으로 서고 「기본값으로 섰음」이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "default_basis": "「채집」 — 별표2 「야면석 등은 가급적 현장에서 채취·사용」 권고. 구조물마다 바꿀 수 있음", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 흙막이는 「떼」만 서고 치수가 정본 평균 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + } + ] + }, + { + "type_id": "boulder_masonry", + "group": "C", + "name": "큰돌쌓기", + "placement": "interval", + "style": { + "color": "#a86a2a", + "abbr": "큰돌" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 2.5, + "required": false, + "phase": "b05", + "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "stone_cm", + "label": "돌규격", + "input": "select", + "choices": [ + "40~60", + "60~80", + "80~100" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "bond", + "label": "쌓기 방식", + "input": "select", + "choices": [ + "메쌓기", + "찰쌓기" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기(13-6)는 채움 콘크리트를 품에 포함 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "default_basis": "「자동(성토 쪽)」 — 칸이 생기기 전의 동작을 이어받는 표식" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」", + "not_in_table": "아직 표에 안 쓰임 — 큰돌쌓기는 물빼기 관을 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "berm", + "group": "C", + "name": "소단", + "placement": "interval", + "style": { + "color": "#7f9a63", + "abbr": "소단" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "width_m", + "label": "폭", + "input": "number", + "unit": "m", + "default": 0.5, + "required": false, + "phase": "b05" + }, + { + "key": "interval_m", + "label": "간격(사면길이)", + "input": "number", + "unit": "m", + "default": 3, + "required": false, + "phase": "b05" + }, + { + "key": "slope_deg", + "label": "안쪽 기울기", + "input": "number", + "unit": "°", + "default": 0, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "erosion_check", + "group": "D", + "name": "골막이", + "placement": "point", + "style": { + "color": "#7a52c7", + "abbr": "골막이" + }, + "drawing_views": [ + "plan", + "profile", + "detail", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "돌", + "돌망태", + "콘크리트", + "통나무", + "바자", + "흙" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "spillway", + "label": "방수로", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 골막이 두께는 정본 식(ℓ3+0.1H · ℓ3+0.4H)으로만 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "back_len_cm", + "label": "뒷길이 ℓ3", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 골막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 골막이는 계수 열을 아직 안 가름 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "top_length_m", + "label": "상장 ⓐ (윗변)", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "bottom_length_m", + "label": "하장 ⓑ (아랫변)", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 4.5 + }, + { + "key": "height_m", + "label": "높이(≤2.0)", + "input": "number", + "unit": "m", + "default": 2 + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + } + ] + }, + { + "type_id": "bed_sill", + "group": "D", + "name": "바닥막이", + "placement": "point", + "style": { + "color": "#7c5f3b", + "abbr": "바닥" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "form", + "label": "형태", + "input": "select", + "choices": [ + "돌붙임(찰)", + "돌붙임(메)" + ], + "default": "돌붙임(메)", + "required": false, + "phase": "b05" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이는 채집석 공제를 아직 안 셈 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이 원단위는 돌붙임 정본 표 붙박이 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김", + "not_in_table": "아직 표에 안 쓰임 — 바닥막이는 평면적(돌붙임 ㎡)이라 기울기 몫이 없음 · 넣어도 수량·금액이 안 바뀜(2026-09-14)" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": 0.3, + "required": false, + "phase": "b05" + } + ] + }, + { + "type_id": "check_dam_small", + "group": "D", + "name": "소형사방댐(복합형)", + "placement": "point", + "style": { + "color": "#6b46b8", + "abbr": "사방댐" + }, + "drawing_views": [ + "plan", + "profile", + "detail", + "quantity" + ], + "options": [ + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "revetment", + "group": "D", + "name": "기슭막이", + "placement": "point", + "managed_by": "pipe_points", + "style": { + "color": "#9b6bdc", + "abbr": "기슭" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "side", + "label": "설치 측", + "input": "select", + "choices": [ + "양쪽", + "좌", + "우" + ], + "default": "양쪽", + "required": false, + "phase": "b05", + "default_basis": "「좌」는 셀렉트 첫 항목일 뿐 도메인 확정값이 아님 — 측점마다 사용자가 고름" + }, + { + "key": "tiers", + "label": "단 수(다단)", + "input": "number", + "unit": "단", + "default": 1, + "required": false, + "phase": "b05", + "default_basis": "1 = 다단 없음 — 도메인 수치가 아니라 「단이 하나」라는 표식" + }, + { + "key": "lift_m", + "label": "기준 올림(사면 위로)", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로" + }, + { + "key": "shift_m", + "label": "기준 좌우 이동", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "b05", + "default_basis": "0 = 자동 자리 그대로" + }, + { + "key": "form", + "label": "형태", + "input": "select", + "choices": [ + "돌쌓기(찰)", + "돌쌓기(메)", + "콘크리트", + "돌망태", + "통나무·목재틀", + "바자" + ], + "default": "돌쌓기(메)", + "required": false, + "phase": "b05" + }, + { + "key": "thickness_top_m", + "label": "상부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 실무 구조물도 식으로 돎(뒷길이 + 0.30) — 값을 넣으면 그 값이 이김" + }, + { + "key": "thickness_bottom_m", + "label": "하부 두께", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 상부 + 0.30×(H−1) 로 돎 — 값을 넣으면 그 값이 이김" + }, + { + "key": "back_len_cm", + "label": "뒷길이", + "input": "select", + "choices": [ + "25", + "30", + "35", + "45", + "55", + "60", + "75" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 13-4-4 [주]⑩ 뒷길이 표준 하한(높이·찰/메로 · 범위 안 가장 작은 규격 — 예 메쌓기 1.5~3m 는 36~45㎝ 라 45㎝)으로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검" + }, + { + "key": "stone_kind", + "label": "돌 종류", + "input": "select", + "choices": [ + "야면석·호박돌", + "깬잡석", + "깬돌", + "견치돌" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 야면석 계수 열로 서고 그 사실이 줄로 뜸 — 돌이 아닌 형태도 있어 필수로 못 검" + }, + { + "key": "stone_supply", + "label": "조달", + "input": "select", + "choices": [ + "채집", + "구입" + ], + "default": "채집", + "required": false, + "phase": "detail" + }, + { + "key": "stone_coeff_basis", + "label": "야면석 계수", + "input": "select", + "choices": [ + "품셈", + "실무 관행" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 열로 돎 — 실무 관행 열은 골라야 씀" + }, + { + "key": "fill_concrete_mpa", + "label": "채움 강도", + "input": "select", + "choices": [ + "180", + "210" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 계산 쪽 기준 강도로 돎" + }, + { + "key": "face_slope_ratio", + "label": "전면 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김" + }, + { + "key": "face_role", + "label": "성토/절토", + "input": "select", + "choices": [ + "성토", + "절토" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김" + }, + { + "key": "foundation", + "label": "기초", + "input": "select", + "choices": [ + "기초유", + "기초버림" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "height_m", + "label": "높이", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "b05", + "empty_means": "설계자 입력 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181 · 사방(계류) 교본 기준). 비우면 줄만 서고 미확정이라 금액에 안 들어감 · 옛 기본 2.5 는 근거 없어 걷음(2026-09-14 브레인 판정)" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10, + "required": false, + "phase": "b05" + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5, + "required": false, + "phase": "b05" + }, + { + "key": "weep_hole_diameter_mm", + "label": "물빼기 구멍 지름", + "input": "number", + "unit": "㎜", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 Ø50 으로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "weep_hole_area_m2", + "label": "물빼기 구멍 1개당 벽면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 국가기준 2㎡당 1개로 돎 — 사용자 확정 「국가기준 + 숫자 변경 가능하게」" + }, + { + "key": "blinding_concrete", + "label": "버림 콘크리트", + "input": "select", + "choices": [ + "넣음", + "안 넣음" + ], + "default": "넣음", + "default_basis": "사용자 확정 ⑭ 「기본은 넣음」 — 두께는 KDS 44 90 00 의 100㎜", + "required": false, + "phase": "detail", + "empty_means": "비워도 넣음 — 「안 넣음」을 골라야 그 줄이 빠짐(B08 `wants_blinding`)" + } + ] + }, + { + "type_id": "refuge", + "group": "E", + "name": "대피소", + "placement": "interval", + "style": { + "color": "#2fa84f", + "abbr": "대피소" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "length_m", + "label": "유효길이", + "input": "number", + "unit": "m", + "default": 15 + } + ] + }, + { + "type_id": "work_yard", + "group": "E", + "name": "정차·작업장", + "placement": "interval", + "style": { + "color": "#268f43", + "abbr": "작업장" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": 7 + }, + { + "key": "length_m", + "label": "유효길이", + "input": "number", + "unit": "m", + "default": 20 + } + ] + }, + { + "type_id": "turnaround", + "group": "E", + "name": "차돌림곳", + "placement": "site", + "style": { + "color": "#37b95a", + "abbr": "차돌림" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "width_m", + "label": "너비", + "input": "number", + "unit": "m", + "default": 10 + } + ] + }, + { + "type_id": "mirror", + "group": "E", + "name": "반사경", + "placement": "point", + "style": { + "color": "#43c76b", + "abbr": "반사경" + }, + "drawing_views": [ + "plan" + ], + "options": [ + { + "key": "facing", + "label": "방향", + "input": "select", + "choices": [ + "좌", + "우", + "양면" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "guardrail", + "group": "E", + "name": "가드레일·경계석·위험표지", + "placement": "interval", + "style": { + "color": "#1f9e46", + "abbr": "가드" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "kind", + "label": "종류", + "input": "select", + "choices": [ + "가드레일", + "경계석", + "위험표지" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "position_sign", + "group": "E", + "name": "국가지점번호판", + "placement": "point", + "style": { + "color": "#53a832", + "abbr": "번호판" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "barrier_gate", + "group": "E", + "name": "차단기", + "placement": "point", + "style": { + "color": "#3f9e2e", + "abbr": "차단기" + }, + "drawing_views": [ + "plan", + "detail" + ], + "options": [] + }, + { + "type_id": "completion_sign", + "group": "E", + "name": "준공표지판", + "placement": "point", + "style": { + "color": "#66b344", + "abbr": "준공" + }, + "drawing_views": [ + "plan" + ], + "options": [] + }, + { + "type_id": "spoil_bank", + "group": "E", + "name": "유용토운반작업장(구 사토장)", + "placement": "interval", + "style": { + "color": "#8c9a2e", + "abbr": "사토장" + }, + "drawing_views": [ + "plan", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "side", + "label": "쌓는 쪽", + "input": "select", + "choices": [ + "자동(성토 쪽)", + "좌", + "우" + ], + "default": "자동(성토 쪽)", + "required": false, + "phase": "b05", + "default_basis": "「자동(성토 쪽)」 — 흙은 성토 쪽에 쌓는다는 지금까지의 동작을 이어받는 표식. 측점마다 좌·우로 바꿈" + }, + { + "key": "fill_slope_ratio", + "label": "비탈 기울기 (1:n 의 n)", + "input": "number", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 그 측점의 노선 성토 기울기를 그대로 씀 — 지식DB 가 「기본 비탈기울기는 근거에 없다」고 못 박은 자리" + }, + { + "key": "extra_distance_m", + "label": "추가 운반거리", + "input": "number", + "unit": "m", + "default": 0, + "required": false, + "phase": "detail", + "default_basis": "0 = 「추가 없음」 — 노선 안 사토장은 측점 누가거리로 거리가 나오므로 이 칸은 외부 사토장 대비 여벌" + }, + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "borrow_pit", + "group": "E", + "name": "토사채취장(토취장)", + "placement": "site", + "style": { + "color": "#7d8a2a", + "abbr": "토취장" + }, + "drawing_views": [ + "plan", + "quantity" + ], + "options": [ + { + "key": "area_m2", + "label": "면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "capacity_m3", + "label": "용량", + "input": "number", + "unit": "㎥", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "wildlife_path", + "enabled": false, + "group": "F", + "name": "야생동물 이동통로", + "placement": "point", + "style": { + "color": "#3aa88f", + "abbr": "동물길" + }, + "drawing_views": [ + "plan", + "cross_section" + ], + "options": [ + { + "key": "form", + "label": "형식", + "input": "select", + "choices": [ + "경사로", + "자연형계단" + ], + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "revegetation", + "enabled": false, + "group": "F", + "name": "비탈면 녹화", + "placement": "interval", + "style": { + "color": "#4cb84c", + "abbr": "녹화" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "method", + "label": "공법", + "input": "select", + "choices": [ + "씨뿌리기(줄파종)", + "새심기", + "선떼붙이기", + "줄떼", + "평떼", + "비탈덮기", + "녹생토 뿜어붙이기", + "조공", + "편책", + "나무심기" + ], + "default": null, + "default_basis": "「초류종자 살포」는 여기 없음 — 토공집계에서 사면 전체로 섬(품셈 5-24 · 두 곳에 두면 이중계상 · 2026-09-15 브레인)", + "required": true, + "phase": "detail" + }, + { + "key": "face", + "label": "대상 면", + "input": "select", + "choices": [ + "절토면", + "성토면", + "양쪽" + ], + "default": null, + "required": true, + "phase": "detail" + }, + { + "key": "cover_kind", + "label": "덮개(비탈덮기)", + "input": "select", + "choices": [ + "거적", + "짚망", + "방초·야자섬유매트" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비탈덮기일 때만 고름 — 비면 비탈덮기 줄이 안 섬(품셈 5-25 거적덮기 ㎡ · 5-28-1 짚망 · 5-28-2 매트 · 재료가 공종을 가름)" + }, + { + "key": "step_height_m", + "label": "단 직고", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기·조공의 단 사이 직고 — 교본은 범위로만 줌(선떼 직고 1~2m · 조공 1.0~1.2m) · 기본값 없음 · 비면 선떼 연장(품셈 5-16-1 [주]② 대상지 직고 ÷ 단 직고 × 연장)이 안 섬" + }, + { + "key": "sod_grade", + "label": "선떼 급수", + "input": "select", + "choices": [ + "1급", + "2급", + "3급", + "4급", + "5급", + "6급", + "7급", + "8급", + "9급" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "선떼붙이기의 급수 — 교본은 1~9급 범위만(6~7급을 많이 씀) · 기본값 없음 · 비면 m당 떼 매수(사방기술교본 표 2-2-6)가 안 섬" + }, + { + "key": "sod_size", + "label": "선떼 떼 크기", + "input": "select", + "choices": [ + "대형 40×40", + "소형 18×18" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "사방기술교본 표 2-2-6 이 대형(40×40)·소형(18×18) 두 줄을 나란히 줌 — 우리가 고를 자리가 아니라 기본값 없음 · 비면 선떼 떼 매수가 안 섬" + }, + { + "key": "cutting_soil", + "label": "단끊기 토질", + "input": "select", + "choices": [ + "보통토사", + "경질ㆍ고사점토 및 자갈섞인 점토", + "호박돌 섞인 토사" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "품셈 5-16-1 단끊기 표의 토질 셋 — B06 지반(토사/암)과 다른 축이라 자동으로 안 고름 · 비면 단끊기 조각만 막힘" + } + ] + }, + { + "type_id": "pavement_concrete", + "group": "G", + "name": "콘크리트 포장", + "placement": "interval", + "style": { + "color": "#8a8f98", + "abbr": "포장" + }, + "drawing_views": [ + "profile", + "cross_section", + "quantity" + ], + "options": [ + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": 10 + }, + { + "key": "before_m", + "label": "기준측점 전", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "after_m", + "label": "기준측점 후", + "input": "number", + "unit": "m", + "default": 5 + }, + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 포장층 두께(표준 횡단면 설정 · 기본 0.2m = 임도기술교본 3-2 「두께 0.2m」)를 이음 — 적으면 적은 값이 이김" + }, + { + "key": "width_m", + "label": "포장 폭", + "input": "number", + "unit": "m", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 노폭(구간 측점의 차도 표준 폭 · 교본 3-2 「포장 폭 3.0m」 · 실무 관측 3.5~4.0)을 이음 — 적으면 적은 값이 이김" + }, + { + "key": "widening_area_m2", + "label": "확폭 면적", + "input": "number", + "unit": "㎡", + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 B06 곡선부 확폭을 측점 사이 평균 × 거리로 더함(교본 3-2 「곡선부에는 확폭」) — 확폭이 없으면 0 을 적을 것" + }, + { + "key": "joint_spacing_m", + "label": "수축줄눈 간격", + "input": "number", + "unit": "m", + "default": 6, + "default_basis": "제안값 · 임도기술교본 부록 4-7 7-1 3.5.1 「수축줄눈의 간격은 4-6m를 기준」 · 실무 소광 6m 관측 · 바꿀 수 있음", + "phase": "detail" + }, + { + "key": "separation_sheet", + "label": "비닐(깔기·양생)", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 비닐 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「양생 재료비(비닐 등)는 별도 계상」이라 설계자가 고름(기본값 없음)" + }, + { + "key": "wire_mesh", + "label": "철망", + "input": "select", + "choices": [ + "있음", + "없음" + ], + "default": null, + "required": false, + "phase": "detail", + "empty_means": "비우면 철망 재료가 안 서고 사유가 뜸 — 품셈 12-6 [주]② 「철망재료비는 별도 계상」 · 교본 부록 7-1 3.4 「설계도서에 따라」 · 실무 소광 「철망포함」 관측" + } + ] + }, + { + "type_id": "gravel_surfacing", + "enabled": false, + "group": "G", + "name": "쇄석(자갈) 포설", + "placement": "interval", + "style": { + "color": "#9aa0a8", + "abbr": "쇄석" + }, + "drawing_views": [ + "cross_section", + "quantity" + ], + "options": [ + { + "key": "thickness_cm", + "label": "두께", + "input": "number", + "unit": "cm", + "default": null, + "required": true, + "phase": "detail" + } + ] + }, + { + "type_id": "etc", + "group": "호환", + "name": "기타", + "placement": "point", + "style": { + "color": "#98a2b3", + "abbr": "기타" + }, + "drawing_views": [ + "profile" + ], + "options": [ + { + "key": "name", + "label": "이름", + "input": "text", + "default": "기타 구조물" + } + ] + } + ], + "종류": { + "pipe": { + "동작": [ + "계곡통과시설", + "유입유출_묶음", + "기슭막이_한벌" + ], + "이름": { + "pipe.diameter": "관경", + "pipe.diameter=800": "800", + "pipe.diameter=1000": "1000", + "pipe.diameter=1200": "1200", + "pipe.diameter=1500": "1500", + "pipe.length": "관 길이 (m)", + "inlet.structure": "구조", + "inlet.structure=집수정": "집수정", + "inlet.structure=기슭막이": "흙막이", + "inlet.form": "형태", + "inlet.form=돌쌓기(찰)": "돌 쌓기(찰)", + "inlet.form=돌쌓기(메)": "돌 쌓기(메)", + "inlet.form=콘크리트": "콘크리트", + "inlet.form=돌망태": "돌망태", + "inlet.form=통나무·목재틀": "통나무/목재틀", + "inlet.form=바자": "바자", + "inlet.form=I": "I형", + "inlet.form=L": "L형", + "inlet.form=U": "ㄷ형", + "inlet.length": "길이 (m)", + "inlet.height": "높이 (m)", + "inlet.before": "기준측점 전 (m)", + "inlet.after": "기준측점 후 (m)", + "outlet.structure": "구조", + "outlet.structure=기슭막이": "흙막이", + "outlet.form": "형태", + "outlet.form=돌쌓기(찰)": "돌 쌓기(찰)", + "outlet.form=돌쌓기(메)": "돌 쌓기(메)", + "outlet.form=콘크리트": "콘크리트", + "outlet.form=돌망태": "돌망태", + "outlet.form=통나무·목재틀": "통나무/목재틀", + "outlet.form=바자": "바자", + "outlet.length": "길이 (m)", + "outlet.height": "높이 (m)", + "outlet.before": "기준측점 전 (m)", + "outlet.after": "기준측점 후 (m)", + "soil_add": "흙막이 추가", + "soil_add.structure": "구조", + "soil_add.structure=기슭막이": "흙막이", + "soil_add.form": "형태", + "soil_add.form=돌쌓기(찰)": "돌 쌓기(찰)", + "soil_add.form=돌쌓기(메)": "돌 쌓기(메)", + "soil_add.form=콘크리트": "콘크리트", + "soil_add.form=돌망태": "돌망태", + "soil_add.form=통나무·목재틀": "통나무/목재틀", + "soil_add.form=바자": "바자", + "soil_add.length": "길이 (m)", + "soil_add.height": "높이 (m)", + "soil_add.before": "기준측점 전 (m)", + "soil_add.after": "기준측점 후 (m)", + "chute_add": "도수로/돌붙임", + "chute_add.structure": "구조", + "chute_add.structure=도수로": "도수로", + "chute_add.structure=돌붙임": "돌붙임" + } + }, + "box_culvert": { + "동작": [ + "계곡통과시설", + "BOX_본체규격", + "날개벽" + ] + }, + "ford_pavement": { + "동작": [ + "계곡통과시설", + "월류단면" + ] + }, + "ford_bridge": { + "동작": [ + "계곡통과시설", + "월류단면", + "날개벽" + ] + }, + "revetment": { + "동작": [ + "계곡통과시설", + "기슭막이_한벌" + ] + }, + "pavement_concrete": { + "동작": [ + "포장_물넘이_겹침" + ] + } + }, + "표시": { + "pipe": { + "pipe_kind": false + } + }, + "그룹": { + "pipe": [ + { + "id": "grv6lny", + "이름": "관", + "옵션": [ + "pipe_kind", + "pipe_diameter_mm" + ], + "형상": {} + }, + { + "id": "gnbf9m9", + "이름": "관 보호공(유입)", + "옵션": [ + "inlet_type" + ], + "형상": {} + }, + { + "id": "g9lq9uk", + "이름": "관 보호공(유출)", + "옵션": [], + "형상": {} + } + ] + }, + "배치": {}, + "규칙": [] +} diff --git a/resources/tester/test_52_6_cross_underdrain.py b/resources/tester/test_52_6_cross_underdrain.py new file mode 100644 index 000000000..b8146e009 --- /dev/null +++ b/resources/tester/test_52_6_cross_underdrain.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""52-6 횡단배수 › 맹암거(표 06 · 새 종류) — 정의 · 컨테이너 · 서버 저장 왕복. + +새 키 `cross_underdrain`(횡단배수 · 점형) — 종단배수 맹암거(`underdrain`)와 따로. +컨테이너 = 정의 그룹 「맹암거」(필수 · 늘 한 벌) — 암거 높이(m) · 암거 경사(%) 손 입력(비워 둘 수 있음) · +그룹 잇기(`group_items`)로 구조물 조합 항목. 저장은 임시 storage 에서만. +""" + +import pytest + +from B05_Profile.B05_Profile_Structures_Schema import ( + StructureInstance, + is_station_planting_type, + load_definition, + structure_type_map, +) + +KEY = "cross_underdrain" +GROUP = "gcrossud" + + +def test_새_종류는_횡단배수_점형_종단배수_맹암거와_따로(): + types = structure_type_map() + new, old = types[KEY], types["underdrain"] + assert (new.group, new.placement, new.name, new.enabled) == ("A", "point", "맹암거", True) + assert (old.group, old.placement) == ("B", "interval") + assert new.style["abbr"] != old.style["abbr"] and new.style["color"] != old.style["color"] + assert is_station_planting_type(new) # 횡단배수 = 그 측점에 횡단도 + + +def test_컨테이너_맹암거_높이_경사_손_입력(): + opts = {o.key: o for o in structure_type_map()[KEY].options} + assert list(opts) == ["culvert_height_m", "culvert_slope_pct"] + for key, label, unit in ( + ("culvert_height_m", "암거 높이", "m"), + ("culvert_slope_pct", "암거 경사", "%"), + ): + o = opts[key] + assert (o.label, o.input, o.unit, o.default, o.required) == ( + label, + "number", + unit, + None, + False, + ) + assert o.empty_means # 손 입력 · 기본값 근거 없음 — 비워 둘 수 있음(B05 단계 필수 금지) + groups = load_definition()["그룹"][KEY] + assert groups == [ + {"id": GROUP, "이름": "맹암거", "옵션": list(opts), "형상": {}}, + ] + + +def test_정의_검사_통과(): + from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition + + assert definition.check(load_definition()) == [] + + +def _item(**options): + return StructureInstance( + type_id=KEY, + placement="point", + chainage_m=63.5, + options=options, + group_items={ + GROUP: {"layer": "system", "owner": None, "key": "SS000001", "name": "맹암거"} + }, + ) + + +def test_저장_다시_읽기_그대로(tmp_path): + from B05_Profile.B05_Profile_Structures_Repository import load_structures, save_structures + + item = _item(culvert_height_m=0.6, culvert_slope_pct=2.5) + save_structures(str(tmp_path), [item], base_revision=0) + _, loaded = load_structures(str(tmp_path)) + assert [(s.type_id, s.chainage_m, s.options) for s in loaded] == [ + (KEY, 63.5, {"culvert_height_m": 0.6, "culvert_slope_pct": 2.5}) + ] + assert loaded[0].group_items == item.group_items + + +@pytest.mark.parametrize( + "options", + [{"culvert_height_m": -1}, {"culvert_slope_pct": -0.5}], +) +def test_음수는_서버가_막음(tmp_path, options): + from B05_Profile.B05_Profile_Structures_Repository import save_structures + + with pytest.raises(ValueError): + save_structures(str(tmp_path), [_item(**options)], base_revision=0) diff --git a/resources/tester/test_b05_structures_define.py b/resources/tester/test_b05_structures_define.py index 1cb497663..dd67ca2c2 100644 --- a/resources/tester/test_b05_structures_define.py +++ b/resources/tester/test_b05_structures_define.py @@ -74,7 +74,7 @@ def test_정의_덧칸_얹기(out): def test_판1_정의는_지금_화면과_같음(out): - assert out["v1Types"] == 37 + assert out["v1Types"] == 38 # 52-6 횡단배수 맹암거 더함 assert out["v1Same"] is True assert out["v1Columns"] == [2] assert out["v1Anchor"] is True diff --git a/resources/tester/test_b05_structures_registry_policy.py b/resources/tester/test_b05_structures_registry_policy.py index d17cd6d55..c6fa19025 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군 3(노출형·개거·맹암거) + C군 5 + D군 3 + E군 10 + G군 1(콘크리트 포장) + 기타 1. 콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로 받는 정본이 됐다(종단경사 자동 포장 적용을 대체). @@ -202,7 +202,7 @@ def test_all_enabled_non_managed_types_are_quick_addable(): 그래서 C군 구간형 시설과 같은 자리에 선다. (측구쪽 `ditch_berm` 은 우클릭 추가 대상이 아니라 여기 수에 안 잡힌다.) """ - assert len(_quick_add_types()) == 23 + assert len(_quick_add_types()) == 24 def test_removed_types_are_absent(): diff --git a/resources/tester/ui_checks/52-6_횡단맹암거.js b/resources/tester/ui_checks/52-6_횡단맹암거.js new file mode 100644 index 000000000..2ce14c7ee --- /dev/null +++ b/resources/tester/ui_checks/52-6_횡단맹암거.js @@ -0,0 +1,216 @@ +/* 52-6 — 횡단배수 › 맹암거(표 06 · 새 종류 `cross_underdrain`) · B05 · B06 「구조물 배치」 + * 시작 화면: B05 종단설계(#/b05-profile) · 아무것도 안 고름 · 측점 3+7 이 비어 있음 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m526 = "단계";` 를 붙여 + * orca eval --expression "window.__m526='edit'; $(cat 이 파일)" → 종류 목록 · 컨테이너 · 값 · 잇기 · [추가] · 다시 열기 + * orca eval --expression "window.__m526='b06'; $(cat 이 파일)" → B06 에서 같은 항목 다시 열기 · B05 와 같은 그리기 + * orca eval --expression "window.__m526='save'; $(cat 이 파일)" → [저장] 요청 가로챔(몸통만 봄) + * orca eval --expression "window.__m526='restore'; $(cat 이 파일)" → sessionStorage 를 처음 그대로 · 뒤에 orca reload + * 쓰는 것: 없음 — [저장] 은 fetch 를 가로채 서버로 안 보냄(공유 storage) · 서버 저장 왕복은 test_52_6_cross_underdrain + * 기대 수치(edit): b05Types 에 cross_underdrain:맹암거 · underdrain 없음 · box 맹암거 · keys 높이 · 경사 · + * pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음 + * 기대 수치(b06): b06Types A 에 맹암거 · B 에 종단배수 맹암거 따로 · sameDrawing · 값 같음 + * 기대 수치(save): saved = PUT 몸통에 같은 값(67 m · 0.6 · 2.5 · gcrossud) + */ +(async () => { + const MODE = window.__m526 || "edit"; + const KEY = "cross_underdrain"; + const GROUP = "gcrossud"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const vis = (n) => n && n.offsetParent !== null; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts")); + const sec = () => document.querySelector(".b05-structure-section"); + const sels = () => sec().querySelectorAll(".b05-structure__grid select"); + const choose = async (sel, value) => { + sel.value = value; + sel.dispatchEvent(new Event("change", { bubbles: true })); + await w(300); + }; + const typesOf = async (group) => { + await choose(sels()[0], group); + return [...sels()[1].options].filter((o) => o.value).map((o) => `${o.value}:${o.text}`); + }; + const box = () => sec().querySelector(`[data-b05-group="${GROUP}"]`); + const input = (k) => box()?.querySelector(`[data-option-keys="${k}"] input`); + const shape = () => { + const b = box(); + return { + legend: b?.querySelector("legend").textContent, + keys: [...(b?.querySelectorAll("[data-option-keys]") ?? [])].map( + (n) => `${n.dataset.optionKeys}|${n.querySelector("span").textContent}`, + ), + values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value), + link: b?.querySelector(".b05-structure__summary-name").textContent, + linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"), + }; + }; + const item = () => + [...document.querySelectorAll(".b05-route__irregular-list li")].find((l) => + /^3\+7.*맹암거/.test(l.textContent.trim()), + ); + const openItem = async () => { + await until(() => item()); + const li = item(); + (li.querySelector("button") ?? li).click(); + await w(600); + // 이미 고른 채로 열렸으면 누르면 풀림 — 풀렸으면 한 번 더(재선택 = 해제) + if (sels()[1].value !== KEY) (li.querySelector("button") ?? li).click(); + await until(() => vis(box())); + await w(300); + }; + const pickLink = async () => { + box().querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const pendingMine = () => + (api.readPendingStructures(projectId) ?? []).filter((s) => s.type_id === KEY); + const out = { mode: MODE }; + + if (MODE === "edit") { + sessionStorage.setItem("__526_session", JSON.stringify({ ...sessionStorage })); + out.b05Types = await typesOf("A"); + out.b05HasLongUnderdrain = (await typesOf("B")).some((t) => t.startsWith("underdrain:")); + await typesOf("A"); + await choose(sels()[1], KEY); + out.boxBeforeStation = { shown: vis(box()), locked: !!box()?.querySelector(".is-locked") }; + const pos = [...sec().querySelectorAll(".b05-structure__position-row input")].filter(vis); + for (const [el, v] of [ + [pos[0], "3"], + [pos[1], "7"], + ]) { + el.value = v; + el.dispatchEvent(new Event("input", { bubbles: true })); + el.dispatchEvent(new Event("change", { bubbles: true })); + } + await w(300); + for (const [k, v] of [ + ["culvert_height_m", "0.6"], + ["culvert_slope_pct", "2.5"], + ]) { + input(k).value = v; + input(k).dispatchEvent(new Event("input", { bubbles: true })); + input(k).dispatchEvent(new Event("change", { bubbles: true })); + } + await w(200); + await pickLink(); + out.form = shape(); + sec().querySelector(".is-primary").click(); // [추가] + await w(800); + const mine = pendingMine(); + out.pending = mine.map((s) => ({ + chainage_m: s.chainage_m, + options: s.options, + groups: Object.keys(s.group_items ?? {}), + })); + // 종단 알약 — 약자 · 색이 종단배수 맹암거(맹암거 · #1f7fb8)와 따로 + const types = await api.fetchStructureTypes(true); + const ids = new Set(mine.map((s) => s.structure_id)); + if (!document.querySelector(".b05-structure__mark")) { + [...document.querySelectorAll("button")] + .find((b) => b.textContent.trim() === "▲종단") + ?.click(); + await until(() => document.querySelector(".b05-structure__mark"), 8000); + } + out.marks = [...document.querySelectorAll(".b05-structure__mark")] + .filter((m) => ids.has(m.dataset.structureId)) + .map((m) => `${m.textContent}|${m.style.color}`); + out.longStyle = types.find((t) => t.type_id === "underdrain")?.style; + // 다시 열기 — 폼을 비우고 목록에서 다시 고름 + const li = item(); + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + await w(400); + await openItem(); + out.reopened = shape(); + out.reopenSame = JSON.stringify(out.reopened) === JSON.stringify(out.form); + } + + if (MODE === "save") { + // [저장] — 요청을 가로채 몸통만 봄(서버 · storage 안 감) · 뒤에 화면이 서버 목록을 다시 읽어 항목이 사라짐 + const real = window.fetch; + const sent = []; + window.fetch = async (url, init = {}) => { + const method = (init.method || "GET").toUpperCase(); + if (method !== "GET" && String(url).includes(`/projects/${projectId}/`)) { + sent.push({ url: String(url), method, body: init.body ? String(init.body) : "" }); + return new Response(JSON.stringify({ status: "success", revision: 0, count: 0 }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return real(url, init); + }; + try { + document.querySelector('[data-dock-action="save"]').click(); + await w(3000); + await until(() => !document.querySelector(".ui-overlay.is-active"), 20000); + } finally { + window.fetch = real; + } + const put = sent.find((r) => r.url.includes("/route/structures") && r.method === "PUT"); + const body = put ? JSON.parse(put.body) : null; + const saved = body?.structures.filter((s) => s.type_id === KEY) ?? []; + out.saved = saved.map((s) => ({ + chainage_m: s.chainage_m, + options: s.options, + groups: Object.keys(s.group_items ?? {}), + })); + out.sent = sent.map((r) => `${r.method} ${r.url.split("/api/")[1] ?? r.url}`); + } + + if (MODE === "b06") { + await openItem(); + const b05 = shape(); + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await openItem(); + const b06 = shape(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + out.b06 = b06; + sels()[1].value = ""; + const li = item(); + (li.querySelector("button") ?? li).click(); // 고름 풂 + await w(300); + out.b06TypesA = await typesOf("A"); + out.b06TypesB = await typesOf("B"); + } + + if (MODE === "restore") { + const raw = sessionStorage.getItem("__526_session"); + if (!raw) return JSON.stringify({ error: "처음 sessionStorage 없음" }); + const before = JSON.parse(raw); + sessionStorage.clear(); + for (const [k, v] of Object.entries(before)) sessionStorage.setItem(k, v); + out.restored = JSON.stringify({ ...sessionStorage }) === JSON.stringify(before); + out.mine = pendingMine().length; + } + return JSON.stringify(out); +})(); From 103a207cee1cebe54104ee14e7288e07e6e42e19 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:42:11 +0900 Subject: [PATCH 5/9] =?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=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=9D=B4=EB=A6=84=20=EA=B3=A0=EC=B9=98=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B8(=EB=AA=85=EC=84=B8=20=EC=9E=88=EB=8A=94=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=20=C2=B7=2052-11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명세가 있는 종류(STRUCTURE_SPECS)는 컨테이너 제목 › 항목 이름 › 값 이름 나무 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 - 필수 컨테이너 제목 = 정의 그룹 이름 자리 · 나머지 = 종류.이름 표(B05 · B06 그리기와 같은 차례) - 미리보기 = B05 · B06 명세 폼에 고친 이름 · 빈 이름은 저장 막음 · [저장] = 새 판 + 이력(48장 길 그대로) - 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 명세 종류에서 숨김 · 명세 없는 종류는 그대로 - 시험: test_m02_so_def_spec(정의 사본) · ui_checks/52-11_정의_이름고치기 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s --- .../StructOptions/M02_SO_Def.ts | 40 +++-- .../StructOptions/M02_SO_Def_Model.ts | 7 +- .../StructOptions/M02_SO_Def_Spec.ts | 67 ++++++++ .../StructOptions/M02_SO_Def_Spec_Model.ts | 91 ++++++++++ .../StructOptions/M02_SO_Style.css | 25 +++ .../StructOptions/M02_SO_Text.ts | 9 + resources/tester/helper_m02_so_def_spec.cjs | 64 ++++++++ resources/tester/test_m02_so_def_spec.py | 58 +++++++ .../tester/ui_checks/52-11_정의_이름고치기.js | 155 ++++++++++++++++++ 9 files changed, 501 insertions(+), 15 deletions(-) create mode 100644 M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts create mode 100644 M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts create mode 100644 resources/tester/helper_m02_so_def_spec.cjs create mode 100644 resources/tester/test_m02_so_def_spec.py create mode 100644 resources/tester/ui_checks/52-11_정의_이름고치기.js diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts index 26899fdc1..d16428ac1 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def.ts @@ -26,6 +26,8 @@ import { } from "./M02_SO_Def_Model"; import { buildOptionsBlock } from "./M02_SO_Def_Options"; import { buildRulesBlock } from "./M02_SO_Def_Rules"; +import { buildSpecBlock } from "./M02_SO_Def_Spec"; +import { STRUCTURE_SPECS } from "../../A00_Common/design/design_structure_pipe"; import { checkBox, textBox } from "./M02_SO_Def_Ui"; import { buildGroupBlock } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; @@ -182,6 +184,8 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { } const edit = canEdit(); const extra = (): NonNullable => (d.종류[type.type_id] ??= {}); + // 명세가 있는 종류(PLAN 52) = 이름만 고침 — 옛 편집기(옵션 · 배치 · 그룹 · 규칙 · 기준 측점)는 숨김 + const spec = STRUCTURE_SPECS[type.type_id]; detail.replaceChildren( el("div", { className: "m02-sod__kindhead", @@ -196,16 +200,20 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { edit, attr: "data-sod-kind-off", }), - checkBox( - to("BaseStationShow"), - baseShown(d, type.type_id), - (v) => { - if (v) delete extra().기준측점; - else extra().기준측점 = false; - changed(); - }, - { edit, attr: "data-sod-base" }, - ), + ...(spec + ? [] + : [ + checkBox( + to("BaseStationShow"), + baseShown(d, type.type_id), + (v) => { + if (v) delete extra().기준측점; + else extra().기준측점 = false; + changed(); + }, + { edit, attr: "data-sod-base" }, + ), + ]), ...(d.종류[type.type_id]?.동작 ?? []).map((a) => { const tag = createTag(a, "neutral"); tag.title = d.동작[a] ?? ""; @@ -213,10 +221,14 @@ function drawDef(first: StructDef, opt: DefScreenOptions): void { }), ], }), - buildOptionsBlock({ def: d, type, edit, open, changed }), - buildLayoutBlock({ def: d, type, edit, changed }), - buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), - buildRulesBlock({ def: d, type, edit, changed }), + ...(spec + ? [buildSpecBlock({ def: d, type, spec, edit, changed })] + : [ + buildOptionsBlock({ def: d, type, edit, open, changed }), + buildLayoutBlock({ def: d, type, edit, changed }), + buildGroupBlock({ list: (d.그룹[type.type_id] ??= []), edit, changed }), + buildRulesBlock({ def: d, type, edit, changed }), + ]), ); }; diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts index d45d9511b..2cb6355bb 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Model.ts @@ -57,7 +57,8 @@ export interface StructDef { 구조물군: Record; 동작: Record; types: TypeDef[]; - 종류: Record; + /** 이름 = 구조물 명세 이름 바꿈 표(PLAN 52 · 키 `{컨테이너}[.{항목}[={값}]]`) */ + 종류: Record }>; 표시: Record>; 그룹: Record; 배치: Record>; @@ -257,6 +258,10 @@ export function problems(def: StructDef): string[] { if (!o.choices?.length) out.push(`${t.name}.${o.label} 고르기 꼴은 선택지가 있어야 함`); } const grp = badNames((def.그룹[t.type_id] ?? []).map((g) => g.이름)); + const spec = Object.entries(def.종류[t.type_id]?.이름 ?? {}) + .filter(([, v]) => !v.trim() || v.length > NAME_MAX) + .map(([k]) => k); + if (spec.length) out.push(`${t.name} 명세 이름: ${spec.join(", ")}`); if (grp.length) out.push(`${t.name} 그룹 이름: ${grp.join(", ")}`); const lap = overlaps(placeAll(t.options, def.배치[t.type_id])); if (lap.length) out.push(`${t.name} 배치 겹침: ${lap.join(", ")}`); diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts new file mode 100644 index 000000000..63157ee50 --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec.ts @@ -0,0 +1,67 @@ +/* ============================================================================= + * M02_SO_Def_Spec.ts + * 구조물 명세가 있는 종류(PLAN 52 · `STRUCTURE_SPECS`)의 이름 고치기 칸(52-11) — 나무 꼴 + * 컨테이너 제목 › 항목 이름 › 값 이름 · 이름만 고침 · 키 · 필수/선택 · 꼴은 읽기만 · + * 아래 미리보기 = B05 · B06 이 그리는 같은 명세 폼(`createSpecForm`)에 고친 이름을 얹음. + * 구조물군 · 종류 이름은 왼쪽 목록 · 머리 칸이 그대로 고침. + * ========================================================================== */ + +import { createTag, el } from "@ui/ui_template_elements"; +import { createSpecForm } from "../../A00_Common/design/design_structure_form"; +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef, TypeDef } from "./M02_SO_Def_Model"; +import { setSpecName, specNameRows, type SpecNameRow } from "./M02_SO_Def_Spec_Model"; +import { head, textBox } from "./M02_SO_Def_Ui"; +import { to, type OptionsTextKey } from "./M02_SO_Text"; + +const TAG: Record, OptionsTextKey> = { + required: "SpecRequired", + optional: "SpecOptional", + select: "FormSelect", + number: "FormNumber", + auto: "FormAuto", +}; + +export function buildSpecBlock(opt: { + def: StructDef; + type: TypeDef; + spec: StructureSpec; + edit: boolean; + changed: () => void; +}): HTMLElement { + const { def, type, spec, edit, changed } = opt; + const rows = specNameRows(def, spec).map((row) => + el("div", { + className: `m02-sod__specrow is-level-${row.level}`, + attrs: { "data-sod-spec": row.nameKey }, + children: [ + textBox(row.label, (v) => setSpecName(def, type.type_id, row, v), changed, { + edit, + attr: "data-sod-spec-name", + cls: "m02-sod__name", + }), + el("small", { className: "m02-opts__key", text: row.key }), + ...(row.tag ? [createTag(to(TAG[row.tag]), "neutral")] : []), + ], + }), + ); + // 미리보기 — 값 없는 새 구조물 폼 · 잇기 줄 없음(조합은 프로젝트 몫) + const preview = createSpecForm(null); + const groups = Object.fromEntries((def.그룹[type.type_id] ?? []).map((g) => [g.id, g.이름])); + preview.write(spec, {}, def.종류[type.type_id]?.이름 ?? {}, groups); + preview.root.inert = true; + return el("div", { + className: "m02-sod__block", + attrs: { "data-sod-spec-block": "" }, + children: [ + ...head(to("SpecHead"), to("SpecNote")), + ...rows, + ...head(to("SpecPreview")), + el("div", { + className: "m02-sod__specpreview", + attrs: { "data-sod-spec-preview": "" }, + children: [preview.root], + }), + ], + }); +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts new file mode 100644 index 000000000..bd6bc693b --- /dev/null +++ b/M02_MasterTemplete/StructOptions/M02_SO_Def_Spec_Model.ts @@ -0,0 +1,91 @@ +/* ============================================================================= + * M02_SO_Def_Spec_Model.ts + * 구조물 명세(PLAN 52 · `design_structure_spec`) 이름 고치기(52-11)의 자료 손질 — 화면 그리기 없음. + * + * · 명세 = 구조 정본(키 · 컨테이너 · 필수/선택 · 꼴) — 여기서는 **이름만** 고침 + * · 이름 자리 = 정의 `종류.{종류}.이름`(키 `{컨테이너}` · `{컨테이너}.{항목}` · `{컨테이너}.{항목}={값}`) + * · 필수 컨테이너 제목 = 이름 표에 없으면 정의 그룹(`그룹.{종류}` 의 `id = link`) 이름 — B05 그리기 + * (`containerTitle`)와 같은 차례로 읽고 그 자리에 씀 + * import 는 꼴만 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { StructureSpec } from "../../A00_Common/design/design_structure_spec"; +import type { StructDef } from "./M02_SO_Def_Model"; + +export interface SpecNameRow { + /** 0 = 컨테이너 · 1 = 항목 · 2 = 값 */ + level: 0 | 1 | 2; + /** 이름 표 키 */ + nameKey: string; + /** 읽기만 표시 — 컨테이너 id · 항목 id · 값 */ + key: string; + /** 지금 이름 */ + label: string; + /** 읽기만 표시 — 필수/선택 · 꼴 */ + tag: "required" | "optional" | "select" | "number" | "auto" | null; + /** 필수 컨테이너 제목이 정의 그룹 이름에서 옴 — 고치면 그 그룹 이름에 씀 */ + group: string | null; +} + +/** 명세 차례대로 이름 줄 — 같은 이름 키(갈래마다 겹친 값)는 한 줄. */ +export function specNameRows(def: StructDef, spec: StructureSpec): SpecNameRow[] { + const names = def.종류[spec.typeId]?.이름 ?? {}; + const groups = def.그룹[spec.typeId] ?? []; + const rows: SpecNameRow[] = []; + const seen = new Set(); + const push = (row: SpecNameRow): void => { + if (seen.has(row.nameKey)) return; + seen.add(row.nameKey); + rows.push(row); + }; + for (const c of spec.containers) { + const group = + c.required && names[c.id] === undefined ? groups.find((g) => g.id === c.link) : undefined; + push({ + level: 0, + nameKey: c.id, + key: c.id, + label: names[c.id] ?? group?.이름 ?? c.title, + tag: c.required ? "required" : "optional", + group: group ? group.id : null, + }); + for (const item of c.items) { + const itemKey = `${c.id}.${item.id}`; + push({ + level: 1, + nameKey: itemKey, + key: item.id, + label: names[itemKey] ?? item.label, + tag: item.kind, + group: null, + }); + const choices = [ + ...(item.choices ?? []), + ...Object.values(item.by?.map ?? {}).flatMap((b) => b.choices ?? []), + ]; + for (const ch of choices) { + const choiceKey = `${itemKey}=${ch.value}`; + push({ + level: 2, + nameKey: choiceKey, + key: ch.value, + label: names[choiceKey] ?? ch.label, + tag: null, + group: null, + }); + } + } + } + return rows; +} + +/** 이름 하나를 사본에 씀 — 그룹에서 온 제목은 그 그룹 이름 · 나머지는 이름 표. */ +export function setSpecName(def: StructDef, typeId: string, row: SpecNameRow, value: string): void { + const group = row.group ? def.그룹[typeId]?.find((g) => g.id === row.group) : undefined; + if (group) { + group.이름 = value; + return; + } + const kind = (def.종류[typeId] ??= {}); + (kind.이름 ??= {})[row.nameKey] = value; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Style.css b/M02_MasterTemplete/StructOptions/M02_SO_Style.css index a8570c86a..492eb8c4f 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Style.css +++ b/M02_MasterTemplete/StructOptions/M02_SO_Style.css @@ -332,3 +332,28 @@ margin: 0; color: var(--color-danger); } + +/* 구조물 명세 이름 고치기(PLAN 52-11) — 컨테이너 › 항목 › 값 들여쓰기 */ +.m02-sod__specrow { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-8); +} + +.m02-sod__specrow.is-level-0 { + margin-top: var(--spacing-8); + font-weight: 600; +} + +.m02-sod__specrow.is-level-1 { + padding-left: 1.5rem; +} + +.m02-sod__specrow.is-level-2 { + padding-left: 3rem; +} + +.m02-sod__specpreview { + max-width: 28rem; +} diff --git a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts index 90037edd8..3235e7762 100644 --- a/M02_MasterTemplete/StructOptions/M02_SO_Text.ts +++ b/M02_MasterTemplete/StructOptions/M02_SO_Text.ts @@ -45,6 +45,15 @@ const TEXT = { FormNumber: ["숫자", "Number"], FormText: ["글", "Text"], FormBool: ["예/아니오", "Yes/No"], + FormAuto: ["자동", "Auto"], + SpecHead: ["구조물 명세 이름", "Structure spec names"], + SpecNote: [ + "컨테이너 제목 › 항목 이름 › 값 이름만 고침 — 키(작은 글) · 필수/선택 · 꼴 · 구조는 구조물 명세가 정함 · [저장]하면 B05 · B06 이 이 이름으로 그림", + "Rename containers › items › values only — keys, required/optional, form and structure come from the spec", + ], + SpecRequired: ["필수", "Required"], + SpecOptional: ["선택", "Optional"], + SpecPreview: ["미리보기 — B05 · B06 폼", "Preview — B05 · B06 form"], Yes: ["예", "Yes"], No: ["아니오", "No"], Unit: ["단위", "Unit"], diff --git a/resources/tester/helper_m02_so_def_spec.cjs b/resources/tester/helper_m02_so_def_spec.cjs new file mode 100644 index 000000000..ede10f95b --- /dev/null +++ b/resources/tester/helper_m02_so_def_spec.cjs @@ -0,0 +1,64 @@ +/* M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증 헬퍼 — + * TS 를 그 자리에서 트랜스파일해 Node 로 돌리고 결과를 JSON 으로 냄(pytest 가 판정). + * 정의는 진짜 `구조물정의.json` 을 읽어 메모리 사본만 고침(파일 쓰기 없음). */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const load = (...parts) => { + const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, ...parts), "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + new Function("exports", "module", "require", js)(box.exports, box, require); + return box.exports; +}; +const m = load("M02_MasterTemplete", "StructOptions", "M02_SO_Def_Spec_Model.ts"); +const { PIPE_SPEC } = load("A00_Common", "design", "design_structure_pipe.ts"); +const real = () => + JSON.parse( + fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"), + ); + +const out = {}; +const def = real(); +const rows = m.specNameRows(def, PIPE_SPEC); +out.levels = [0, 1, 2].map((lv) => rows.filter((r) => r.level === lv).length); +out.unique = new Set(rows.map((r) => r.nameKey)).size === rows.length; +out.pipeTitle = rows.find((r) => r.nameKey === "pipe"); +out.optional = rows.find((r) => r.nameKey === "soil_add"); +out.basinForm = rows.find((r) => r.nameKey === "inlet.form=I"); + +// 이름 고침 — 그룹 제목 · 항목 · 값 · 선택 컨테이너 +const work = real(); +const pick = (k) => m.specNameRows(work, PIPE_SPEC).find((r) => r.nameKey === k); +m.setSpecName(work, "pipe", pick("pipe"), "관 본체"); +m.setSpecName(work, "pipe", pick("pipe.diameter"), "관 지름"); +m.setSpecName(work, "pipe", pick("pipe.diameter=800"), "Ø800"); +m.setSpecName(work, "pipe", pick("soil_add"), "흙막이 더"); +out.groupName = work.그룹.pipe.find((g) => g.id === "grv6lny").이름; +out.names = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map( + (k) => work.종류.pipe.이름[k] ?? null, +); +out.reread = ["pipe", "pipe.diameter", "pipe.diameter=800", "soil_add"].map((k) => pick(k).label); +// 구조는 그대로 — types · 동작 · 다른 종류 · 이름 표 키 수(새 키 없음) +const before = real(); +out.sameTypes = JSON.stringify(work.types) === JSON.stringify(before.types); +out.sameKeys = + Object.keys(work.종류.pipe.이름).length === Object.keys(before.종류.pipe.이름).length; + +// 이름 표가 빈 종류 — 명세 글로 읽고 고치면 이름 표가 생김 +const bare = real(); +delete bare.종류.pipe.이름; +const bareRows = m.specNameRows(bare, PIPE_SPEC); +out.bareLabel = bareRows.find((r) => r.nameKey === "inlet.structure=기슭막이").label; +m.setSpecName( + bare, + "pipe", + bareRows.find((r) => r.nameKey === "inlet"), + "유입", +); +out.bareGroup = bare.그룹.pipe.find((g) => g.id === "gnbf9m9").이름; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_so_def_spec.py b/resources/tester/test_m02_so_def_spec.py new file mode 100644 index 000000000..3677816ff --- /dev/null +++ b/resources/tester/test_m02_so_def_spec.py @@ -0,0 +1,58 @@ +"""M02 구조물 명세 이름 고치기(`M02_SO_Def_Spec_Model.ts` · PLAN 52-11) 단위검증. + +Node 헬퍼(`helper_m02_so_def_spec.cjs`)가 TS 를 트랜스파일해 진짜 정의 사본(메모리)으로 돌리고 +pytest 는 그 결과(JSON)를 판정. + +못박는 것 + - 배관 명세 나무: 컨테이너 5 · 항목 · 값 줄 · 이름 키 겹침 없음 · 필수/선택 · 꼴 표시. + - 필수 컨테이너 제목은 정의 그룹 이름에 씀 · 항목 · 값 · 선택 컨테이너는 이름 표에 씀. + - 이름만 바뀜 — types(옵션 · 키) 그대로 · 이름 표에 없는 키를 새로 만들지 않음. + - 이름 표가 빈 종류는 명세 글로 읽음. +""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent + + +@pytest.fixture(scope="module") +def r() -> dict: + proc = subprocess.run( + ["node", str(HERE / "helper_m02_so_def_spec.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_배관_나무(r) -> None: + assert r["levels"][0] == 5 + assert r["levels"][1] > 5 and r["levels"][2] > 10 + assert r["unique"] is True + assert r["pipeTitle"]["tag"] == "required" and r["pipeTitle"]["group"] == "grv6lny" + assert r["optional"]["tag"] == "optional" and r["optional"]["group"] is None + assert r["basinForm"]["label"] == "I형" and r["basinForm"]["key"] == "I" + + +def test_이름_고침_자리(r) -> None: + assert r["groupName"] == "관 본체" + assert r["names"] == [None, "관 지름", "Ø800", "흙막이 더"] + assert r["reread"] == ["관 본체", "관 지름", "Ø800", "흙막이 더"] + + +def test_구조는_그대로(r) -> None: + assert r["sameTypes"] is True and r["sameKeys"] is True + + +def test_이름_표_없는_종류(r) -> None: + assert r["bareLabel"] == "흙막이" + assert r["bareGroup"] == "유입" diff --git a/resources/tester/ui_checks/52-11_정의_이름고치기.js b/resources/tester/ui_checks/52-11_정의_이름고치기.js new file mode 100644 index 000000000..6d4679afe --- /dev/null +++ b/resources/tester/ui_checks/52-11_정의_이름고치기.js @@ -0,0 +1,155 @@ +/* 52-11 — M02 「구조물 옵션 정의」 = 구조물 명세 이름 고치기(명세 있는 종류) · B05 가 그 이름으로 그림 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m5211 = "단계";` 를 붙여 + * M02 마스터 층(#/m02-master-template?root=master) · 시스템 관리자: + * orca eval --expression "window.__m5211='m02'; $(cat 이 파일)" + * B05 종단설계(#/b05-profile) · 목록에 「배관 1」: + * orca eval --expression "window.__m5211='b05'; $(cat 이 파일)" + * 쓰는 것: 없음 — M02 는 화면 사본만 고치고 [저장] 안 누름 · 되돌린 뒤 판 그대로를 GET 으로 봄 · + * B05 는 받아 둔 종류 객체(메모리)만 고침 · 끝나면 orca reload 로 사본을 버림 + * 기대 수치(m02): + * - spec = 배관 나무 줄(컨테이너 5) · old = 옛 편집기 0(옵션 · 배치 · 그룹 · 규칙 · 기준 측점) · other = 명세 없는 종류는 옛 편집기 + * - 이름 칸만 고침: inputs = 줄 수 · 키 · 꼴은 글(입력 칸 아님) + * - 컨테이너 제목 · 항목 이름 · 값 이름 고침 → 미리보기 legend · 칸 이름 · 선택지 글 따라감 · [저장] 켜짐 + * - 빈 이름 → issues 「명세 이름」 · [저장] 꺼짐 · 되돌리면 이름 그대로 · 판 그대로 + * 기대 수치(b05): 사본 이름(컨테이너 · 항목 · 값)이 배관 폼에 그대로 → named.* = true + */ +(async () => { + const MODE = window.__m5211 || "m02"; + const W = (ms) => new Promise((r) => setTimeout(r, ms)); + const $ = (s, root = document) => root.querySelector(s); + const $$ = (s, root = document) => [...root.querySelectorAll(s)]; + const until = async (fn, ms = 10000) => { + for (let t = 0; t < ms; t += 150) { + const v = fn(); + if (v) return v; + await W(150); + } + return null; + }; + const put = (box, value) => { + box.value = value; + box.dispatchEvent(new Event("input", { bubbles: true })); + box.dispatchEvent(new Event("change", { bubbles: true })); + }; + const NEW = { pipe: "관 본체X", "pipe.diameter": "관 지름X", "pipe.diameter=800": "Ø800X" }; + const out = { mode: MODE }; + + if (MODE === "m02") { + const getDef = async () => + (await fetch("/api/m02/structure-definition", { credentials: "include" })).json(); + const first = await getDef(); + out.판 = first.판; + const row = await until(() => $("[data-options-row]")); + if (!row) return JSON.stringify({ error: "정의 줄 없음" }); + row.click(); + if (!(await until(() => $$("[data-sod-kind]").length))) + return JSON.stringify({ error: "종류 목록 없음" }); + // 명세 없는 종류 — 옛 편집기 그대로 + $('[data-sod-kind="retaining_wall"]').click(); + await W(200); + out.other = { base: !!$("[data-sod-base]"), spec: !!$("[data-sod-spec-block]") }; + // 배관 — 명세 이름 나무 + $('[data-sod-kind="pipe"]').click(); + await W(200); + const block = $("[data-sod-spec-block]"); + const detail = $("[data-sod-detail]"); + const rows = $$("[data-sod-spec]", block); + out.spec = { + rows: rows.length, + containers: $$(".is-level-0", block).length, + inputs: $$("input, select, textarea", block).filter( + (n) => !n.closest("[data-sod-spec-preview]"), + ).length, + nameInputs: $$("[data-sod-spec-name]", block).length, + keysReadOnly: rows.every((r) => r.querySelector("small.m02-opts__key")), + }; + out.old = { + base: !!$("[data-sod-base]", detail), + blocks: detail.children.length, // 머리 칸 + 명세 칸 = 2 + }; + const save = $("[data-sod-save]"); + out.saveBefore = save.disabled; + const box = (k) => $(`[data-sod-spec="${k}"] [data-sod-spec-name]`); + const orig = Object.fromEntries(Object.keys(NEW).map((k) => [k, box(k).value])); + for (const [k, v] of Object.entries(NEW)) { + put(box(k), v); + await W(150); + } + const preview = $("[data-sod-spec-preview]"); + const pipeBox = $('[data-spec-container="pipe"]', preview); + out.preview = { + title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe), + item: pipeBox + .querySelector('[data-spec-item="pipe.diameter"]') + .textContent.includes(NEW["pipe.diameter"]), + choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some( + (o) => o.textContent === NEW["pipe.diameter=800"], + ), + }; + out.saveAfterEdit = $("[data-sod-save]").disabled; + // 빈 이름 → 저장 막힘 + put(box("pipe.diameter"), ""); + await W(150); + out.blank = { + issue: $("[data-sod-issues]").textContent.includes("명세 이름"), + save: $("[data-sod-save]").disabled, + }; + // 되돌림 + for (const [k, v] of Object.entries(orig)) { + put(box(k), v); + await W(150); + } + out.restored = Object.entries(orig).every(([k, v]) => box(k).value === v); + out.판After = (await getDef()).판; + out.ok = + out.other.base && + !out.other.spec && + out.spec.containers === 5 && + out.spec.inputs === out.spec.rows && + out.spec.nameInputs === out.spec.rows && + out.spec.keysReadOnly && + !out.old.base && + out.old.blocks === 2 && + out.preview.title && + out.preview.item && + out.preview.choice && + !out.saveAfterEdit && + out.blank.issue && + out.blank.save && + out.restored && + out.판After === out.판; + } else { + const sec = $(".b05-structure-section"); + if (!sec) return JSON.stringify({ error: "구조물 배치 칸 없음" }); + const url = performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes("/B05_Profile/B05_Profile_Api_Structures.ts")) + .pop(); + const api = await import(url); + const pipe = (await api.fetchStructureTypes(true)).find((t) => t.type_id === "pipe"); + pipe.spec_names = { ...(pipe.spec_names ?? {}), ...NEW }; + await until(() => $$(".b05-route__irregular-list li").length); + const li = $$(".b05-route__irregular-list li").find((l) => + /배관 1$/.test(l.textContent.trim()), + ); + const spec = () => $(".b05-structure__spec", sec); + (li.querySelector("button") ?? li).click(); + await W(800); + if (!(spec() && spec().offsetParent !== null)) (li.querySelector("button") ?? li).click(); + await until(() => spec() && spec().offsetParent !== null); + const pipeBox = $('[data-spec-container="pipe"]', spec()); + out.named = { + title: pipeBox.querySelector("legend").textContent.includes(NEW.pipe), + item: pipeBox + .querySelector('[data-spec-item="pipe.diameter"]') + .textContent.includes(NEW["pipe.diameter"]), + choice: [...pipeBox.querySelectorAll('[data-spec-item="pipe.diameter"] option')].some( + (o) => o.textContent === NEW["pipe.diameter=800"], + ), + }; + (li.querySelector("button") ?? li).click(); // 재선택 = 해제 + out.ok = Object.values(out.named).every(Boolean); + } + return JSON.stringify(out); +})(); From 532c20085cd5fcea7805e63036033ee87082160b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:43:05 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat(design):=20=ED=9A=A1=EB=8B=A8=EB=B0=B0?= =?UTF-8?q?=EC=88=98=20=EC=84=B8=EC=9B=94=EA=B5=90(=ED=91=9C=2004)=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=ED=8F=BC=20=C2=B7=20?= =?UTF-8?q?=EC=9B=94=EB=A5=98=20=ED=8F=AD=20=E2=86=94=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 세월교 명세(design_structure_ford) — 세월교(관경 · 월류 폭 · 월류 높이 · 수량(련)) · 유입 구조 · 유출 구조(없음 · 날개벽 · 흙막이) · 모두 필수 · 컨테이너마다 구조물 조합 잇기(gford · gfordin · gfordout) - 구조 키 inlet_type · outlet_type + 옛 날개벽 설치 칸(wing_in · wing_out) 함께 씀 · 옛 저장 · B06 조정창 설치 칸으로 구조를 맞춤 · 옛 관종 · 날개벽 치수는 지난 정의 값 - 계산 파일 따로(design_structure_ford_calc) — 높이 = 설계유량을 그 폭으로 넘기는 Manning 필요 수심 · 0.01 올림 · 폭을 바꾸면 채움 · 그 아래는 안 받음 · 근거 통수단면.md:23~33 · :59 - B06 날개벽 조정 행 자리 = 유입 · 유출 구조 컨테이너 끝 · 세월교 옛 관종 · 관경 · 월류 칸 걷음 - 정의 종류.ford_bridge.이름 표 · 명세 등록(STRUCTURE_SPECS) · 52-1 시험 헬퍼가 같은 폴더 명세 import 를 읽음 - 시험 test_52_4_structure_spec_ford · 화면 시나리오 ui_checks/52-4_세월교_컨테이너 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK --- A00_Common/design/design_structure_ford.ts | 88 +++++++++ .../design/design_structure_ford_calc.ts | 27 +++ A00_Common/design/design_structure_pipe.ts | 8 +- .../B05_Profile_UI_Drainage_Facility.ts | 124 ++++++------ .../B05_Profile_UI_Drainage_Facility_Merge.ts | 8 +- resources/master_template/구조물정의.json | 20 +- resources/tester/helper_52_4_ford.cjs | 86 +++++++++ resources/tester/helper_52_structure_spec.cjs | 3 +- .../tester/test_52_4_structure_spec_ford.py | 109 +++++++++++ .../tester/ui_checks/52-4_세월교_컨테이너.js | 181 ++++++++++++++++++ 10 files changed, 590 insertions(+), 64 deletions(-) create mode 100644 A00_Common/design/design_structure_ford.ts create mode 100644 A00_Common/design/design_structure_ford_calc.ts create mode 100644 resources/tester/helper_52_4_ford.cjs create mode 100644 resources/tester/test_52_4_structure_spec_ford.py create mode 100644 resources/tester/ui_checks/52-4_세월교_컨테이너.js diff --git a/A00_Common/design/design_structure_ford.ts b/A00_Common/design/design_structure_ford.ts new file mode 100644 index 000000000..55bb897e0 --- /dev/null +++ b/A00_Common/design/design_structure_ford.ts @@ -0,0 +1,88 @@ +/* ============================================================================= + * design_structure_ford.ts + * 횡단배수 › 세월교 명세 — 사용자 구조물 표 04(`docs/raw/구조물정의_표/04_횡단배수_세월교.md`) 그대로. + * + * 컨테이너 셋: 세월교(필수) · 유입 구조(필수) · 유출 구조(필수). 구조 · 정본 = 이 명세 · 이름만 정의(M02)에서 바꿈. + * 저장 키 = pipe_points.json 옛 키 그대로(B06 세월교 세트 · B07 · B08 이 읽는 자리) — 새 키 = 유입 · 유출 구조 + * (`inlet_type` · `outlet_type` = 없음 · 날개벽 · 흙막이) · 고르면 옛 날개벽 설치 칸(`wing_in` · `wing_out`)도 함께 적음. + * 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래로는 안 받음) — 계산은 `design_structure_ford_calc`. + * import 없음 — 시험이 이 파일만 옮겨 돌림. + * ========================================================================== */ + +import type { SpecContainer, SpecChoice, StructureSpec } from "./design_structure_spec"; + +/** 유입 · 유출 구조 — 날개벽만 옛 설치 칸이 「있음」. */ +const guard = (side: "in" | "out"): SpecChoice[] => [ + { value: "없음", label: "없음", also: { [`wing_${side}`]: "없음" } }, + { value: "날개벽", label: "날개벽", also: { [`wing_${side}`]: "있음" } }, + { value: "흙막이", label: "흙막이", also: { [`wing_${side}`]: "없음" } }, +]; + +const guardContainer = (id: "inlet" | "outlet", title: string, link: string): SpecContainer => ({ + id, + title, + required: true, + link, + items: [ + { + id: "structure", + label: "구조", + kind: "select", + key: `${id}_type`, + choices: guard(id === "inlet" ? "in" : "out"), + }, + ], +}); + +export const FORD_SPEC: StructureSpec = { + typeId: "ford_bridge", + containers: [ + { + id: "ford", + title: "세월교", + required: true, + link: "gford", + items: [ + { + id: "diameter", + label: "관경", + kind: "select", + key: "pipe_diameter_mm", + asNumber: true, + choices: ["800", "1000", "1200", "1500"].map((v) => ({ value: v, label: v })), + }, + { id: "width", label: "월류 폭 (m)", kind: "number", key: "ford_width_m", step: 0.1 }, + { id: "height", label: "월류 높이 (m)", kind: "number", key: "ford_height_m", step: 0.01 }, + { id: "count", label: "수량 (련)", kind: "number", key: "pipe_count", step: 1 }, + ], + }, + guardContainer("inlet", "유입 구조", "gfordin"), + guardContainer("outlet", "유출 구조", "gfordout"), + ], + // 표 04 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값) + retired: [ + "pipe_kind", + "wing_in_height_m", + "wing_in_length_m", + "wing_in_angle_deg", + "wing_out_height_m", + "wing_out_length_m", + "wing_out_angle_deg", + ], +}; + +/** 구조를 날개벽 설치 칸(옛 저장 · B06 조정창이 고치는 칸)에 맞춤 — 있음 = 날개벽 · 없음 = 날개벽이면 없음 + * (흙막이는 그대로) · 설치 칸이 없으면 구조 그대로. */ +export function withLegacyGuards( + options: Record, +): Record { + const out = { ...options }; + for (const [type, wing] of [ + ["inlet_type", "wing_in"], + ["outlet_type", "wing_out"], + ]) { + if (out[wing] === "있음") out[type] = "날개벽"; + else if (out[wing] === "없음" && (out[type] ?? "날개벽") === "날개벽") out[type] = "없음"; + } + return out; +} diff --git a/A00_Common/design/design_structure_ford_calc.ts b/A00_Common/design/design_structure_ford_calc.ts new file mode 100644 index 000000000..a9ec865a0 --- /dev/null +++ b/A00_Common/design/design_structure_ford_calc.ts @@ -0,0 +1,27 @@ +/* ============================================================================= + * design_structure_ford_calc.ts + * 세월교 명세 계산 — 월류 폭 ↔ 월류 높이 연동(PLAN 52-4). + * + * 높이 = 담당 유역 설계유량을 그 월류 폭으로 넘기는 필요 수심(Manning 개수로 · `fordSection` 그대로). + * 근거: Manning 공식 `resources/knowledge/technical_info/01_임도/03_계산정보/통수단면.md:23~33` + * (KDS 44 40 00 2.7.1~2.7.2) · 조도계수 0.017 = 같은 파일 :59(콘크리트 수로 보통 · 울진 물넘이) · + * 경사 10 % = 실무 수리계산서 역산값(`config_frontend.FORD_SLOPE` · 지식DB 수치 없음). + * 폭을 바꾸면 높이 칸을 이 값으로 채움 · 사용자는 올려 적을 수 있고 이 값 아래로는 안 받음. + * 높이는 저장되는 입력값 — 서버는 받은 높이로 노면을 내림(`B06_Section_Engine_Culvert._ford_set`) · 재계산 안 함. + * 설계유량이 없으면 null = 손 입력. + * ========================================================================== */ + +import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields"; + +/** 월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림. 유량 · 폭이 없으면 null. */ +export function fordHeightM(designFlowM3s: number | null, widthM: number): number | null { + const section = fordSection(designFlowM3s, widthM); + return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null; +} + +/** 높이 빈 칸 안내. */ +export function fordHeightHint(designFlowM3s: number | null, widthM: number): string { + const need = fordHeightM(designFlowM3s, widthM); + if (need !== null) return `필요 수심 ${need.toFixed(2)} 이상`; + return designFlowM3s ? "월류 폭을 넣으면 셈" : "설계유량 없음 — 손 입력"; +} diff --git a/A00_Common/design/design_structure_pipe.ts b/A00_Common/design/design_structure_pipe.ts index c72f84c0b..ffc019894 100644 --- a/A00_Common/design/design_structure_pipe.ts +++ b/A00_Common/design/design_structure_pipe.ts @@ -8,10 +8,11 @@ * (`inlet_type` · `*_revet_*`)에 둠. 새 키 = 집수정 높이 · 선택 컨테이너(`soil_add_{n}_*` · `chute_add_{n}_*`). * 정함(PLAN 52): 유출 형태 = 흙막이 6가지 · 도수로/돌붙임 = 구조만 · 길이 = 전 + 후 맞물림. * 계산(관 길이 · 높이 안내)은 `design_structure_pipe_calc`. - * import 없음 — 시험이 이 파일만 옮겨 돌림. + * import = 같은 폴더 명세뿐 — 시험이 이 폴더 파일만 옮겨 돌림. * ========================================================================== */ import type { SpecChoice, SpecItem, StructureSpec } from "./design_structure_spec"; +import { FORD_SPEC } from "./design_structure_ford"; const SOIL = "기슭막이"; // 저장 값(옛 키 그대로) — 화면 이름은 흙막이 const BASIN = "집수정"; @@ -184,4 +185,7 @@ export const PIPE_SPEC: StructureSpec = { }; /** 명세로 그리는 종류 — 종류 키 → 명세(표가 오는 대로 더함). */ -export const STRUCTURE_SPECS: Readonly> = { pipe: PIPE_SPEC }; +export const STRUCTURE_SPECS: Readonly> = { + pipe: PIPE_SPEC, + ford_bridge: FORD_SPEC, +}; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index e7371d120..3b540ee46 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -53,6 +53,8 @@ import { } from "./B05_Profile_UI_Drainage_Facility_Fields"; import { createSpecForm, type SpecLinks } from "../A00_Common/design/design_structure_form"; import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe"; +import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford"; +import { fordHeightHint, fordHeightM } from "../A00_Common/design/design_structure_ford_calc"; import { guardHeightHint, pipeLengthM } from "../A00_Common/design/design_structure_pipe_calc"; import { hasLinks, @@ -250,45 +252,31 @@ export function createFacilityOptionsForm( root.className = "b05-drainage__facility"; root.hidden = true; - // ── 관종 · 관경 한 행 — 세월교 몫(배관은 아래 명세 폼 · PLAN 52-1) ───────── - const pipeMaterial = optionalSelect("— 안 정함 —", ["흄관", "VR관", "파형강관"]); - const pipeDiameter = document.createElement("select"); - pipeDiameter.replaceChildren( - new Option("— 안 정함 —", ""), - ...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)), - ); - const pipeRow = grid( - labeled("관종", pipeMaterial, "pipe_kind"), - labeled("관경", pipeDiameter, "pipe_diameter_mm"), - ); - // ── 배관 = 사용자 구조물 표 01 명세(PLAN 52-1) — 컨테이너 다섯 · 옛 관종 · 날개벽 칸은 뺌(지난 정의 값) ── let linkSource: SpecLinks | null = null; let chainage: number | null = null; - const pipeForm = createSpecForm( - { - get: () => linkSource?.get() ?? {}, - pick: (key) => linkSource?.pick(key), - clear: (key) => linkSource?.clear(key), - set: (key, item) => linkSource?.set(key, item), + const specLinks: SpecLinks = { + get: () => linkSource?.get() ?? {}, + pick: (key) => linkSource?.pick(key), + clear: (key) => linkSource?.clear(key), + set: (key, item) => linkSource?.set(key, item), + }; + const pipeForm = createSpecForm(specLinks, { + auto: (_c, item) => { + if (item.id !== "length") return ""; + const length = pipeLengthM(chainage); + return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2); }, - { - auto: (_c, item) => { - if (item.id !== "length") return ""; - const length = pipeLengthM(chainage); - return length === null ? "횡단 설계 뒤 셈" : length.toFixed(2); - }, - hint: (c, item, state) => { - const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이"; - return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : ""; - }, - onChange: (c, item) => { - if (c.id === "inlet" && (item.id === "structure" || item.id === "form")) - inletStructureHandler?.(inletStructureOf()); - emit(); - }, + hint: (c, item, state) => { + const soil = c.id !== "inlet" || state.inlet?.[0]?.structure === "기슭막이"; + return item.id === "height" && soil ? guardHeightHint(state.pipe?.[0]?.diameter ?? "") : ""; }, - ); + onChange: (c, item) => { + if (c.id === "inlet" && (item.id === "structure" || item.id === "form")) + inletStructureHandler?.(inletStructureOf()); + emit(); + }, + }); // 조정창에서 옮겨 오는 행(단 수·옵션)이 붙을 자리 — 유입 · 유출 컨테이너 끝(2026-08-29 지시 4) const inletSlot = pipeForm.slot("inlet"); const outletSlot = pipeForm.slot("outlet"); @@ -301,6 +289,26 @@ export function createFacilityOptionsForm( : "auto"; }; + // ── 세월교 = 사용자 구조물 표 04 명세(PLAN 52-4) — 세월교 · 유입 구조 · 유출 구조 ── + // 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`) + let fordFlow: number | null = null; + const fordNeed = (): number | null => + fordHeightM(fordFlow, Number(fordForm.state().ford?.[0]?.width ?? "")); + const fordForm = createSpecForm(specLinks, { + hint: (c, item, state) => + c.id === "ford" && item.id === "height" + ? fordHeightHint(fordFlow, Number(state.ford?.[0]?.width ?? "")) + : "", + onChange: (c, item) => { + const need = c.id === "ford" ? fordNeed() : null; + const height = fordForm.state().ford?.[0]?.height ?? ""; + const low = height !== "" && !(Number(height) >= (need ?? 0)); + if (need !== null && (item.id === "width" || (item.id === "height" && low))) + fordForm.setValue("ford", "height", need.toFixed(2)); + emit(); + }, + }); + // ── 추가 기슭막이(다단) — 횡단도에서 다단 벽을 고를 때만 뜬다(2026-08-30 사용자 // 지시 4). 유입구·유출구와 같은 양식이되, 값은 조정창 사본의 행이 그대로 옮겨 온다 // (다단 벽은 폼에 자기 칸이 없다 — 높이·형태·구간값이 전부 조정 채널에 있다). @@ -370,6 +378,8 @@ export function createFacilityOptionsForm( ); const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS); const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS); + const wingInHome = wingInFields.slot.parentElement!; + const wingOutHome = wingOutFields.slot.parentElement!; const hiddenOptions = createHiddenOptions(root); @@ -399,8 +409,8 @@ export function createFacilityOptionsForm( // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( saveNote, - pipeRow, pipeForm.root, + fordForm.root, ford.widthRow, ford.slopeRow, ford.countRow, @@ -421,15 +431,18 @@ export function createFacilityOptionsForm( if (current === null) return; const isPipe = current === "pipe"; const isBox = current === "box_culvert"; - pipeRow.hidden = current !== "ford_bridge"; + const isBridge = current === "ford_bridge"; pipeForm.root.hidden = !isPipe; + fordForm.root.hidden = !isBridge; boxWrap.hidden = !isBox; - // 날개벽은 BOX암거와 세월교가 같은 옵션 한 벌을 쓴다(2026-08-25 사용자 확정). - const hasWing = isBox || current === "ford_bridge"; + // 날개벽 칸 = BOX암거 · 세월교는 유입 · 유출 구조 컨테이너(52-4) — B06 조정 행 자리만 그 컨테이너 끝으로 + const hasWing = isBox; wingInFields.root.hidden = !hasWing; wingOutFields.root.hidden = !hasWing; - const isFord = current === "ford_pavement" || current === "ford_bridge"; - ford.countRow.hidden = current !== "ford_bridge"; + (isBridge ? fordForm.slot("inlet") : wingInHome).append(wingInFields.slot); + (isBridge ? fordForm.slot("outlet") : wingOutHome).append(wingOutFields.slot); + const isFord = current === "ford_pavement"; + ford.countRow.hidden = true; ford.widthRow.hidden = !isFord; // 바닥 경사는 물넘이포장만 쓴다 — 세월교는 구체 위 노면이라 파임이 없다. ford.slopeRow.hidden = current !== "ford_pavement"; @@ -462,8 +475,6 @@ export function createFacilityOptionsForm( revetOutlet.fields.onChange(emit); revetSide.addEventListener("change", emit); [ - pipeMaterial, - pipeDiameter, boxSize, boxWidth, boxHeight, @@ -500,8 +511,12 @@ export function createFacilityOptionsForm( bindLinks(links) { linkSource = links; pipeForm.paintLinks(); + fordForm.paintLinks(); + }, + paintLinks: () => { + pipeForm.paintLinks(); + fordForm.paintLinks(); }, - paintLinks: () => pipeForm.paintLinks(), setChainage(chainageM) { chainage = chainageM; if (current === "pipe") pipeForm.refresh(); @@ -516,6 +531,7 @@ export function createFacilityOptionsForm( current = facility; hiddenOptions.remember(options); ford.setDesignFlow(designFlow ?? null); + fordFlow = designFlow ?? null; if (facility === null) { syncVisibility(); return; @@ -523,12 +539,11 @@ export function createFacilityOptionsForm( const text = (key: string): string => options[key] !== undefined ? String(options[key]) : ""; // 받은 값만 칸에 — 없으면 「안 정함」(새 시설은 부르는 쪽이 등록부 기본값을 실어 줌 · 46-7). - pipeMaterial.value = text("pipe_kind"); - pipeDiameter.value = text("pipe_diameter_mm"); - if (facility === "pipe") { - const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); + const groups = Object.fromEntries((definition?.groups ?? []).map((g) => [g.id, g.name])); + if (facility === "pipe") pipeForm.write(PIPE_SPEC, options, definition?.spec_names ?? {}, groups); - } + if (facility === "ford_bridge") + fordForm.write(FORD_SPEC, withLegacyGuards(options), definition?.spec_names ?? {}, groups); // 저장된 폭·높이가 프리셋과 맞으면 그 프리셋을, 아니면 "사용자 지정"을 고른다. // 값이 없으면 「안 정함」. const savedWidth = Number.parseFloat(text("body_width_m")); @@ -549,7 +564,7 @@ export function createFacilityOptionsForm( boxHeight.value = hasSaved ? String(savedHeight) : ""; wingInFields.write(options); wingOutFields.write(options); - if (facility === "ford_pavement" || facility === "ford_bridge") ford.write(facility, options); + if (facility === "ford_pavement") ford.write(facility, options); revetSide.value = text("side"); const spread = legacyRevetOptions(options); revetInlet.fields.write(spread); @@ -558,15 +573,13 @@ export function createFacilityOptionsForm( }, setDesignFlow(designFlow) { ford.setDesignFlow(designFlow ?? null); + fordFlow = designFlow ?? null; + if (current === "ford_bridge") fordForm.refresh(); // 높이 안내 syncVisibility(); }, readOptions() { const options: Record = {}; // 고른 칸만 싣는다 — 「안 정함」은 빈 값(기본값을 몰래 확정으로 안 바꿈). - const putPipe = (): void => { - if (pipeDiameter.value) options.pipe_diameter_mm = Number(pipeDiameter.value); - if (pipeMaterial.value) options.pipe_kind = pipeMaterial.value; - }; if (current === "pipe") { Object.assign(options, pipeForm.read()); // 명세 컨테이너 값 = 옛 키 그대로(PLAN 52-1) } else if (current === "box_culvert") { @@ -578,10 +591,7 @@ export function createFacilityOptionsForm( } else if (current === "ford_pavement") { ford.read(current, options); } else if (current === "ford_bridge") { - putPipe(); - ford.read(current, options); - wingInFields.read(options); - wingOutFields.read(options); + Object.assign(options, fordForm.read()); // 표 04 명세 값 · 옛 관종 · 날개벽 치수는 병합이 이어 붙임(52-4) } else if (current === "revetment") { if (revetSide.value) options.side = revetSide.value; // 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고 diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts index 4e6afd657..51b56df66 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts @@ -42,14 +42,16 @@ export const FORM_OPTION_KEYS: Readonly> = { ], box_culvert: ["body_width_m", "body_height_m", ...wingKeys("in"), ...wingKeys("out")], ford_pavement: ["ford_width_m", "ford_height_m", "ford_slope_pct", "thickness_cm", "length_m"], + // 세월교 = 표 04 명세 키(PLAN 52-4 · `design_structure_ford`) — 옛 관종 · 날개벽 치수는 폼에서 빠져 이어 붙임 ford_bridge: [ - "pipe_kind", "pipe_diameter_mm", "pipe_count", "ford_width_m", "ford_height_m", - ...wingKeys("in"), - ...wingKeys("out"), + "inlet_type", + "outlet_type", + "wing_in", + "wing_out", ], revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")], }; diff --git a/resources/master_template/구조물정의.json b/resources/master_template/구조물정의.json index 5b2e2bf07..dedd4bf76 100644 --- a/resources/master_template/구조물정의.json +++ b/resources/master_template/구조물정의.json @@ -3308,7 +3308,25 @@ "계곡통과시설", "월류단면", "날개벽" - ] + ], + "이름": { + "ford.diameter": "관경", + "ford.diameter=800": "800", + "ford.diameter=1000": "1000", + "ford.diameter=1200": "1200", + "ford.diameter=1500": "1500", + "ford.width": "월류 폭 (m)", + "ford.height": "월류 높이 (m)", + "ford.count": "수량 (련)", + "inlet.structure": "구조", + "inlet.structure=없음": "없음", + "inlet.structure=날개벽": "날개벽", + "inlet.structure=흙막이": "흙막이", + "outlet.structure": "구조", + "outlet.structure=없음": "없음", + "outlet.structure=날개벽": "날개벽", + "outlet.structure=흙막이": "흙막이" + } }, "revetment": { "동작": [ diff --git a/resources/tester/helper_52_4_ford.cjs b/resources/tester/helper_52_4_ford.cjs new file mode 100644 index 000000000..b11d13b4c --- /dev/null +++ b/resources/tester/helper_52_4_ford.cjs @@ -0,0 +1,86 @@ +/* 세월교 명세(표 04) · 월류 폭 ↔ 높이 계산 단위검증 헬퍼(PLAN 52-4) — `design_structure_spec.ts` · + * `design_structure_ford.ts` · `design_structure_ford_calc.ts`(+ `fordSection` 이 든 B05 칸 조각)를 그 자리에서 + * 트랜스파일해 Node 로 돌림. 결과 = JSON 한 벌. 인자 `names` 면 세월교 이름 표(정의 `종류.ford_bridge.이름`)만 냄. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +/** TS 한 파일을 CommonJS 로 — 상대 import 는 그 파일 자리 기준 · `@config/` 는 config 폴더 · .json 은 그대로. */ +const load = (file) => { + if (cache.has(file)) return cache.get(file); + if (file.endsWith(".json")) return JSON.parse(fs.readFileSync(file, "utf8")); + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(file, box.exports); + const near = (m) => { + const base = m.startsWith("@config/") + ? path.join(ROOT, "config", m.slice(8)) + : path.join(path.dirname(file), m); + return load(base.endsWith(".json") ? base : `${base}.ts`); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + cache.set(file, box.exports); + return box.exports; +}; +const D = (f) => path.join(ROOT, "A00_Common", "design", f); +const s = load(D("design_structure_spec.ts")); +const { FORD_SPEC, withLegacyGuards } = load(D("design_structure_ford.ts")); +const calc = load(D("design_structure_ford_calc.ts")); + +if (process.argv[2] === "names") { + process.stdout.write(JSON.stringify(s.nameTable(FORD_SPEC))); + process.exit(0); +} + +const old = { + pipe_kind: "파형강관", + pipe_diameter_mm: 1000, + pipe_count: 2, + ford_width_m: 10, + ford_height_m: 0.3, + wing_in: "있음", + wing_in_height_m: 1, + wing_out: "없음", +}; +const legacy = withLegacyGuards(old); +const state = s.readState(FORD_SPEC, legacy); +const back = s.writeOptions(FORD_SPEC, state); +const soil = JSON.parse(JSON.stringify(state)); +soil.inlet[0].structure = "흙막이"; +soil.outlet[0].structure = "날개벽"; +const soilOut = s.writeOptions(FORD_SPEC, soil); + +const flows = [0.5, 2.54, 8]; +const widths = [3, 5, 10, 20]; +const table = flows.flatMap((q) => widths.map((b) => [q, b, calc.fordHeightM(q, b)])); + +const out = { + containers: FORD_SPEC.containers.map((c) => [c.id, c.title, c.required]), + items: FORD_SPEC.containers.map((c) => c.items.map((i) => [i.id, i.kind, i.key ?? null])), + choices: FORD_SPEC.containers.map((c) => + c.items.filter((i) => i.kind === "select").map((i) => s.choicesOf(i, {}).map((ch) => ch.label)), + ), + legacy: { inlet: legacy.inlet_type, outlet: legacy.outlet_type }, + keepSoil: withLegacyGuards({ inlet_type: "흙막이", wing_in: "없음" }).inlet_type, + b06Off: withLegacyGuards({ inlet_type: "날개벽", wing_in: "없음" }).inlet_type, + b06On: withLegacyGuards({ inlet_type: "없음", wing_in: "있음" }).inlet_type, + state, + back, + soilOut, + links: FORD_SPEC.containers.map((c) => s.linkOf(c, 1)), + keys: s.specKeys(FORD_SPEC), + retired: FORD_SPEC.retired, + table, + none: [calc.fordHeightM(null, 10), calc.fordHeightM(2.54, 0)], + hints: [ + calc.fordHeightHint(2.54, 10), + calc.fordHeightHint(2.54, 0), + calc.fordHeightHint(null, 10), + ], +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_52_structure_spec.cjs b/resources/tester/helper_52_structure_spec.cjs index 0ff82723b..168721216 100644 --- a/resources/tester/helper_52_structure_spec.cjs +++ b/resources/tester/helper_52_structure_spec.cjs @@ -13,7 +13,8 @@ const load = (file, deps = {}) => { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; - new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? require(m)); + const near = (m) => (m.startsWith("./") ? load(`${m.slice(2)}.ts`) : require(m)); // 같은 폴더 명세 + new Function("exports", "module", "require", js)(box.exports, box, (m) => deps[m] ?? near(m)); return box.exports; }; const s = load("design_structure_spec.ts"); diff --git a/resources/tester/test_52_4_structure_spec_ford.py b/resources/tester/test_52_4_structure_spec_ford.py new file mode 100644 index 000000000..90a973a9b --- /dev/null +++ b/resources/tester/test_52_4_structure_spec_ford.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""횡단배수 › 세월교(표 04) 명세 + 월류 폭 ↔ 높이 연동 — PLAN 52-4. + +순수 규칙은 Node 헬퍼(`helper_52_4_ford.cjs`)가 돌림 · 화면은 ORCA `ui_checks/52-4_세월교_컨테이너.js`. +높이 거울 = 이 파일이 Manning(통수단면.md §2 · n 0.017 · S 0.10)을 따로 풀어 같은 값인지 봄. +""" + +import json +import math +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(HERE)) +N, S = 0.017, 0.10 + + +def _node(*args): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_52_4_ford.cjs"), *args], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +@pytest.fixture(scope="module") +def out(): + return _node() + + +def test_컨테이너_셋_모두_필수(out): + assert out["containers"] == [ + ["ford", "세월교", True], + ["inlet", "유입 구조", True], + ["outlet", "유출 구조", True], + ] + assert out["items"][0] == [ + ["diameter", "select", "pipe_diameter_mm"], + ["width", "number", "ford_width_m"], + ["height", "number", "ford_height_m"], + ["count", "number", "pipe_count"], + ] + assert out["choices"] == [ + [["800", "1000", "1200", "1500"]], + [["없음", "날개벽", "흙막이"]], + [["없음", "날개벽", "흙막이"]], + ] + + +def test_옛_날개벽_칸으로_구조를_세우고_같은_키로_씀(out): + assert out["legacy"] == {"inlet": "날개벽", "outlet": "없음"} + assert out["keepSoil"] == "흙막이" # 흙막이는 설치 칸 없음이어도 그대로 + assert out["b06Off"] == "없음" and out["b06On"] == "날개벽" # B06 조정창 설치 칸을 따름 + back = out["back"] + assert back["pipe_diameter_mm"] == 1000 and back["pipe_count"] == 2 + assert back["ford_width_m"] == 10 and back["ford_height_m"] == 0.3 + assert back["wing_in"] == "있음" and back["wing_out"] == "없음" + # 표 04 에 없는 옛 칸은 폼이 안 적음(병합이 이어 붙임 = 지난 정의 값) + assert "pipe_kind" not in back and "wing_in_height_m" not in back + assert "pipe_kind" in out["retired"] + + +def test_흙막이_날개벽_고르면_옛_설치_칸도_맞춰_씀(out): + s = out["soilOut"] + assert s["inlet_type"] == "흙막이" and s["wing_in"] == "없음" + assert s["outlet_type"] == "날개벽" and s["wing_out"] == "있음" + + +def test_잇기_키_컨테이너마다(out): + assert out["links"] == ["gford", "gfordin", "gfordout"] + + +def _manning_depth(q, b): + """넓은 직사각 수로 Manning 을 h 에 대해 이분법으로 풂 — 헬퍼와 다른 풀이.""" + + def cap(h): + return b * h * (1 / N) * ((b * h) / (b + 2 * h)) ** (2 / 3) * math.sqrt(S) + + lo, hi = 0.0, 5.0 + for _ in range(100): + mid = (lo + hi) / 2 + lo, hi = (mid, hi) if cap(mid) < q else (lo, mid) + return hi + + +def test_월류_높이는_폭과_연동_필요_수심_올림(out): + for q, b, h in out["table"]: + need = _manning_depth(q, b) + assert h == pytest.approx(math.ceil(need * 100 - 1e-9) / 100), (q, b) + # 폭이 넓을수록 높이가 낮아짐 + row = [h for q, b, h in out["table"] if q == 2.54] + assert row == sorted(row, reverse=True) and len(set(row)) == len(row) + assert out["none"] == [None, None] # 설계유량 · 폭이 없으면 손 입력 + assert out["hints"] == ["필요 수심 0.08 이상", "월류 폭을 넣으면 셈", "설계유량 없음 — 손 입력"] + + +def test_정의_파일에_세월교_이름_표가_올라_있음(): + names = _node("names") + with open( + os.path.join(ROOT, "resources", "master_template", "구조물정의.json"), encoding="utf-8" + ) as f: + doc = json.load(f) + assert doc["종류"]["ford_bridge"]["이름"] == names diff --git a/resources/tester/ui_checks/52-4_세월교_컨테이너.js b/resources/tester/ui_checks/52-4_세월교_컨테이너.js new file mode 100644 index 000000000..c5c9143e0 --- /dev/null +++ b/resources/tester/ui_checks/52-4_세월교_컨테이너.js @@ -0,0 +1,181 @@ +/* 52-4 — B05 · B06 좌측 「구조물 배치」 세월교 = 사용자 구조물 표 04 명세(컨테이너 셋) + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub6_검증) · 목록에 「세월교 1」 + * 부르는 법: 단계마다 한 번씩 — 앞에 `window.__m524 = "단계";` 를 붙여 + * orca eval --expression "window.__m524='edit'; $(cat 이 파일)" → 고치고 [저장] (원래 관 지점은 sessionStorage 에 둠) + * (orca reload 로 다시 열기) + * orca eval --expression "window.__m524='verify'; $(cat 이 파일)" → 다시 연 값 · B06 같은 그리기 + * orca eval --expression "window.__m524='restore'; $(cat 이 파일)" → 원래 관 지점으로 되돌림(PUT) · 초안 비움 + * 쓰는 것: 검증 프로젝트 pipe_points.json(되돌림 단계가 원래 값으로 다시 씀) + * 기대 수치(edit): 컨테이너 3(세월교 · 유입 구조 · 유출 구조 · 모두 필수) · 관경 800/1000/1200/1500 · + * 구조 없음/날개벽/흙막이 · 옛 관종 · 날개벽 칸 안 보임 · 폭 8 → 높이 = 필요 수심(설계유량 있으면) · + * 낮게 적으면 필요 수심으로 되돌림 · 수량 3 · 잇기 세월교 · 유입 지정 · 유출 지정 뒤 풂 + * 기대 수치(verify): 저장 값 그대로 · 같은 그리기(B05 = B06) + */ +(async () => { + const MODE = window.__m524 || "edit"; + const NAME = /세월교 1$/; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 20000) => { + for (let t = 0; t < ms; t += 200) { + if (fn()) return true; + await w(200); + } + return false; + }; + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const fetchApi = await import(modUrl("/B04_PreProcess/B04_PreProcess_Api_Fetch.ts")); + const spec = () => + [...document.querySelectorAll(".b05-structure__spec")].find((n) => n.offsetParent !== null); + const box = (c) => spec().querySelector(`[data-spec-container="${c}"]`); + const ctl = (c, item) => + box(c)?.querySelector(`[data-spec-item="${c}.${item}"]`)?.querySelector("select, input"); + const set = async (c, item, v) => { + const el = ctl(c, item); + el.value = v; + el.dispatchEvent(new Event("change", { bubbles: true })); + await w(250); + }; + const labels = (c, item) => [...ctl(c, item).options].filter((o) => o.value).map((o) => o.text); + const pickFord = async () => { + const find = () => + [...document.querySelectorAll(".b05-route__irregular-list li")].find( + (l) => l.offsetParent !== null && NAME.test(l.textContent.trim()), + ); + await until(() => !!find()); + const li = find(); + (li.querySelector("button") ?? li).click(); + await w(800); + if (!spec()) (li.querySelector("button") ?? li).click(); + await until(() => !!spec()); + await w(500); + }; + const shape = () => + [...spec().querySelectorAll("[data-spec-container]")].map((b) => ({ + c: b.dataset.specContainer, + legend: b.querySelector("legend").firstChild.textContent, + items: [...b.querySelectorAll("[data-spec-item]")].map((n) => [ + n.dataset.specItem, + n.querySelector("select, input").value, + ]), + link: b.querySelector(".b05-structure__summary-name")?.textContent, + })); + const assigned = (c) => + box(c).querySelector(".b05-structure__summary-name").classList.contains("is-assigned"); + const pickLink = async (c) => { + box(c).querySelector('[data-b05-summary="pick"]').click(); + await until(() => document.querySelector(".ui-item-picker__tree .ui-tree__row")); + for (let k = 0; k < 8; k++) { + const rows = [...document.querySelectorAll(".ui-item-picker__tree .ui-tree__row")]; + const leaf = rows.find((r) => !r.classList.contains("is-branch")); + if (leaf) { + leaf.click(); + break; + } + rows.find((r) => r.querySelector(".ui-tree__caret")?.textContent === "▸")?.click(); + await w(300); + } + await w(300); + [...document.querySelectorAll(".ui-item-picker__actions button")] + .find((b) => b.textContent.includes("적용")) + .click(); + await w(500); + }; + const visibleKeys = () => + [...document.querySelectorAll(".b05-drainage__facility [data-option-keys]")] + .filter((n) => n.offsetParent !== null) + .map((n) => n.dataset.optionKeys); + const out = { mode: MODE }; + + if (MODE === "edit") { + const before = await fetchApi.fetchDetailPipePoints(projectId); + sessionStorage.setItem("__524_original", JSON.stringify(before.pipe_points)); + await pickFord(); + const s0 = shape(); + out.containers = s0.map((b) => [b.c, b.legend]); + out.oldFieldsVisible = visibleKeys(); // 옛 관종 · 월류 · 날개벽 칸 — 빈 목록이어야 + out.diameters = labels("ford", "diameter"); + out.inletChoices = labels("inlet", "structure"); + out.outletChoices = labels("outlet", "structure"); + out.start = s0.map((b) => b.items); + await set("ford", "diameter", "1200"); + await set("ford", "width", "8"); + out.heightAfterWidth8 = ctl("ford", "height").value; + out.heightHint = ctl("ford", "height").placeholder; + await set("ford", "width", "4"); + out.heightAfterWidth4 = ctl("ford", "height").value; + await set("ford", "height", "0.01"); + out.heightLowBounced = ctl("ford", "height").value; + const raised = (Number(out.heightAfterWidth4 || 0) + 0.2).toFixed(2); + await set("ford", "height", raised); + out.heightRaisedKept = ctl("ford", "height").value === raised; + await set("ford", "count", "3"); + await set("inlet", "structure", "흙막이"); + await set("outlet", "structure", "날개벽"); + await pickLink("ford"); + await pickLink("inlet"); + await pickLink("outlet"); + out.linksPicked = ["ford", "inlet", "outlet"].map(assigned); + box("outlet").querySelector('[data-b05-summary="clear"]').click(); + await w(300); + out.linkOutletCleared = !assigned("outlet"); + out.edited = shape().map((b) => b.items); + document.querySelector('[data-dock-action="save"]').click(); + await w(500); + out.saved = await until(() => !document.querySelector(".ui-overlay.is-active"), 60000); + const after = await fetchApi.fetchDetailPipePoints(projectId); + const p = after.pipe_points.find((x) => x.chainage_m > 170 && x.chainage_m < 176); // 세월교 1 = 8+13 + out.file = p && { options: p.options, group_items: Object.keys(p.group_items ?? {}) }; + } + + if (MODE === "verify") { + await pickFord(); + const b05 = shape(); + out.reopened = b05.map((b) => b.items); + out.links = ["ford", "inlet", "outlet"].map(assigned); + location.hash = "#/b06-section"; + await until(() => location.hash.includes("b06"), 5000); + await w(6000); + await pickFord(); + const b06 = shape(); + out.sameDrawing = JSON.stringify(b05) === JSON.stringify(b06); + if (!out.sameDrawing) out.diff = { b05, b06 }; + out.b06WingSlots = ["inlet", "outlet"].map( + (c) => box(c).querySelector(".b05-structure__adjust-slot")?.childElementCount ?? -1, + ); + } + + if (MODE === "restore") { + const original = JSON.parse(sessionStorage.getItem("__524_original") || "null"); + if (!original) return JSON.stringify({ error: "원래 값 없음" }); + const keys = [ + "chainage_m", + "source", + "facility", + "start_m", + "end_m", + "options", + "summary_item", + "group_items", + ]; + const points = original.map((p) => + Object.fromEntries( + keys.filter((k) => p[k] !== undefined && p[k] !== null).map((k) => [k, p[k]]), + ), + ); + await fetchApi.saveDetailPipePoints(projectId, points); + for (const k of Object.keys(sessionStorage)) + if (k.startsWith("aislo:draft:pipes:") || k === "__524_original") + sessionStorage.removeItem(k); + const now = await fetchApi.fetchDetailPipePoints(projectId); + const pick = (list) => + JSON.stringify(list.map((p) => [p.chainage_m, p.options ?? {}, p.group_items ?? {}])); + out.restored = pick(now.pipe_points) === pick(original); + } + return JSON.stringify(out); +})(); From e197727a9b75a84ed81d0ad8de10848039cfad9a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:45:26 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix(structure):=20=EB=A7=B9=EC=95=94?= =?UTF-8?q?=EA=B1=B0=20=EC=A2=85=EB=A5=98=20=EB=8B=A8=EC=9C=84=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=ED=95=AD=EB=AA=A9=20=EC=A4=84=20=EA=B0=80=EB=A6=BC?= =?UTF-8?q?(=ED=91=9C=2006=20=C2=B7=2052-6b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 52-1 규칙 넓힘 — 정의 그룹 상자(그룹마다 잇기 줄)가 있는 종류도 종류 단위 「구조물 조합」 줄 가림 - 정의 그룹 가진 종류 = 배관(이미 명세 종류) · 맹암거뿐 — 다른 종류 동작 · 저장 값 자리 그대로 - ORCA 52-6 시나리오에 잇기 줄 수(linkRows) 더함 — 맹암거 1 · 노출형 횡단수로 1 · 개거 1 · 배관 3 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP --- B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts | 5 +++-- resources/tester/ui_checks/52-6_횡단맹암거.js | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index 97032d397..e35163c76 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -23,9 +23,10 @@ import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { specKeys } from "../A00_Common/design/design_structure_spec"; -/** 구조물 명세 컨테이너로 그리는 종류인지 — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1). */ +/** 구조물 명세 컨테이너 · 정의 그룹 상자(그룹마다 잇기 줄)로 그리는 종류인지 — 그때 종류 단위 + * 「구조물 조합」 줄은 가림(PLAN 52-1 · 52-6b). */ export const hasSpecContainers = (type: StructureType): boolean => - !!STRUCTURE_SPECS[type.type_id]?.containers.length; + !!STRUCTURE_SPECS[type.type_id]?.containers.length || !!type.groups?.length; /** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */ export interface OptionRenderContext { diff --git a/resources/tester/ui_checks/52-6_횡단맹암거.js b/resources/tester/ui_checks/52-6_횡단맹암거.js index 2ce14c7ee..3519947f5 100644 --- a/resources/tester/ui_checks/52-6_횡단맹암거.js +++ b/resources/tester/ui_checks/52-6_횡단맹암거.js @@ -7,7 +7,7 @@ * orca eval --expression "window.__m526='restore'; $(cat 이 파일)" → sessionStorage 를 처음 그대로 · 뒤에 orca reload * 쓰는 것: 없음 — [저장] 은 fetch 를 가로채 서버로 안 보냄(공유 storage) · 서버 저장 왕복은 test_52_6_cross_underdrain * 기대 수치(edit): b05Types 에 cross_underdrain:맹암거 · underdrain 없음 · box 맹암거 · keys 높이 · 경사 · - * pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음 + * 잇기 줄 1(맹암거 상자 안만 · 종류 단위 줄 가림 · 52-6b) · pending 높이 0.6 · 경사 2.5 · 잇기 gcrossud 지정 · reopened 같음 * 기대 수치(b06): b06Types A 에 맹암거 · B 에 종단배수 맹암거 따로 · sameDrawing · 값 같음 * 기대 수치(save): saved = PUT 몸통에 같은 값(67 m · 0.6 · 2.5 · gcrossud) */ @@ -55,6 +55,7 @@ values: ["culvert_height_m", "culvert_slope_pct"].map((k) => input(k)?.value), link: b?.querySelector(".b05-structure__summary-name").textContent, linked: b?.querySelector(".b05-structure__summary-name").classList.contains("is-assigned"), + linkRows: [...sec().querySelectorAll("[data-b05-summary=row]")].filter(vis).length, }; }; const item = () => From 6bd9773d0fbc6360a1942e521251b378d90f5b47 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:45:46 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat(B05=C2=B7B06):=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A2=85=EB=8B=A8=EB=B0=B0=EC=88=98=C2=B7?= =?UTF-8?q?=EA=B3=84=EB=A5=98=EC=82=AC=EB=B0=A9=20=EC=88=A8=EA=B9=80=20(PL?= =?UTF-8?q?AN=2052-8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 군 드롭다운 · 우클릭 메뉴에서 B · D 군 뺌 — 옛 값 · 도면 · 집계 그대로 - 종단배수 종류 단위 조합 잇기 줄 숨김 - 정의 폼의 가림 풀기가 군 숨김을 건드리지 않게 표식 추가 - 화면 시나리오 52-8_종단_계류_숨김.js Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- .../B05_Profile_UI_Profile_Structures.ts | 5 +- .../B05_Profile_UI_Structures_Define_Form.ts | 4 +- .../B05_Profile_UI_Structures_Panel.ts | 7 +- ...B05_Profile_UI_Structures_Panel_Options.ts | 7 +- .../B05_Profile_UI_Structures_Panel_Types.ts | 12 +++ .../tester/ui_checks/52-8_종단_계류_숨김.js | 99 +++++++++++++++++++ 6 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 resources/tester/ui_checks/52-8_종단_계류_숨김.js diff --git a/B05_Profile/B05_Profile_UI_Profile_Structures.ts b/B05_Profile/B05_Profile_UI_Profile_Structures.ts index c22646ad4..4eb2380a8 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Structures.ts @@ -17,6 +17,7 @@ import { } from "./B05_Profile_UI_IrregularStations"; import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures"; import { GROUP_LABELS } from "./B05_Profile_UI_Structures_Panel"; +import { HIDDEN_PICK_GROUPS } from "./B05_Profile_UI_Structures_Panel_Types"; /** 우클릭 메뉴용 최소 타입 정보 — 사이드 「구조물 배치」 종류 목록과 같은 원천. */ export interface StructureMenuType { @@ -32,7 +33,9 @@ export function structureGroupMenuItems( types: ReadonlyArray, onPick: (typeId: string) => void, ): MapContextMenuItem[] { - const groups = [...new Set(types.map((type) => type.group))]; + const groups = [...new Set(types.map((type) => type.group))].filter( + (group) => !HIDDEN_PICK_GROUPS.has(group), + ); return groups.map((group) => ({ label: GROUP_LABELS[group] ?? group, children: types diff --git a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts index e3e44d790..7f321904a 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Define_Form.ts @@ -78,7 +78,9 @@ export function createFacilityDefinition(root: HTMLElement): FacilityDefinition root .querySelectorAll(".is-rule-hidden") .forEach((node) => node.classList.remove("is-rule-hidden")); - root.querySelectorAll("option[hidden]").forEach((o) => (o.hidden = false)); + root + .querySelectorAll("option[hidden]:not([data-pick-hidden])") + .forEach((o) => (o.hidden = false)); paint(); }, }; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 7f1c2ffe0..ffae71d9d 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -24,7 +24,7 @@ import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { type FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility"; import { field, optionDefaults } from "./B05_Profile_UI_Structures_Fields"; import { - hasSpecContainers, + hidesTypeSummary, paintStale, renderOptionFields as renderOptionFieldsInto, } from "./B05_Profile_UI_Structures_Panel_Options"; @@ -54,6 +54,7 @@ export type { PipeFacilityItem, StructuresSection } from "./B05_Profile_UI_Struc import { DEFAULT_INTERVAL_LENGTH_M, GROUP_LABELS, + groupOption, hasComputedRange, readBeforeOption, readLengthOption, @@ -227,7 +228,7 @@ export function createStructuresSection( !!type && !managed && type.placement === "interval" && !hasComputedRange(type); positionRow.hidden = !type; positionDivider.hidden = !type; - summary.root.hidden = !type || hasSpecContainers(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1) + summary.root.hidden = !type || hidesTypeSummary(type); // 컨테이너 종류는 컨테이너마다 잇기(52-1) startFields.wrap.hidden = !isInterval; endFields.wrap.hidden = !isInterval; primary.disabled = !type; @@ -544,7 +545,7 @@ export function createStructuresSection( // 첫 항목은 빈칸 — 진입·리셋 상태에서 군·종류를 강제로 고르게 두지 않는다. 이름은 정의(48) 먼저. groupSelect.replaceChildren( new Option("— 선택 —", ""), - ...[...groups].map(([g, name]) => new Option(name ?? GROUP_LABELS[g] ?? g, g)), + ...[...groups].map(([g, name]) => groupOption(g, name ?? GROUP_LABELS[g] ?? g)), ); groupSelect.value = ""; syncTypeOptions(); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts index 97032d397..8fe069b7d 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Options.ts @@ -23,9 +23,10 @@ import { FORM_OPTION_KEYS } from "./B05_Profile_UI_Drainage_Facility_Merge"; import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe"; import { specKeys } from "../A00_Common/design/design_structure_spec"; -/** 구조물 명세 컨테이너로 그리는 종류인지 — 그때 종류 단위 「구조물 조합」 줄은 가림(PLAN 52-1). */ -export const hasSpecContainers = (type: StructureType): boolean => - !!STRUCTURE_SPECS[type.type_id]?.containers.length; +/** 종류 단위 「구조물 조합」 줄을 가릴 종류인지 — 구조물 명세 컨테이너로 그리는 종류(PLAN 52-1) · + * 종단배수(B군 · 조합 잇기 없음 · 표 07). */ +export const hidesTypeSummary = (type: StructureType): boolean => + !!STRUCTURE_SPECS[type.type_id]?.containers.length || type.group === "B"; /** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */ export interface OptionRenderContext { diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index ff1daad7a..04fa037cd 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -34,6 +34,18 @@ export const GROUP_LABELS: Record = { 호환: "기타", }; +/** 새로 놓을 때 고르기 목록(군 드롭다운 · 우클릭 메뉴)에서 뺄 군 — 종단배수·계류사방(표 07). + * 옛 값 · 도면 · 집계는 그대로이고 데이터도 지우지 않는다. */ +export const HIDDEN_PICK_GROUPS: ReadonlySet = new Set(["B", "D"]); + +/** 군 드롭다운 항목 — 뺄 군은 가림(옛 항목을 열 때는 값으로 그대로 고를 수 있음). */ +export function groupOption(group: string, name: string): HTMLOptionElement { + const option = new Option(name, group); + option.hidden = HIDDEN_PICK_GROUPS.has(group); + if (option.hidden) option.dataset.pickHidden = ""; // 정의 폼의 「가림 풀기」가 건드리지 않게 + return option; +} + /** 구간형 신규 추가 시 기본 구간 길이(m). 사용자가 종점을 바로 고칠 수 있게 짧게 잡는다. */ export const DEFAULT_INTERVAL_LENGTH_M = 15; diff --git a/resources/tester/ui_checks/52-8_종단_계류_숨김.js b/resources/tester/ui_checks/52-8_종단_계류_숨김.js new file mode 100644 index 000000000..d77b951a7 --- /dev/null +++ b/resources/tester/ui_checks/52-8_종단_계류_숨김.js @@ -0,0 +1,99 @@ +/* 52-8 — B05 · B06 좌측 「구조물 배치」 종단배수(B) · 계류사방(D) 고르기 목록에서 숨김 + * 시작 화면: B05 종단설계(#/b05-profile) · 검증 프로젝트(sub2_검증) + * 부르는 법: 앞에 `window.__m528 = "단계";` 를 붙여 + * orca eval --expression "window.__m528='seed'; $(cat 이 파일)" → 옛 값 둘(골막이 D · 측구 B)을 세션 초안에만 둠 + * (orca reload) + * orca eval --expression "window.__m528='verify'; $(cat 이 파일)" → 군 목록 · 우클릭 메뉴 · 옛 값 열기 (B05) + * (#/b06-section 으로 가서 reload 후 verify 한 번 더) + * orca eval --expression "window.__m528='clear'; $(cat 이 파일)" → 세션 초안 비움(서버 저장 안 함) + * 기대 수치(verify): visibleGroups 에 B · D 없음(0) · hiddenGroups 가림만 · menuHasBD 0 · + * 옛 값 목록 둘 그대로 · 옛 값을 열면 군 = D / B 로 열림(editGroup) · 잇기 칸 pickLinks 골막이(D) 1 · 측구(B) 0 + */ +(async () => { + const MODE = window.__m528 || "verify"; + const w = (ms) => new Promise((r) => setTimeout(r, ms)); + const projectId = localStorage.getItem("frd_current_project_id"); + const modUrl = (part) => + performance + .getEntriesByType("resource") + .map((e) => e.name) + .filter((n) => n.includes(part)) + .pop(); + const api = await import(modUrl("/B05_Profile/B05_Profile_Api_Structures.ts")); + const out = { mode: MODE, hash: location.hash }; + if (MODE === "seed") { + const base = { + placement_source: "manual", + status: "draft", + revision: 0, + geometry: null, + memo: "", + options: {}, + }; + api.writePendingStructures(projectId, [ + { + ...base, + structure_id: "m528d", + type_id: "erosion_check", + placement: "point", + chainage_m: 40, + }, + { + ...base, + structure_id: "m528b", + type_id: "ditch_side", + placement: "interval", + start_m: 60, + end_m: 80, + }, + ]); + out.seeded = api.readPendingStructures(projectId).length; + return JSON.stringify(out); + } + if (MODE === "clear") { + api.writePendingStructures(projectId, null); + out.cleared = api.readPendingStructures(projectId) === null; + return JSON.stringify(out); + } + const groupSel = [...document.querySelectorAll("select")].find((s) => + [...s.options].some((o) => o.text.includes("A 횡단배수")), + ); + const isBD = (o) => /^(B|D) /.test(o.text); + out.visibleGroups = [...groupSel.options].filter((o) => !o.hidden && o.value).map((o) => o.text); + out.visibleBD = [...groupSel.options].filter((o) => !o.hidden && isBD(o)).length; + out.hiddenGroups = [...groupSel.options].filter((o) => o.hidden).map((o) => o.text); + // 우클릭 메뉴 목록(공용 만들기) — 종류 전부를 줘도 B · D 군이 안 나옴 + const menuMod = await import(modUrl("/B05_Profile/B05_Profile_UI_Profile_Structures.ts")); + const types = (await api.fetchStructureTypes(true)).map((t) => ({ + type_id: t.type_id, + group: t.group, + name: t.name, + })); + const menu = menuMod.structureGroupMenuItems(types, () => {}); + out.menuGroups = menu.map((m) => m.label); + out.menuHasBD = menu.filter((m) => /^(B|D) /.test(m.label)).length; + // 옛 값 — 목록에 그대로 · 열면 그 군으로 열림 + const rows = [...document.querySelectorAll(".b05-route__irregular-list li")]; + out.rows = rows.map((r) => r.textContent.trim().replace(/\s+/g, " ").slice(0, 40)); + const old = rows.find((r) => + (window.__m528row ? new RegExp(window.__m528row) : /골막이/).test(r.textContent), + ); + if (old) { + (old.querySelector("button") ?? old).click(); + await w(1500); + const sels = [...document.querySelectorAll("select")].filter((s) => + [...s.options].some((o) => o.text.includes("A 횡단배수")), + ); + // 다시 열면 고른 것이 복원돼 있음 — 누르면 풀리므로 풀렸으면 한 번 더 + if (!sels[0]?.value) { + (old.querySelector("button") ?? old).click(); + await w(1500); + } + out.editGroup = sels[0]?.value; + // 종단배수(B)는 조합 잇기 칸이 없음(0) · 계류사방(D)은 옛 그대로(1) + out.pickLinks = [...document.querySelectorAll('[data-b05-summary="pick"]')].filter( + (n) => n.offsetParent, + ).length; + } + return JSON.stringify(out); +})(); From 3cebfc6d0ae53f9c4292e511d429f2d9973e5939 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 5 Oct 2026 23:47:37 +0900 Subject: [PATCH 9/9] =?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(=EC=A2=8C=EC=B8=A1=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=C2=B7=20=EA=B8=B0=EC=A1=B4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=A7=8C)=20=EC=9B=90=EB=AC=B8?= 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_01FJqWQpoB1RE7hiaQ2wH8qV --- 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 좌측 패널의 생성과 기존 로직 연결에만 신경쓸것. +나는 이제 자러 갈거니 너가 다른 워크트리들을 잘 제어해서 계획서의 내용을 반영하고 최종검증을 진행 후 아티팩트로 결과를 보여주는데 만약 개선이 필요하다고 판단되는 부분이 있다면 계획서에 내용을 추가해서 개선할것.