feat(B05): 구조물 선택지 재편·배관 부속 옵션 — B06 이관 보류·리셋 빈값
- 레지스트리: 바닥막이·생태연못 삭제(임도 미사용 확정), B·F·G군 10종 enabled:false 보존(B06 횡단도 옵션 재사용), 배관 부속 13종(유형/관경 기본 1000/집수정/기슭막이/돌붙임)·BOX암거 본체+날개벽(유입·유출 개별) 옵션 정의 - FacilityEditor 재작성: 유형 제안(자동 배치 source 승격), 집수정 유무 토글로 유입부 옵션 전환, 기슭막이 길이 총/앞/뒤 상호 연계로 범위(시작·종료) 자동 - 구조물 배치: 군·종류 빈값 옵션+리셋 시 초기화, 사용법 설명 문단 제거 - tmp/tests 98건·ruff·prettier·typecheck·build 통과 (2026-08-17 사용자 확정)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 docs/raw/PLAN.md 구조물 리스트(2026-08-16)와 resources/knowledge/technical_info/01_임도 참조. enabled 기본 true(2026-08-16 사용자 확정 — 제외 표기 항목만 false). 배관은 pipe_points.json 정본 관리(managed_by) — structures.json에 저장하지 않는다. ★기본값 원칙(2026-08-16 크로스체크 2차 반영): default를 두는 것은 ①법정이 등급과 무관하게 못박은 단일 수치 ②사용자가 개발 단계에서 확정한 값 ③도메인 수치가 아닌 표시용 문자열, 이 셋뿐이다. 재료·형식 같은 설계자 판단 선택지와 미확정 제원은 default 없이 required로 두어 사용자가 직접 고르게 한다 (지식DB 원칙: 기본값 선정은 사용자 협의 영역).",
|
||||
"comment": "B05 구조물 타입 레지스트리 정본. 근거·옵션 상세는 docs/raw/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 원칙값+사용자 확정 — 화이트리스트 등재).",
|
||||
"types": [
|
||||
{
|
||||
"type_id": "pipe",
|
||||
@@ -12,14 +12,128 @@
|
||||
"color": "#4f8ef7",
|
||||
"abbr": "관"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"options": []
|
||||
"drawing_views": ["plan", "profile", "cross_section", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "flow_type",
|
||||
"label": "유형",
|
||||
"input": "select",
|
||||
"choices": ["계곡부형", "보완형"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "pipe_diameter_mm",
|
||||
"label": "관경",
|
||||
"input": "select",
|
||||
"choices": ["800", "1000", "1200", "1500"],
|
||||
"unit": "mm",
|
||||
"default": "1000",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "pipe_kind",
|
||||
"label": "관종",
|
||||
"input": "select",
|
||||
"choices": ["흄관", "VR관", "파형강관"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "catch_basin",
|
||||
"label": "집수정",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "catch_basin_form",
|
||||
"label": "집수정 형식",
|
||||
"input": "select",
|
||||
"choices": ["동물이동형", "□형(기본형)", "돌집수정 ㄴ형", "돌집수정 ㄷ형"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "revetment",
|
||||
"label": "기슭막이",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "revet_length_m",
|
||||
"label": "기슭막이 길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "revet_front_m",
|
||||
"label": "기슭막이 앞 분배",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "revet_form",
|
||||
"label": "기슭막이 형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"돌쌓기(찰)",
|
||||
"돌쌓기(메)",
|
||||
"돌붙임",
|
||||
"콘크리트",
|
||||
"돌망태",
|
||||
"통나무·목재틀",
|
||||
"바자"
|
||||
],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "revet_height_m",
|
||||
"label": "기슭막이 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_pitching",
|
||||
"label": "돌붙임",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "stone_pitching_type",
|
||||
"label": "돌붙임 형식",
|
||||
"input": "select",
|
||||
"choices": ["찰", "메"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "stone_pitching_area_m2",
|
||||
"label": "돌붙임 면적",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "box_culvert",
|
||||
@@ -31,14 +145,97 @@
|
||||
"color": "#3b6fd4",
|
||||
"abbr": "암"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"options": []
|
||||
"drawing_views": ["plan", "profile", "cross_section", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "body_width_m",
|
||||
"label": "본체 폭",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "body_height_m",
|
||||
"label": "본체 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_in",
|
||||
"label": "날개벽(유입)",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "wing_in_height_m",
|
||||
"label": "유입 날개벽 짧은쪽 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_in_length_m",
|
||||
"label": "유입 날개벽 길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_in_angle_deg",
|
||||
"label": "유입 날개벽 각도",
|
||||
"input": "number",
|
||||
"unit": "°",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_out",
|
||||
"label": "날개벽(유출)",
|
||||
"input": "select",
|
||||
"choices": ["있음", "없음"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"key": "wing_out_height_m",
|
||||
"label": "유출 날개벽 짧은쪽 높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_out_length_m",
|
||||
"label": "유출 날개벽 길이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "wing_out_angle_deg",
|
||||
"label": "유출 날개벽 각도",
|
||||
"input": "number",
|
||||
"unit": "°",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "ford_pavement",
|
||||
@@ -50,13 +247,7 @@
|
||||
"color": "#2f9e8f",
|
||||
"abbr": "물"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "cross_section", "detail", "quantity"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -69,23 +260,13 @@
|
||||
"color": "#28857a",
|
||||
"abbr": "세"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "cross_section", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "pipe_kind",
|
||||
"label": "구체 내 배관 종류",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"흄관",
|
||||
"VR관",
|
||||
"파형강관"
|
||||
],
|
||||
"choices": ["흄관", "VR관", "파형강관"],
|
||||
"default": null,
|
||||
"required": false
|
||||
},
|
||||
@@ -93,12 +274,7 @@
|
||||
"key": "pipe_diameter_mm",
|
||||
"label": "배관 크기",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"800",
|
||||
"1000",
|
||||
"1200",
|
||||
"1500"
|
||||
],
|
||||
"choices": ["800", "1000", "1200", "1500"],
|
||||
"unit": "mm",
|
||||
"default": null,
|
||||
"required": false
|
||||
@@ -122,12 +298,7 @@
|
||||
"color": "#5aa7e8",
|
||||
"abbr": "횡"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "cross_section", "quantity"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -139,16 +310,12 @@
|
||||
"color": "#6d9bd1",
|
||||
"abbr": "개"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "cross_section", "quantity"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "ditch_side",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "측구(옆도랑)",
|
||||
"placement": "interval",
|
||||
@@ -156,22 +323,13 @@
|
||||
"color": "#2eaadc",
|
||||
"abbr": "측"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"일반형(제형)",
|
||||
"L형",
|
||||
"U형",
|
||||
"J형"
|
||||
],
|
||||
"choices": ["일반형(제형)", "L형", "U형", "J형"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -187,6 +345,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "ditch_ridge",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "산마루측구",
|
||||
"placement": "interval",
|
||||
@@ -194,21 +353,13 @@
|
||||
"color": "#2e8bdc",
|
||||
"abbr": "산"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "material",
|
||||
"label": "재료",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"콘크리트",
|
||||
"돌",
|
||||
"떼"
|
||||
],
|
||||
"choices": ["콘크리트", "돌", "떼"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -217,6 +368,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "ditch_berm",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "소단측구",
|
||||
"placement": "interval",
|
||||
@@ -224,14 +376,12 @@
|
||||
"color": "#2ec5dc",
|
||||
"abbr": "소"
|
||||
},
|
||||
"drawing_views": [
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["cross_section", "quantity"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"type_id": "chute",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "도수로·산비탈수로",
|
||||
"placement": "interval",
|
||||
@@ -239,24 +389,13 @@
|
||||
"color": "#1f9bb8",
|
||||
"abbr": "도"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "material",
|
||||
"label": "재료",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"떼",
|
||||
"돌",
|
||||
"콘크리트",
|
||||
"블록판",
|
||||
"벤치플륨관",
|
||||
"반원관"
|
||||
],
|
||||
"choices": ["떼", "돌", "콘크리트", "블록판", "벤치플륨관", "반원관"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -265,11 +404,7 @@
|
||||
"key": "section",
|
||||
"label": "단면형",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"반달꼴",
|
||||
"U자형",
|
||||
"사다리꼴"
|
||||
],
|
||||
"choices": ["반달꼴", "U자형", "사다리꼴"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -278,6 +413,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "slope_drain",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "절토사면 배수로",
|
||||
"placement": "interval",
|
||||
@@ -285,19 +421,13 @@
|
||||
"color": "#28b1a1",
|
||||
"abbr": "사"
|
||||
},
|
||||
"drawing_views": [
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "material",
|
||||
"label": "재료",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"떼",
|
||||
"돌"
|
||||
],
|
||||
"choices": ["떼", "돌"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -306,6 +436,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "underdrain",
|
||||
"enabled": false,
|
||||
"group": "B",
|
||||
"name": "맹암거",
|
||||
"placement": "interval",
|
||||
@@ -313,11 +444,7 @@
|
||||
"color": "#1f7fb8",
|
||||
"abbr": "맹"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "note",
|
||||
@@ -336,23 +463,13 @@
|
||||
"color": "#e07a2e",
|
||||
"abbr": "옹"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"중력식",
|
||||
"반중력식",
|
||||
"캔틸레버식",
|
||||
"부벽식"
|
||||
],
|
||||
"choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -377,11 +494,7 @@
|
||||
"color": "#d0692a",
|
||||
"abbr": "찰"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "height_m",
|
||||
@@ -396,15 +509,7 @@
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"25",
|
||||
"30",
|
||||
"35",
|
||||
"45",
|
||||
"55",
|
||||
"60",
|
||||
"75"
|
||||
],
|
||||
"choices": ["25", "30", "35", "45", "55", "60", "75"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -420,11 +525,7 @@
|
||||
"color": "#c25d2c",
|
||||
"abbr": "메"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "height_m",
|
||||
@@ -439,15 +540,7 @@
|
||||
"key": "back_len_cm",
|
||||
"label": "뒷길이",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"25",
|
||||
"30",
|
||||
"35",
|
||||
"45",
|
||||
"55",
|
||||
"60",
|
||||
"75"
|
||||
],
|
||||
"choices": ["25", "30", "35", "45", "55", "60", "75"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -463,25 +556,13 @@
|
||||
"color": "#b8742e",
|
||||
"abbr": "흙"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"콘크리트",
|
||||
"돌(찰)",
|
||||
"돌(메)",
|
||||
"블록쌓기",
|
||||
"돌망태",
|
||||
"흙포대",
|
||||
"통나무쌓기"
|
||||
],
|
||||
"choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -506,11 +587,7 @@
|
||||
"color": "#a86a2a",
|
||||
"abbr": "큰"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "height_m",
|
||||
@@ -525,11 +602,7 @@
|
||||
"key": "stone_cm",
|
||||
"label": "돌규격",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"40~60",
|
||||
"60~80",
|
||||
"80~100"
|
||||
],
|
||||
"choices": ["40~60", "60~80", "80~100"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -545,25 +618,13 @@
|
||||
"color": "#7a52c7",
|
||||
"abbr": "골"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"돌",
|
||||
"돌망태",
|
||||
"콘크리트",
|
||||
"통나무",
|
||||
"바자",
|
||||
"흙"
|
||||
],
|
||||
"choices": ["돌", "돌망태", "콘크리트", "통나무", "바자", "흙"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -593,12 +654,7 @@
|
||||
"color": "#6b46b8",
|
||||
"abbr": "댐"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"profile",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "profile", "detail", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "height_m",
|
||||
@@ -611,44 +667,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "bed_sill",
|
||||
"group": "D",
|
||||
"name": "바닥막이",
|
||||
"placement": "point",
|
||||
"style": {
|
||||
"color": "#8a5cd0",
|
||||
"abbr": "바"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "height_m",
|
||||
"label": "높이(≤3.0)",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
},
|
||||
{
|
||||
"key": "apron",
|
||||
"label": "물받이",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"있음",
|
||||
"없음"
|
||||
],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "revetment",
|
||||
"group": "D",
|
||||
@@ -658,11 +676,7 @@
|
||||
"color": "#9b6bdc",
|
||||
"abbr": "기"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
@@ -701,11 +715,7 @@
|
||||
"color": "#2fa84f",
|
||||
"abbr": "대"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "width_m",
|
||||
@@ -734,11 +744,7 @@
|
||||
"color": "#268f43",
|
||||
"abbr": "작"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "width_m",
|
||||
@@ -765,10 +771,7 @@
|
||||
"color": "#37b95a",
|
||||
"abbr": "차"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "width_m",
|
||||
@@ -788,19 +791,13 @@
|
||||
"color": "#43c76b",
|
||||
"abbr": "반"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"drawing_views": ["plan"],
|
||||
"options": [
|
||||
{
|
||||
"key": "facing",
|
||||
"label": "방향",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"좌",
|
||||
"우",
|
||||
"양면"
|
||||
],
|
||||
"choices": ["좌", "우", "양면"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -816,21 +813,13 @@
|
||||
"color": "#1f9e46",
|
||||
"abbr": "가"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "kind",
|
||||
"label": "종류",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"가드레일",
|
||||
"경계석",
|
||||
"위험표지"
|
||||
],
|
||||
"choices": ["가드레일", "경계석", "위험표지"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
@@ -846,10 +835,7 @@
|
||||
"color": "#53a832",
|
||||
"abbr": "번"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"detail"
|
||||
],
|
||||
"drawing_views": ["plan", "detail"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -861,10 +847,7 @@
|
||||
"color": "#3f9e2e",
|
||||
"abbr": "단"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"detail"
|
||||
],
|
||||
"drawing_views": ["plan", "detail"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -876,9 +859,7 @@
|
||||
"color": "#66b344",
|
||||
"abbr": "준"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan"
|
||||
],
|
||||
"drawing_views": ["plan"],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -890,10 +871,7 @@
|
||||
"color": "#8c9a2e",
|
||||
"abbr": "사"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "area_m2",
|
||||
@@ -924,10 +902,7 @@
|
||||
"color": "#7d8a2a",
|
||||
"abbr": "토"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["plan", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "area_m2",
|
||||
@@ -951,6 +926,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "wildlife_path",
|
||||
"enabled": false,
|
||||
"group": "F",
|
||||
"name": "야생동물 이동통로",
|
||||
"placement": "point",
|
||||
@@ -958,51 +934,22 @@
|
||||
"color": "#3aa88f",
|
||||
"abbr": "야"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"cross_section"
|
||||
],
|
||||
"drawing_views": ["plan", "cross_section"],
|
||||
"options": [
|
||||
{
|
||||
"key": "form",
|
||||
"label": "형식",
|
||||
"input": "select",
|
||||
"choices": [
|
||||
"경사로",
|
||||
"자연형계단"
|
||||
],
|
||||
"choices": ["경사로", "자연형계단"],
|
||||
"default": null,
|
||||
"required": true,
|
||||
"phase": "detail"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "eco_pond",
|
||||
"group": "F",
|
||||
"name": "생태연못",
|
||||
"placement": "site",
|
||||
"style": {
|
||||
"color": "#2f9e86",
|
||||
"abbr": "연"
|
||||
},
|
||||
"drawing_views": [
|
||||
"plan",
|
||||
"detail",
|
||||
"quantity"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"key": "area_m2",
|
||||
"label": "면적(≥100)",
|
||||
"input": "number",
|
||||
"unit": "㎡",
|
||||
"default": 100
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type_id": "revegetation",
|
||||
"enabled": false,
|
||||
"group": "F",
|
||||
"name": "비탈면 녹화",
|
||||
"placement": "interval",
|
||||
@@ -1010,10 +957,7 @@
|
||||
"color": "#4cb84c",
|
||||
"abbr": "녹"
|
||||
},
|
||||
"drawing_views": [
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "method",
|
||||
@@ -1040,6 +984,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "pavement_concrete",
|
||||
"enabled": false,
|
||||
"group": "G",
|
||||
"name": "콘크리트 포장",
|
||||
"placement": "interval",
|
||||
@@ -1047,11 +992,7 @@
|
||||
"color": "#8a8f98",
|
||||
"abbr": "포"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile",
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["profile", "cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "thickness_cm",
|
||||
@@ -1066,6 +1007,7 @@
|
||||
},
|
||||
{
|
||||
"type_id": "gravel_surfacing",
|
||||
"enabled": false,
|
||||
"group": "G",
|
||||
"name": "쇄석(자갈) 포설",
|
||||
"placement": "interval",
|
||||
@@ -1073,10 +1015,7 @@
|
||||
"color": "#9aa0a8",
|
||||
"abbr": "쇄"
|
||||
},
|
||||
"drawing_views": [
|
||||
"cross_section",
|
||||
"quantity"
|
||||
],
|
||||
"drawing_views": ["cross_section", "quantity"],
|
||||
"options": [
|
||||
{
|
||||
"key": "thickness_cm",
|
||||
@@ -1098,9 +1037,7 @@
|
||||
"color": "#98a2b3",
|
||||
"abbr": "기"
|
||||
},
|
||||
"drawing_views": [
|
||||
"profile"
|
||||
],
|
||||
"drawing_views": ["profile"],
|
||||
"options": [
|
||||
{
|
||||
"key": "name",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile_UI_Drainage_Facility.ts
|
||||
* 계곡 통과 시설 편집 소구획 — 선택한 관 지점의 시설 종류·구간·세월교 옵션.
|
||||
* 계곡 통과 시설 편집 소구획 — 선택한 관 지점의 시설 종류·구간·부속 옵션.
|
||||
*
|
||||
* 같은 계곡 교차점에서 유량·지형에 따라 배관/BOX암거/물넘이포장/세월교를 택일한다
|
||||
* (2026-08-17 사용자 확정 — 교량은 임도용이 아니라 없다). 정본은 `pipe_points.json`
|
||||
* 하나이고, 세부유역 계산은 기준점(chainage)만 읽으므로 종류를 바꿔도 계산은 같다.
|
||||
*
|
||||
* B05는 유무·종류·위치 단계라 여기서는 종류·구간·관 수량 수준만 받는다 — 날개벽
|
||||
* 규격 같은 상세 치수는 B06/B07 몫이다. 시설 확장 정보는 패널이 chainage 기준으로
|
||||
* 들고 다니며 요청에 되붙인다(`FacilityStore`).
|
||||
* 배관 부속(2026-08-17 확정): 유형(계곡부형/보완형 — 자동 배치 source로 제안),
|
||||
* 집수정 유무(토글 시 유입부 옵션 전환), 관경(기본 1000), 기슭막이(길이 총/앞/뒤
|
||||
* 상호 연계 — 범위를 만든다), 돌붙임. 형식·치수 제원은 B06/B07 필수 승격 대상이라
|
||||
* 여기서는 보이되 비필수다. 시설 확장 정보는 패널이 chainage 기준으로 들고 다니며
|
||||
* 요청에 되붙인다(`FacilityStore`).
|
||||
* ========================================================================== */
|
||||
|
||||
import {
|
||||
@@ -18,7 +20,8 @@ import {
|
||||
type PipeSource,
|
||||
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
|
||||
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. */
|
||||
/** 시설 확장 정보 한 건 — 관 지점(chainage)에 얹힌다. 배관(pipe)도 부속 옵션을
|
||||
* 가지면 저장한다. */
|
||||
export interface FacilityAttributes {
|
||||
facility: PipeFacility;
|
||||
start_m?: number;
|
||||
@@ -26,6 +29,13 @@ export interface FacilityAttributes {
|
||||
options?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
/** 확장 정보가 실제로 있는지 — 배관이면서 아무 부속도 없으면 보관할 이유가 없다. */
|
||||
function hasExtras(attributes: FacilityAttributes): boolean {
|
||||
if (attributes.facility !== "pipe") return true;
|
||||
if (attributes.start_m !== undefined || attributes.end_m !== undefined) return true;
|
||||
return !!attributes.options && Object.keys(attributes.options).length > 0;
|
||||
}
|
||||
|
||||
/** 관 이동·재계산 왕복에서 시설 정보를 잃지 않게 chainage 최근접으로 되붙이는 보관소.
|
||||
* 백엔드 `carry_facility_attributes`와 같은 기준(정확 일치 → 최근접)이다. */
|
||||
export function createFacilityStore() {
|
||||
@@ -43,21 +53,21 @@ export function createFacilityStore() {
|
||||
}>,
|
||||
): void {
|
||||
entries = points
|
||||
.filter((point) => point.facility && point.facility !== "pipe")
|
||||
.map((point) => ({
|
||||
chainage: point.chainage_m,
|
||||
attributes: {
|
||||
facility: point.facility as PipeFacility,
|
||||
facility: (point.facility ?? "pipe") as PipeFacility,
|
||||
start_m: point.start_m,
|
||||
end_m: point.end_m,
|
||||
options: point.options,
|
||||
},
|
||||
}));
|
||||
}))
|
||||
.filter((entry) => hasExtras(entry.attributes));
|
||||
},
|
||||
/** 한 지점의 시설을 바꾼다(배관으로 되돌리면 엔트리 삭제). */
|
||||
/** 한 지점의 시설을 바꾼다(맨 배관으로 되돌리면 엔트리 삭제). */
|
||||
set(chainageM: number, attributes: FacilityAttributes | null): void {
|
||||
entries = entries.filter((entry) => Math.abs(entry.chainage - chainageM) > 0.005);
|
||||
if (attributes && attributes.facility !== "pipe") {
|
||||
if (attributes && hasExtras(attributes)) {
|
||||
entries.push({ chainage: chainageM, attributes });
|
||||
}
|
||||
},
|
||||
@@ -95,8 +105,12 @@ interface FacilityEditorCallbacks {
|
||||
|
||||
export interface FacilityEditor {
|
||||
root: HTMLElement;
|
||||
/** 선택된 관 지점을 폼에 올린다. null이면 숨긴다. */
|
||||
sync: (chainageM: number | null, attributes: FacilityAttributes | null) => void;
|
||||
/** 선택된 관 지점을 폼에 올린다. null이면 숨긴다. source는 유형 제안에 쓴다. */
|
||||
sync: (
|
||||
chainageM: number | null,
|
||||
attributes: FacilityAttributes | null,
|
||||
source?: PipeSource,
|
||||
) => void;
|
||||
}
|
||||
|
||||
function labeled(text: string, input: HTMLElement): HTMLLabelElement {
|
||||
@@ -108,6 +122,40 @@ function labeled(text: string, input: HTMLElement): HTMLLabelElement {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function numberInput(step: string, min: string, placeholder = ""): HTMLInputElement {
|
||||
const input = document.createElement("input");
|
||||
input.type = "number";
|
||||
input.step = step;
|
||||
input.min = min;
|
||||
if (placeholder) input.placeholder = placeholder;
|
||||
return input;
|
||||
}
|
||||
|
||||
/** 첫 항목이 빈값(자동/미지정)인 셀렉트 — 명시하지 않으면 저장하지 않는다. */
|
||||
function optionalSelect(blankLabel: string, choices: ReadonlyArray<string>): HTMLSelectElement {
|
||||
const element = document.createElement("select");
|
||||
element.replaceChildren(
|
||||
new Option(blankLabel, ""),
|
||||
...choices.map((choice) => new Option(choice, choice)),
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
function row(...fields: HTMLElement[]): HTMLDivElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = "b05-route__irregular-row";
|
||||
div.append(...fields);
|
||||
return div;
|
||||
}
|
||||
|
||||
/** 자동 배치 source → 유형 제안 (2026-08-17 확정: 세류선 교차 = 계곡부형(유량 多),
|
||||
* 간격 보충 = 보완형(유량 少·집수정), 수동 = 미지정). */
|
||||
function suggestFlowType(source: PipeSource | undefined): string {
|
||||
if (source === "stream") return "계곡부형";
|
||||
if (source === "spacing") return "보완형";
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 선택한 관 지점 아래 붙는 시설 편집 폼. */
|
||||
export function createFacilityEditor(callbacks: FacilityEditorCallbacks): FacilityEditor {
|
||||
const root = document.createElement("div");
|
||||
@@ -119,56 +167,188 @@ export function createFacilityEditor(callbacks: FacilityEditorCallbacks): Facili
|
||||
...PIPE_FACILITY_LABELS.map(([value, label]) => new Option(label, value)),
|
||||
);
|
||||
|
||||
// 구간 — 기준점 앞뒤로 유입·유출 부속이 차지하는 범위(m). 비우면 폭 0(기준점만).
|
||||
const startInput = document.createElement("input");
|
||||
startInput.type = "number";
|
||||
startInput.step = "0.1";
|
||||
startInput.min = "0";
|
||||
startInput.placeholder = "비우면 기준점";
|
||||
const endInput = document.createElement("input");
|
||||
endInput.type = "number";
|
||||
endInput.step = "0.1";
|
||||
endInput.min = "0";
|
||||
endInput.placeholder = "비우면 기준점";
|
||||
// 구간 — 기준점 앞뒤로 유입·유출 부속이 차지하는 범위(m, 누가거리). 비우면 폭 0.
|
||||
// 기슭막이 길이를 넣으면 자동으로 채워지고, 직접 고치면 그 값이 우선한다.
|
||||
const startInput = numberInput("0.1", "0", "비우면 기준점");
|
||||
const endInput = numberInput("0.1", "0", "비우면 기준점");
|
||||
|
||||
// 세월교 전용 — 구체 내 배관 종류/크기/수량으로 폭을 정한다(2026-08-17 사용자 확정).
|
||||
const pipeKindSelect = document.createElement("select");
|
||||
pipeKindSelect.replaceChildren(
|
||||
...["", "흄관", "VR관", "파형강관"].map(
|
||||
(kind) => new Option(kind === "" ? "선택하세요" : kind, kind),
|
||||
),
|
||||
// ── 배관 부속 (2026-08-17 확정) ──────────────────────────────────────────
|
||||
const flowSelect = optionalSelect("미지정", ["계곡부형", "보완형"]);
|
||||
const catchBasinSelect = optionalSelect("자동(유형 따름)", ["있음", "없음"]);
|
||||
const catchBasinFormSelect = optionalSelect("선택", [
|
||||
"동물이동형",
|
||||
"□형(기본형)",
|
||||
"돌집수정 ㄴ형",
|
||||
"돌집수정 ㄷ형",
|
||||
]);
|
||||
const pipeDiameterSelect = document.createElement("select");
|
||||
pipeDiameterSelect.replaceChildren(
|
||||
...["800", "1000", "1200", "1500"].map((size) => new Option(`Ø${size}`, size)),
|
||||
);
|
||||
const diameterSelect = document.createElement("select");
|
||||
diameterSelect.replaceChildren(
|
||||
...["", "800", "1000", "1200", "1500"].map(
|
||||
(size) => new Option(size === "" ? "선택하세요" : `Ø${size}`, size),
|
||||
),
|
||||
);
|
||||
const countInput = document.createElement("input");
|
||||
countInput.type = "number";
|
||||
countInput.step = "1";
|
||||
countInput.min = "1";
|
||||
countInput.placeholder = "련";
|
||||
pipeDiameterSelect.value = "1000"; // 별표2 원칙값 + 사용자 확정 기본 (2026-08-17)
|
||||
const pipeMaterialSelect = optionalSelect("선택", ["흄관", "VR관", "파형강관"]);
|
||||
const revetSelect = optionalSelect("자동(유형 따름)", ["있음", "없음"]);
|
||||
const revetLength = numberInput("0.1", "0");
|
||||
const revetFront = numberInput("0.1", "0");
|
||||
const revetBack = numberInput("0.1", "0");
|
||||
const revetFormSelect = optionalSelect("선택", [
|
||||
"돌쌓기(찰)",
|
||||
"돌쌓기(메)",
|
||||
"돌붙임",
|
||||
"콘크리트",
|
||||
"돌망태",
|
||||
"통나무·목재틀",
|
||||
"바자",
|
||||
]);
|
||||
const pitchSelect = optionalSelect("자동(유형 따름)", ["있음", "없음"]);
|
||||
const pitchTypeSelect = optionalSelect("선택", ["찰", "메"]);
|
||||
|
||||
const spanRow = document.createElement("div");
|
||||
spanRow.className = "b05-route__irregular-row";
|
||||
spanRow.append(labeled("시작 (m)", startInput), labeled("종료 (m)", endInput));
|
||||
const fordRow = document.createElement("div");
|
||||
fordRow.className = "b05-route__irregular-row";
|
||||
fordRow.append(
|
||||
labeled("배관 종류", pipeKindSelect),
|
||||
labeled("배관 크기", diameterSelect),
|
||||
labeled("수량 (련)", countInput),
|
||||
// ── BOX암거 — 본체·날개벽 (짧은쪽 높이/길이/각도, 유입·유출 개별) ─────────
|
||||
const boxWidth = numberInput("0.1", "0");
|
||||
const boxHeight = numberInput("0.1", "0");
|
||||
const wingIn = optionalSelect("미지정", ["있음", "없음"]);
|
||||
const wingInHeight = numberInput("0.1", "0");
|
||||
const wingInLength = numberInput("0.1", "0");
|
||||
const wingInAngle = numberInput("1", "0");
|
||||
const wingOut = optionalSelect("미지정", ["있음", "없음"]);
|
||||
const wingOutHeight = numberInput("0.1", "0");
|
||||
const wingOutLength = numberInput("0.1", "0");
|
||||
const wingOutAngle = numberInput("1", "0");
|
||||
|
||||
// ── 세월교 — 구체 내 배관 종류/크기/수량으로 폭을 정한다 ─────────────────
|
||||
const fordKindSelect = optionalSelect("선택", ["흄관", "VR관", "파형강관"]);
|
||||
const fordDiameterSelect = optionalSelect("선택", ["800", "1000", "1200", "1500"]);
|
||||
const fordCount = numberInput("1", "1", "련");
|
||||
|
||||
const spanRow = row(labeled("시작 (m)", startInput), labeled("종료 (m)", endInput));
|
||||
const pipeTypeRow = row(
|
||||
labeled("유형", flowSelect),
|
||||
labeled("관경", pipeDiameterSelect),
|
||||
labeled("관종", pipeMaterialSelect),
|
||||
);
|
||||
const pipeInletRow = row(
|
||||
labeled("집수정", catchBasinSelect),
|
||||
labeled("집수정 형식", catchBasinFormSelect),
|
||||
);
|
||||
const revetRow = row(
|
||||
labeled("기슭막이", revetSelect),
|
||||
labeled("길이 (m)", revetLength),
|
||||
labeled("앞 (m)", revetFront),
|
||||
labeled("뒤 (m)", revetBack),
|
||||
labeled("형식", revetFormSelect),
|
||||
);
|
||||
const pitchRow = row(labeled("돌붙임", pitchSelect), labeled("형식", pitchTypeSelect));
|
||||
const boxRow = row(labeled("본체 폭 (m)", boxWidth), labeled("본체 높이 (m)", boxHeight));
|
||||
const wingInRow = row(
|
||||
labeled("날개벽(유입)", wingIn),
|
||||
labeled("짧은쪽 높이 (m)", wingInHeight),
|
||||
labeled("길이 (m)", wingInLength),
|
||||
labeled("각도 (°)", wingInAngle),
|
||||
);
|
||||
const wingOutRow = row(
|
||||
labeled("날개벽(유출)", wingOut),
|
||||
labeled("짧은쪽 높이 (m)", wingOutHeight),
|
||||
labeled("길이 (m)", wingOutLength),
|
||||
labeled("각도 (°)", wingOutAngle),
|
||||
);
|
||||
const fordRow = row(
|
||||
labeled("배관 종류", fordKindSelect),
|
||||
labeled("배관 크기", fordDiameterSelect),
|
||||
labeled("수량 (련)", fordCount),
|
||||
);
|
||||
|
||||
root.append(labeled("시설 종류", facilitySelect), spanRow, fordRow);
|
||||
root.append(
|
||||
labeled("시설 종류", facilitySelect),
|
||||
spanRow,
|
||||
pipeTypeRow,
|
||||
pipeInletRow,
|
||||
revetRow,
|
||||
pitchRow,
|
||||
boxRow,
|
||||
wingInRow,
|
||||
wingOutRow,
|
||||
fordRow,
|
||||
);
|
||||
|
||||
let currentChainage: number | null = null;
|
||||
let currentSource: PipeSource | undefined;
|
||||
let syncing = false;
|
||||
|
||||
function readAttributes(): FacilityAttributes | null {
|
||||
/** 명시값이 없을 때의 유형 — 자동 배치 source 제안을 따른다. */
|
||||
function effectiveFlow(): string {
|
||||
return flowSelect.value || suggestFlowType(currentSource);
|
||||
}
|
||||
|
||||
/** 집수정 유무 — 명시값 우선, 없으면 유형이 정한다(보완형 = 있음). */
|
||||
function effectiveCatchBasin(): string {
|
||||
if (catchBasinSelect.value) return catchBasinSelect.value;
|
||||
const flow = effectiveFlow();
|
||||
return flow ? (flow === "보완형" ? "있음" : "없음") : "";
|
||||
}
|
||||
|
||||
/** 기슭막이 총/앞/뒤 상호 연계(2026-08-17 확정 — 예: 총 10 중 앞 4 → 뒤 6).
|
||||
* changed 필드를 기준으로 나머지를 재계산한다. */
|
||||
function linkRevet(changed: "length" | "front" | "back"): void {
|
||||
const length = Number.parseFloat(revetLength.value);
|
||||
if (!Number.isFinite(length) || length <= 0) return;
|
||||
let front = Number.parseFloat(revetFront.value);
|
||||
let back = Number.parseFloat(revetBack.value);
|
||||
if (changed === "back" && Number.isFinite(back)) {
|
||||
back = Math.min(Math.max(back, 0), length);
|
||||
front = length - back;
|
||||
} else {
|
||||
front = Number.isFinite(front) ? Math.min(Math.max(front, 0), length) : length / 2;
|
||||
back = length - front;
|
||||
}
|
||||
revetFront.value = String(Number(front.toFixed(1)));
|
||||
revetBack.value = String(Number(back.toFixed(1)));
|
||||
}
|
||||
|
||||
/** 기슭막이 길이로 구간을 자동 채운다(범위 = 기준 − 앞 ~ 기준 + 뒤).
|
||||
* 시작·종료를 직접 고친 경우는 건드리지 않도록, 기슭막이 입력 change에서만 부른다. */
|
||||
function applyRevetSpan(): void {
|
||||
if (currentChainage === null) return;
|
||||
const length = Number.parseFloat(revetLength.value);
|
||||
const front = Number.parseFloat(revetFront.value);
|
||||
const back = Number.parseFloat(revetBack.value);
|
||||
if (!Number.isFinite(length) || length <= 0) return;
|
||||
if (!Number.isFinite(front) || !Number.isFinite(back)) return;
|
||||
startInput.value = (currentChainage - front).toFixed(1);
|
||||
endInput.value = (currentChainage + back).toFixed(1);
|
||||
}
|
||||
|
||||
function syncVisibility(): void {
|
||||
const facility = facilitySelect.value as PipeFacility;
|
||||
const isPipe = facility === "pipe";
|
||||
const isBox = facility === "box_culvert";
|
||||
pipeTypeRow.hidden = !isPipe;
|
||||
pipeInletRow.hidden = !isPipe;
|
||||
revetRow.hidden = !isPipe;
|
||||
pitchRow.hidden = !isPipe;
|
||||
boxRow.hidden = !isBox;
|
||||
wingInRow.hidden = !isBox;
|
||||
wingOutRow.hidden = !isBox;
|
||||
fordRow.hidden = facility !== "ford_bridge";
|
||||
if (isPipe) {
|
||||
// 유형 첫 항목에 제안을 드러낸다 — 저장은 사용자가 고른 값만.
|
||||
const suggested = suggestFlowType(currentSource);
|
||||
flowSelect.options[0].text = suggested ? `자동(${suggested})` : "미지정";
|
||||
catchBasinFormSelect.parentElement!.hidden = effectiveCatchBasin() !== "있음";
|
||||
pitchTypeSelect.parentElement!.hidden = pitchSelect.value === "없음";
|
||||
const noRevet = revetSelect.value === "없음";
|
||||
[revetLength, revetFront, revetBack, revetFormSelect].forEach(
|
||||
(input) => (input.parentElement!.hidden = noRevet),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function putNumber(target: Record<string, string | number>, key: string, raw: string): void {
|
||||
const value = Number.parseFloat(raw);
|
||||
if (Number.isFinite(value) && value > 0) target[key] = Number(value.toFixed(1));
|
||||
}
|
||||
|
||||
function readAttributes(): FacilityAttributes {
|
||||
const facility = facilitySelect.value as PipeFacility;
|
||||
if (facility === "pipe") return null;
|
||||
const attributes: FacilityAttributes = { facility };
|
||||
const start = Number.parseFloat(startInput.value);
|
||||
const end = Number.parseFloat(endInput.value);
|
||||
@@ -176,47 +356,136 @@ export function createFacilityEditor(callbacks: FacilityEditorCallbacks): Facili
|
||||
attributes.start_m = Math.min(start, end);
|
||||
attributes.end_m = Math.max(start, end);
|
||||
}
|
||||
if (facility === "ford_bridge") {
|
||||
const options: Record<string, string | number> = {};
|
||||
if (pipeKindSelect.value) options.pipe_kind = pipeKindSelect.value;
|
||||
if (diameterSelect.value) options.pipe_diameter_mm = Number(diameterSelect.value);
|
||||
const count = Number.parseInt(countInput.value, 10);
|
||||
const options: Record<string, string | number> = {};
|
||||
if (facility === "pipe") {
|
||||
if (flowSelect.value) options.flow_type = flowSelect.value;
|
||||
options.pipe_diameter_mm = Number(pipeDiameterSelect.value);
|
||||
if (pipeMaterialSelect.value) options.pipe_kind = pipeMaterialSelect.value;
|
||||
if (catchBasinSelect.value) options.catch_basin = catchBasinSelect.value;
|
||||
if (catchBasinFormSelect.value && effectiveCatchBasin() === "있음")
|
||||
options.catch_basin_form = catchBasinFormSelect.value;
|
||||
if (revetSelect.value) options.revetment = revetSelect.value;
|
||||
if (revetSelect.value !== "없음") {
|
||||
putNumber(options, "revet_length_m", revetLength.value);
|
||||
putNumber(options, "revet_front_m", revetFront.value);
|
||||
if (revetFormSelect.value) options.revet_form = revetFormSelect.value;
|
||||
}
|
||||
if (pitchSelect.value) options.stone_pitching = pitchSelect.value;
|
||||
if (pitchTypeSelect.value && pitchSelect.value !== "없음")
|
||||
options.stone_pitching_type = pitchTypeSelect.value;
|
||||
} else if (facility === "box_culvert") {
|
||||
putNumber(options, "body_width_m", boxWidth.value);
|
||||
putNumber(options, "body_height_m", boxHeight.value);
|
||||
if (wingIn.value) options.wing_in = wingIn.value;
|
||||
if (wingIn.value === "있음") {
|
||||
putNumber(options, "wing_in_height_m", wingInHeight.value);
|
||||
putNumber(options, "wing_in_length_m", wingInLength.value);
|
||||
putNumber(options, "wing_in_angle_deg", wingInAngle.value);
|
||||
}
|
||||
if (wingOut.value) options.wing_out = wingOut.value;
|
||||
if (wingOut.value === "있음") {
|
||||
putNumber(options, "wing_out_height_m", wingOutHeight.value);
|
||||
putNumber(options, "wing_out_length_m", wingOutLength.value);
|
||||
putNumber(options, "wing_out_angle_deg", wingOutAngle.value);
|
||||
}
|
||||
} else if (facility === "ford_bridge") {
|
||||
if (fordKindSelect.value) options.pipe_kind = fordKindSelect.value;
|
||||
if (fordDiameterSelect.value) options.pipe_diameter_mm = Number(fordDiameterSelect.value);
|
||||
const count = Number.parseInt(fordCount.value, 10);
|
||||
if (Number.isFinite(count) && count > 0) options.pipe_count = count;
|
||||
if (Object.keys(options).length) attributes.options = options;
|
||||
}
|
||||
if (Object.keys(options).length) attributes.options = options;
|
||||
return attributes;
|
||||
}
|
||||
|
||||
function syncVisibility(): void {
|
||||
const facility = facilitySelect.value as PipeFacility;
|
||||
spanRow.hidden = facility === "pipe";
|
||||
fordRow.hidden = facility !== "ford_bridge";
|
||||
}
|
||||
|
||||
function emit(): void {
|
||||
if (syncing || currentChainage === null) return;
|
||||
syncVisibility();
|
||||
callbacks.onApply(currentChainage, readAttributes());
|
||||
}
|
||||
|
||||
[facilitySelect, startInput, endInput, pipeKindSelect, diameterSelect, countInput].forEach(
|
||||
(input) => input.addEventListener("change", emit),
|
||||
[revetLength, revetFront, revetBack].forEach((input, index) =>
|
||||
input.addEventListener("change", () => {
|
||||
if (syncing) return;
|
||||
linkRevet(index === 2 ? "back" : index === 1 ? "front" : "length");
|
||||
applyRevetSpan();
|
||||
emit();
|
||||
}),
|
||||
);
|
||||
[
|
||||
facilitySelect,
|
||||
startInput,
|
||||
endInput,
|
||||
flowSelect,
|
||||
pipeDiameterSelect,
|
||||
pipeMaterialSelect,
|
||||
catchBasinSelect,
|
||||
catchBasinFormSelect,
|
||||
revetSelect,
|
||||
revetFormSelect,
|
||||
pitchSelect,
|
||||
pitchTypeSelect,
|
||||
boxWidth,
|
||||
boxHeight,
|
||||
wingIn,
|
||||
wingInHeight,
|
||||
wingInLength,
|
||||
wingInAngle,
|
||||
wingOut,
|
||||
wingOutHeight,
|
||||
wingOutLength,
|
||||
wingOutAngle,
|
||||
fordKindSelect,
|
||||
fordDiameterSelect,
|
||||
fordCount,
|
||||
].forEach((input) => input.addEventListener("change", emit));
|
||||
|
||||
return {
|
||||
root,
|
||||
sync(chainageM, attributes) {
|
||||
sync(chainageM, attributes, source) {
|
||||
currentChainage = chainageM;
|
||||
currentSource = source;
|
||||
root.hidden = chainageM === null;
|
||||
if (chainageM === null) return;
|
||||
syncing = true;
|
||||
const options = attributes?.options ?? {};
|
||||
const text = (key: string): string =>
|
||||
options[key] !== undefined ? String(options[key]) : "";
|
||||
facilitySelect.value = attributes?.facility ?? "pipe";
|
||||
startInput.value = attributes?.start_m !== undefined ? String(attributes.start_m) : "";
|
||||
endInput.value = attributes?.end_m !== undefined ? String(attributes.end_m) : "";
|
||||
const options = attributes?.options ?? {};
|
||||
pipeKindSelect.value = String(options.pipe_kind ?? "");
|
||||
diameterSelect.value = options.pipe_diameter_mm ? String(options.pipe_diameter_mm) : "";
|
||||
countInput.value = options.pipe_count ? String(options.pipe_count) : "";
|
||||
const isFord = attributes?.facility === "ford_bridge";
|
||||
flowSelect.value = text("flow_type");
|
||||
pipeDiameterSelect.value = (!isFord && text("pipe_diameter_mm")) || "1000";
|
||||
pipeMaterialSelect.value = isFord ? "" : text("pipe_kind");
|
||||
catchBasinSelect.value = text("catch_basin");
|
||||
catchBasinFormSelect.value = text("catch_basin_form");
|
||||
revetSelect.value = text("revetment");
|
||||
revetLength.value = text("revet_length_m");
|
||||
revetFront.value = text("revet_front_m");
|
||||
// 뒤 = 총 − 앞 (파생값 — 저장하지 않고 표시만 한다).
|
||||
const length = Number.parseFloat(revetLength.value);
|
||||
const front = Number.parseFloat(revetFront.value);
|
||||
revetBack.value =
|
||||
Number.isFinite(length) && Number.isFinite(front)
|
||||
? String(Number((length - front).toFixed(1)))
|
||||
: "";
|
||||
revetFormSelect.value = text("revet_form");
|
||||
pitchSelect.value = text("stone_pitching");
|
||||
pitchTypeSelect.value = text("stone_pitching_type");
|
||||
boxWidth.value = text("body_width_m");
|
||||
boxHeight.value = text("body_height_m");
|
||||
wingIn.value = text("wing_in");
|
||||
wingInHeight.value = text("wing_in_height_m");
|
||||
wingInLength.value = text("wing_in_length_m");
|
||||
wingInAngle.value = text("wing_in_angle_deg");
|
||||
wingOut.value = text("wing_out");
|
||||
wingOutHeight.value = text("wing_out_height_m");
|
||||
wingOutLength.value = text("wing_out_length_m");
|
||||
wingOutAngle.value = text("wing_out_angle_deg");
|
||||
fordKindSelect.value = isFord ? text("pipe_kind") : "";
|
||||
fordDiameterSelect.value = isFord ? text("pipe_diameter_mm") : "";
|
||||
fordCount.value = isFord ? text("pipe_count") : "";
|
||||
syncing = false;
|
||||
syncVisibility();
|
||||
},
|
||||
|
||||
@@ -366,6 +366,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
facilityEditor.sync(
|
||||
picked ? picked.chainage_m : null,
|
||||
picked ? facilityStore.get(picked.chainage_m) : null,
|
||||
// 자동 배치 source — 배관 유형(계곡부형/보완형) 제안 근거 (2026-08-17).
|
||||
picked ? ((picked.reason || "user") as PipeSource) : undefined,
|
||||
);
|
||||
selectBasin(basinIndexOfPipe(basins, picked ?? null));
|
||||
}
|
||||
|
||||
@@ -221,12 +221,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
|
||||
const list = document.createElement("ul");
|
||||
list.className = "b05-route__irregular-list";
|
||||
const help = document.createElement("p");
|
||||
help.className = "b05-route__note";
|
||||
help.textContent =
|
||||
"구조물군과 종류를 고르고 측점(측점번호 + 잔여거리)을 입력해 추가합니다. 구간형은 " +
|
||||
"기준 측점에 마크가 찍힙니다(비우면 시작 측점). 계곡 통과 시설(배관·BOX암거·물넘이·" +
|
||||
"세월교)은 배수유역 정본과 연동되고, 상세 치수는 B06/B07 단계에서 입력합니다.";
|
||||
// 사용법 설명 문단은 두지 않는다 — 공간 대비 의미 없음(2026-08-17 사용자 지시,
|
||||
// 필요 시 별도 설명 페이지로).
|
||||
|
||||
body.append(
|
||||
field("구조물군", groupSelect),
|
||||
@@ -236,7 +232,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
optionRow,
|
||||
actions,
|
||||
list,
|
||||
help,
|
||||
);
|
||||
|
||||
let types: StructureType[] = [];
|
||||
@@ -268,13 +263,16 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
const interval = (): number => callbacks.getInterval();
|
||||
|
||||
/** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설은 기준 측점만 받고
|
||||
* 설치측·이격도 정본(관 지점) 소관이라 감춘다. */
|
||||
* 설치측·이격도 정본(관 지점) 소관이라 감춘다. 종류 미선택(빈값)이면 입력 칸
|
||||
* 전체를 숨긴다 — 리셋 직후 상태(2026-08-17 사용자 지시). */
|
||||
function syncPlacementFields(): void {
|
||||
const type = currentType();
|
||||
const isInterval = !type?.managed_by && type?.placement === "interval";
|
||||
positionRow.hidden = !type;
|
||||
startFields.wrap.hidden = !isInterval;
|
||||
endFields.wrap.hidden = !isInterval;
|
||||
sideRow.hidden = !!type?.managed_by;
|
||||
sideRow.hidden = !type || !!type.managed_by;
|
||||
primary.disabled = !type;
|
||||
anchorFields.wrap.querySelector("span")!.textContent = isInterval
|
||||
? "기준 측점 (비우면 시작)"
|
||||
: "기준 측점";
|
||||
@@ -323,14 +321,17 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
}
|
||||
|
||||
/** 고른 구조물군의 타입을 종류 목록에 채운다 — A군은 계곡 통과 시설(managed_by)도
|
||||
* 포함한다(추가 시 관 지점 정본으로 간다, 2026-08-17 통합). */
|
||||
* 포함한다(추가 시 관 지점 정본으로 간다, 2026-08-17 통합). 첫 항목은 빈칸 —
|
||||
* 초기·리셋 상태를 "안 고름"으로 드러낸다. */
|
||||
function syncTypeOptions(keepTypeId?: string): void {
|
||||
const group = groupSelect.value;
|
||||
const candidates = types.filter((type) => type.group === group);
|
||||
typeSelect.replaceChildren(...candidates.map((type) => new Option(type.name, type.type_id)));
|
||||
if (keepTypeId && candidates.some((type) => type.type_id === keepTypeId)) {
|
||||
typeSelect.value = keepTypeId;
|
||||
}
|
||||
const candidates = group ? types.filter((type) => type.group === group) : [];
|
||||
typeSelect.replaceChildren(
|
||||
new Option("— 선택 —", ""),
|
||||
...candidates.map((type) => new Option(type.name, type.type_id)),
|
||||
);
|
||||
typeSelect.value =
|
||||
keepTypeId && candidates.some((type) => type.type_id === keepTypeId) ? keepTypeId : "";
|
||||
syncPlacementFields();
|
||||
renderOptionFields();
|
||||
}
|
||||
@@ -583,7 +584,12 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
loadForm(null);
|
||||
emit();
|
||||
});
|
||||
resetButton.addEventListener("click", () => loadForm(null));
|
||||
resetButton.addEventListener("click", () => {
|
||||
// 리셋은 폼만 아니라 구조물군·종류도 빈칸으로 되돌린다(2026-08-17 사용자 지시).
|
||||
groupSelect.value = "";
|
||||
syncTypeOptions();
|
||||
loadForm(null);
|
||||
});
|
||||
|
||||
body.insertBefore(field("메모", memoField), actions);
|
||||
syncButtons();
|
||||
@@ -594,10 +600,12 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu
|
||||
setTypes(next) {
|
||||
types = next;
|
||||
const groups = [...new Set(next.map((t) => t.group))];
|
||||
// 첫 항목은 빈칸 — 진입·리셋 상태에서 군·종류를 강제로 고르게 두지 않는다.
|
||||
groupSelect.replaceChildren(
|
||||
new Option("— 선택 —", ""),
|
||||
...groups.map((group) => new Option(GROUP_LABELS[group] ?? group, group)),
|
||||
);
|
||||
groupSelect.value = groups.includes("A") ? "A" : (groups[0] ?? "");
|
||||
groupSelect.value = "";
|
||||
syncTypeOptions();
|
||||
},
|
||||
setStructures(next) {
|
||||
|
||||
Reference in New Issue
Block a user