diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index 654a1ff6..c805c061 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -512,15 +512,6 @@ }, "drawing_views": ["profile", "cross_section", "detail", "quantity"], "options": [ - { - "key": "form", - "label": "형식", - "input": "select", - "choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"], - "default": null, - "required": true, - "phase": "detail" - }, { "key": "height_m", "label": "높이", @@ -528,6 +519,24 @@ "unit": "m", "default": null, "required": true, + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": ["중력식", "반중력식", "캔틸레버식", "부벽식"], + "default": null, + "required": true, "phase": "detail" } ] @@ -550,7 +559,16 @@ "unit": "m", "default": null, "required": true, - "phase": "detail" + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "b05" }, { "key": "back_len_cm", @@ -581,7 +599,16 @@ "unit": "m", "default": null, "required": true, - "phase": "detail" + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "b05" }, { "key": "back_len_cm", @@ -605,15 +632,6 @@ }, "drawing_views": ["profile", "cross_section", "quantity"], "options": [ - { - "key": "form", - "label": "형식", - "input": "select", - "choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"], - "default": null, - "required": true, - "phase": "detail" - }, { "key": "height_m", "label": "높이", @@ -621,6 +639,24 @@ "unit": "m", "default": null, "required": true, + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "b05" + }, + { + "key": "form", + "label": "형식", + "input": "select", + "choices": ["콘크리트", "돌(찰)", "돌(메)", "블록쌓기", "돌망태", "흙포대", "통나무쌓기"], + "default": null, + "required": true, "phase": "detail" } ] @@ -643,7 +679,16 @@ "unit": "m", "default": null, "required": true, - "phase": "detail" + "phase": "b05" + }, + { + "key": "length_m", + "label": "길이", + "input": "number", + "unit": "m", + "default": null, + "required": true, + "phase": "b05" }, { "key": "stone_cm", diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index ea2b8d96..dfa362d1 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -28,6 +28,7 @@ import { } from "./B05_Profile_UI_Drainage_Facility"; import { field, numberInput, select, stationFields } from "./B05_Profile_UI_Structures_Fields"; import { renderStructureList } from "./B05_Profile_UI_Structures_List"; +import { formatStation } from "./B05_Profile_Util_Station"; /** 구조물군 표시 이름. 리스트 기호(A~G)만으로는 무엇인지 알기 어렵다. * 우클릭 메뉴(종단그래프·배수유역도)도 같은 이름을 쓴다(2026-08-18 일원화). */ @@ -128,10 +129,18 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu memoField.type = "text"; memoField.placeholder = "메모(선택)"; + // 측점 범위(계산 표시) — C군 사면안정처럼 길이 옵션으로 구간이 정해지는 타입은 + // 시작·종료를 직접 입력하지 않고 기준 측점 + 길이로 계산해 보여만 준다 + // (2026-08-19 사용자 지시). 표기는 하단 구조물 목록과 같은 시작 ~ 종료 측점. + const rangeValue = document.createElement("span"); + rangeValue.className = "b05-structure__range-value"; + const rangeWrap = field("측점 범위", rangeValue); + // 측점은 3행 — 한 행이 [라벨][측점번호][잔여거리](2026-08-17 사용자 지시 2). + // 범위 계산 타입은 [측점 범위][기준 측점]만 보인다(범위 행이 기준 측점 위). const positionRow = document.createElement("div"); positionRow.className = "b05-structure__position-row"; - positionRow.append(startFields.wrap, anchorFields.wrap, endFields.wrap); + positionRow.append(rangeWrap, startFields.wrap, anchorFields.wrap, endFields.wrap); // 기본 인터페이스는 2열 격자(2026-08-17 사용자 지시 1). const typeRow = document.createElement("div"); @@ -157,6 +166,40 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu return type.type_id === "revetment" ? "revetment" : null; } + /** 구간을 직접 입력하지 않는 타입인가 — C군 사면안정 5종처럼 B05 길이 옵션이 + * 있으면 시작·종료 입력 칸을 없애고 기준 측점 + 길이로 범위를 계산해 보여만 + * 준다(2026-08-19 사용자 지시). */ + function hasComputedRange(type: StructureType | null): boolean { + return ( + !!type && + !type.managed_by && + type.placement === "interval" && + type.options.some((option) => option.key === "length_m" && isB05Option(option)) + ); + } + + /** 길이 옵션(length_m) 값(m). 비었거나 0 이하면 null — 범위를 계산할 수 없다. */ + function readLengthM(): number | null { + const entry = optionInputs.find((input) => input.key === "length_m"); + if (!entry || entry.isEmpty()) return null; + const length = Number(entry.read()); + return Number.isFinite(length) && length > 0 ? length : null; + } + + /** 측점 범위 표시 갱신 — 기준 측점 + 길이가 다 있으면 "시작 ~ 종료", 아니면 "—". */ + function syncRangeDisplay(): void { + const computed = hasComputedRange(currentType()); + rangeWrap.hidden = !computed; + if (!computed) return; + const step = interval(); + const anchor = anchorFields.read(step, false); + const length = readLengthM(); + rangeValue.textContent = + anchor !== null && length !== null + ? `${formatStation(anchor, step)} ~ ${formatStation(anchor + length, step)}` + : "—"; + } + const primary = document.createElement("button"); primary.type = "button"; primary.className = "b05-route__irregular-btn is-primary"; @@ -223,11 +266,13 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu /** 배치형태·관리 주체에 맞춰 위치 칸을 바꾼다. 계곡 통과 시설(배수관 등)은 점형이라 * 기준 측점 하나만 받는다(2026-08-17 사용자 지시 2 — 시작·종료 불필요). + * 범위 계산 타입(C군 등)도 기준 측점만 받고 범위는 표시 행이 대신한다(2026-08-19). * 종류 미선택(빈값)이면 입력 칸 전체를 숨긴다 — 리셋 직후 상태. */ function syncPlacementFields(): void { const type = currentType(); const managed = !!type?.managed_by; - const isInterval = !!type && !managed && type.placement === "interval"; + const isInterval = + !!type && !managed && type.placement === "interval" && !hasComputedRange(type); positionRow.hidden = !type; positionDivider.hidden = !type; startFields.wrap.hidden = !isInterval; @@ -236,6 +281,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu anchorFields.wrap.querySelector("span")!.textContent = isInterval ? "기준 측점 (비우면 시작)" : "기준 측점"; + syncRangeDisplay(); } /** 종류에 맞춰 부속 옵션 서브폼을 켠다(계곡 통과 시설·독립 기슭막이, 값은 인자로). */ @@ -247,12 +293,13 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu syncOptionLock(); } - /** 위치(측점)가 채워졌는지 — 상세 옵션을 여는 조건(2026-08-18 입력 순서 가이드). */ + /** 위치(측점)가 채워졌는지 — 상세 옵션을 여는 조건(2026-08-18 입력 순서 가이드). + * 범위 계산 타입은 기준 측점 하나가 곧 위치다(2026-08-19). */ function hasPosition(): boolean { const type = currentType(); if (!type) return false; const step = interval(); - if (!type.managed_by && type.placement === "interval") { + if (!type.managed_by && type.placement === "interval" && !hasComputedRange(type)) { return startFields.read(step, false) !== null && endFields.read(step, false) !== null; } return anchorFields.read(step, false) !== null; @@ -298,6 +345,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu function handleStationInput(): void { if (!hasPosition()) positionConfirmed = false; syncOptionLock(); + syncRangeDisplay(); } /** 위치 입력 확정 — 측점번호+잔여거리를 다 받고 입력군을 빠져나온 순간. @@ -306,6 +354,7 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu positionConfirmed = hasPosition(); commitTempPipe(); syncOptionLock(); + syncRangeDisplay(); } /** 타입의 옵션 스키마대로 입력 칸을 다시 그린다 — 상세(detail) 옵션은 B06/B07 @@ -349,8 +398,14 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu read: () => (option.input === "number" ? Number(input.value) || 0 : input.value), isEmpty: () => input.value.trim() === "", }); + // 길이가 바뀌면 측점 범위 표시가 즉시 따라온다(범위 계산 타입, 2026-08-19). + if (option.key === "length_m") { + input.addEventListener("input", syncRangeDisplay); + input.addEventListener("change", syncRangeDisplay); + } }); syncOptionLock(); + syncRangeDisplay(); } /** 고른 구조물군의 타입을 종류 목록에 채운다 — A군은 계곡 통과 시설(managed_by)도 @@ -450,6 +505,14 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu endFields.write(target.end_m ?? null, step); memoField.value = target.memo ?? ""; renderOptionFields(target.options); + // 구간 직접 입력 시절에 저장된 항목 호환 — 길이 옵션이 비어 있으면 저장된 + // 시작~종료 구간 길이로 채워 범위 표시·재저장이 이어지게 한다(2026-08-19). + if (target.start_m != null && target.end_m != null && target.end_m > target.start_m) { + const lengthEntry = optionInputs.find((entry) => entry.key === "length_m"); + if (lengthEntry && lengthEntry.isEmpty()) { + lengthEntry.input.value = (target.end_m - target.start_m).toFixed(1); + } + } syncFacilityForm(target.options); } else { anchorFields.write(null, step); @@ -560,7 +623,21 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu let anchor: number | null; let start: number | null = null; let end: number | null = null; - if (placement === "interval") { + if (placement === "interval" && hasComputedRange(type)) { + // 범위 계산 타입(C군 등) — 시작 = 기준 측점, 종료 = 기준 + 길이(2026-08-19). + anchor = anchorFields.read(step, true); + if (anchor === null) { + anchorFields.station.focus(); + return; + } + const length = readLengthM(); + if (length === null) { + optionInputs.find((entry) => entry.key === "length_m")?.input.focus(); + return; + } + start = anchor; + end = anchor + length; + } else if (placement === "interval") { start = startFields.read(step, true); end = endFields.read(step, true); if (start === null || end === null || end <= start) { @@ -792,7 +869,8 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu positionConfirmed = true; // 우클릭은 측점이 확정돼 들어온다 — 옵션도 바로 연다. const placement = placementOf(typeId); const step = interval(); - const isInterval = !type.managed_by && placement === "interval"; + // 범위 계산 타입은 시작·종료를 쓰지 않는다 — 길이 옵션이 정한다(2026-08-19). + const isInterval = !type.managed_by && placement === "interval" && !hasComputedRange(type); anchorFields.write(chainageM, step); startFields.write(isInterval ? chainageM : null, step); endFields.write(isInterval ? chainageM + DEFAULT_INTERVAL_LENGTH_M : null, step); diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index e85be5c8..9dc7e7fc 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -222,6 +222,14 @@ background: rgba(224, 91, 91, 0.08); } +/* 측점 범위(계산 표시) — C군 등 길이 옵션 기반 구간 타입. 입력 칸이 아니라 기준 + * 측점 + 길이로 계산된 "시작 ~ 종료"를 보여만 준다(2026-08-19 사용자 지시). */ +.b05-structure__position-row > .b05-route__field > .b05-structure__range-value { + flex: 1; + min-width: 0; + font-weight: 600; +} + /* 측점번호 + 잔여거리 두 칸 입력(2026-08-17 통합 — 구 비정규 측점 UI와 같은 방식). */ .b05-structure__station-pair { display: flex;