From 5331167439bae6ea7d41b9bafe311766dff47b87 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 3 Sep 2026 15:47:16 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B05):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=ED=8F=BC=20=EB=BC=88=EB=8C=80=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=A1=B0=EB=A6=BD=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20(700=EC=A4=84=20=EC=A0=9C=ED=95=9C=201?= =?UTF-8?q?=EC=B0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_UI_Structures_Panel` 1,078줄 → 1,024줄. 화면 요소를 세우는 몫만 `_UI_Structures_Form` 으로 옮기고 패널은 값·검증·저장·목록을 맡는다. 배치 규칙(행 순서·구분선·버튼 묶음)은 사용자 지시로 굳은 것이라 그대로 옮겼다. 아직 제한을 넘는다 — 남은 큰 덩어리는 `commit()` 약 200줄과 옵션 필드 렌더·시설 폼 로드 약 170줄이다. 둘 다 폼 상태를 깊게 만지므로 [추가]·[삭제] 실동작 검증을 붙여 따로 옮긴다. 검증 — 공용 브라우저 B05 「구조물 배치」 실측: 제목·측점 행·메모 칸·목록 1개, 버튼 [추가]·[삭제]·[리셋], 구조물군 7항목 그대로. pytest 383 passed·17 skipped, typecheck·prettier 통과. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Structures_Form.ts | 155 ++++++++++++++++++ .../B05_Profile_UI_Structures_Panel.ts | 110 ++++--------- 2 files changed, 183 insertions(+), 82 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_Structures_Form.ts diff --git a/B05_Profile/B05_Profile_UI_Structures_Form.ts b/B05_Profile/B05_Profile_UI_Structures_Form.ts new file mode 100644 index 00000000..d7410144 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Structures_Form.ts @@ -0,0 +1,155 @@ +/* ============================================================================= + * B05_Profile_UI_Structures_Form.ts + * 「구조물 배치」 폼의 **뼈대**만 세우는 조립기 (2026-09-03 · 700줄 제한). + * + * 패널 본체(`_UI_Structures_Panel`)는 값 읽기·검증·저장·목록 갱신을 맡고, 여기서는 + * 화면 요소를 만들어 넘기기만 한다. 배치 규칙(행 순서·구분선·버튼 묶음)은 사용자 지시로 + * 굳은 것이라 그대로 옮겼다. + * + * 화면 구성(2026-08-17~29 사용자 지시): + * [구조물군][종류] ← 2열 격자 + * [측점 범위] [시작 측점] [기준 측점] [종료 측점] ← 타입에 따라 보이는 행이 갈린다 + * ──────── 구분선 + * [옵션 격자] / [계곡 통과 시설 서브폼] + * [추가·삭제·리셋] + * 목록(`list`)은 본문에 붙이지 않는다 — 폼이 길어지면 스크롤 밖으로 밀려 안 보였다. + * ========================================================================== */ + +import { + field, + select, + type StationFields, + stationFields, +} from "./B05_Profile_UI_Structures_Fields"; +import { createFacilityOptionsForm } from "./B05_Profile_UI_Drainage_Facility"; + +export interface StructuresFormElements { + root: HTMLElement; + body: HTMLElement; + groupSelect: HTMLSelectElement; + typeSelect: HTMLSelectElement; + startFields: StationFields; + anchorFields: StationFields; + endFields: StationFields; + memoField: HTMLInputElement; + /** 계산으로 채우는 측점 범위 표시(직접 입력이 아니다). */ + rangeValue: HTMLElement; + rangeWrap: HTMLElement; + positionRow: HTMLElement; + /** 측점 그룹과 옵션 사이 구분선 — 타입이 없으면 함께 숨긴다. */ + positionDivider: HTMLElement; + optionRow: HTMLElement; + /** 버튼 묶음 — 패널이 그 앞에 메모 칸을 끼워 넣는다. */ + actions: HTMLElement; + facilityOptions: ReturnType; + primary: HTMLButtonElement; + removeButton: HTMLButtonElement; + resetButton: HTMLButtonElement; + /** 하단 고정 영역에 패널이 따로 붙이는 구조물 목록. */ + list: HTMLElement; +} + +/** 폼 뼈대를 세운다. 값 채우기·이벤트 배선은 호출한 쪽(패널)이 한다. */ +export function buildStructuresForm(options: { + /** 측점 잔여거리 정규화에 쓰는 현재 측점 간격(m). */ + getInterval: () => number; + /** 계곡 통과 시설 서브폼 값이 바뀔 때 — 패널이 즉시 반영을 건다. */ + onFacilityChange: () => void; +}): StructuresFormElements { + const root = document.createElement("section"); + // b05-structure-section: 이 섹션 안의 입력·버튼 높이를 한 값(--b05-control-h)으로 + // 묶는 스코프(2026-08-29 사용자 지시 — 조작 컨트롤 높이 통일). + root.className = + "b05-route__panel-section b05-structure-section ui-collapsible ui-sidebar-section"; + const heading = document.createElement("h3"); + heading.className = "ui-collapsible__title"; + heading.textContent = "구조물 배치"; + const body = document.createElement("div"); + body.className = "b05-route__panel-body"; + root.append(heading, body); + + const groupSelect = select([]); + const typeSelect = select([]); + // 위치 입력 순서 = 시작 측점 → 기준 측점 → 종료 측점 (2026-08-17 사용자 확정). + // 점형은 기준 측점만 보인다. 잔여거리는 [0, 간격) — 넘치면 측점번호로 올려 표시한다. + const startFields = stationFields("시작 측점", options.getInterval); + const anchorFields = stationFields("기준 측점", options.getInterval); + const endFields = stationFields("종료 측점", options.getInterval); + const memoField = document.createElement("input"); + 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(rangeWrap, startFields.wrap, anchorFields.wrap, endFields.wrap); + + // 기본 인터페이스는 2열 격자(2026-08-17 사용자 지시 1). + const typeRow = document.createElement("div"); + typeRow.className = "b05-structure__grid"; + typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect)); + + // 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다. + const optionRow = document.createElement("div"); + optionRow.className = "b05-structure__grid"; + + // 계곡 통과 시설(배관 등)의 부속 옵션 서브폼 — 일반 구조물의 옵션 칸과 같은 자리에서 + // 같은 흐름(종류 선택 → 옵션 → 추가/수정)으로 편집한다(2026-08-17 사용자 지시 — + // 자동·수동 배관은 같은 구조물, 별도 UI 금지). + const facilityOptions = createFacilityOptionsForm({ onChange: options.onFacilityChange }); + + const primary = document.createElement("button"); + primary.type = "button"; + primary.className = "b05-route__irregular-btn is-primary"; + const removeButton = document.createElement("button"); + removeButton.type = "button"; + removeButton.className = "b05-route__irregular-btn is-danger"; + removeButton.textContent = "삭제"; + const resetButton = document.createElement("button"); + resetButton.type = "button"; + resetButton.className = "b05-route__irregular-btn"; + resetButton.textContent = "리셋"; + const actions = document.createElement("div"); + actions.className = "b05-route__irregular-actions"; + actions.append(primary, removeButton, resetButton); + + // 목록은 이 섹션 본문에 붙이지 않는다 — 폼이 길어지면 스크롤 밖으로 밀려 안 보였다. + // 패널(B05_Profile_UI_Panel)이 하단 고정 영역에 배치한다(2026-08-18 사용자 지시). + const list = document.createElement("ul"); + list.className = "b05-route__irregular-list"; + + // 측점 그룹과 옵션 나열 사이 구분선(2026-08-17 사용자 지시 2). + const positionDivider = document.createElement("hr"); + positionDivider.className = "b05-structure__divider"; + + body.append(typeRow, positionRow, positionDivider, optionRow, facilityOptions.root, actions); + + return { + root, + body, + groupSelect, + typeSelect, + startFields, + anchorFields, + endFields, + memoField, + rangeValue, + rangeWrap, + positionRow, + positionDivider, + optionRow, + actions, + facilityOptions, + primary, + removeButton, + resetButton, + list, + }; +} diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 49a37b3f..892cde77 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -23,11 +23,11 @@ import { } from "./B05_Profile_Api_Structures"; import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { - createFacilityOptionsForm, type FacilityAttributes, type FacilityOptionsForm, } from "./B05_Profile_UI_Drainage_Facility"; -import { field, numberInput, select, stationFields } from "./B05_Profile_UI_Structures_Fields"; +import { field, numberInput, select } from "./B05_Profile_UI_Structures_Fields"; +import { buildStructuresForm } from "./B05_Profile_UI_Structures_Form"; import { renderStructureList } from "./B05_Profile_UI_Structures_List"; import { splitPavementRange } from "./B05_Profile_UI_Structures_Pavement"; import { formatStation } from "./B05_Profile_Util_Station"; @@ -115,58 +115,32 @@ interface StructuresCallbacks { } export function createStructuresSection(callbacks: StructuresCallbacks): StructuresSection { - const root = document.createElement("section"); - // b05-structure-section: 이 섹션 안의 입력·버튼 높이를 한 값(--b05-control-h)으로 - // 묶는 스코프(2026-08-29 사용자 지시 — 조작 컨트롤 높이 통일). - root.className = - "b05-route__panel-section b05-structure-section ui-collapsible ui-sidebar-section"; - const heading = document.createElement("h3"); - heading.className = "ui-collapsible__title"; - heading.textContent = "구조물 배치"; - const body = document.createElement("div"); - body.className = "b05-route__panel-body"; - root.append(heading, body); - - const groupSelect = select([]); - const typeSelect = select([]); - // 위치 입력 순서 = 시작 측점 → 기준 측점 → 종료 측점 (2026-08-17 사용자 확정). - // 점형은 기준 측점만 보인다. - // 측점 표기 전역 규칙(2026-08-18): 잔여거리는 [0, 간격) — 넘치면 측점번호로 - // 올려 표시한다(1+30 → 2+10). 정규화는 stationFields가 입력 확정 때 처리. - const startFields = stationFields("시작 측점", () => callbacks.getInterval()); - const anchorFields = stationFields("기준 측점", () => callbacks.getInterval()); - const endFields = stationFields("종료 측점", () => callbacks.getInterval()); - const memoField = document.createElement("input"); - 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(rangeWrap, startFields.wrap, anchorFields.wrap, endFields.wrap); - - // 기본 인터페이스는 2열 격자(2026-08-17 사용자 지시 1). - const typeRow = document.createElement("div"); - typeRow.className = "b05-structure__grid"; - typeRow.append(field("구조물군", groupSelect), field("종류", typeSelect)); - - // 옵션 칸은 타입마다 다르므로 선택할 때마다 새로 그린다. - const optionRow = document.createElement("div"); - optionRow.className = "b05-structure__grid"; - - // 계곡 통과 시설(배관 등)의 부속 옵션 서브폼 — 일반 구조물의 옵션 칸과 같은 - // 자리에서, 같은 흐름(종류 선택 → 옵션 → 추가/수정)으로 편집한다(2026-08-17 - // 사용자 지시 — 자동·수동 배관은 같은 구조물, 별도 UI 금지). 기슭막이 길이를 - // 넣으면 시작·종료 측점이 기준 − 앞 ~ 기준 + 뒤로 자동 채워진다. - const facilityOptions = createFacilityOptionsForm({ onChange: () => liveCommit() }); + // 폼 뼈대는 전용 조립기가 세운다(2026-09-03 · 700줄 제한) — 여기서는 값·검증·저장만. + const form = buildStructuresForm({ + getInterval: () => callbacks.getInterval(), + onFacilityChange: () => liveCommit(), + }); + const { + root, + body, + groupSelect, + typeSelect, + startFields, + anchorFields, + endFields, + memoField, + rangeValue, + rangeWrap, + positionRow, + positionDivider, + optionRow, + actions, + facilityOptions, + primary, + removeButton, + resetButton, + list, + } = form; /** 이 타입이 서브폼(계곡 통과 시설 부속)을 쓰는지 — 쓰면 레지스트리 옵션 칸 대신 * 서브폼이 그린다. 독립 기슭막이(D4)는 서브폼을 떠나 C군과 같은 레지스트리 옵션 @@ -248,34 +222,6 @@ export function createStructuresSection(callbacks: StructuresCallbacks): Structu : "—"; } - const primary = document.createElement("button"); - primary.type = "button"; - primary.className = "b05-route__irregular-btn is-primary"; - const removeButton = document.createElement("button"); - removeButton.type = "button"; - removeButton.className = "b05-route__irregular-btn is-danger"; - removeButton.textContent = "삭제"; - const resetButton = document.createElement("button"); - resetButton.type = "button"; - resetButton.className = "b05-route__irregular-btn"; - resetButton.textContent = "리셋"; - const actions = document.createElement("div"); - actions.className = "b05-route__irregular-actions"; - actions.append(primary, removeButton, resetButton); - - // 목록은 이 섹션 본문에 붙이지 않는다 — 폼이 길어지면 스크롤 밖으로 밀려 안 보였다. - // 패널(B05_Profile_UI_Panel)이 하단 고정 영역에 배치한다(2026-08-18 사용자 지시). - const list = document.createElement("ul"); - list.className = "b05-route__irregular-list"; - // 사용법 설명 문단은 두지 않는다 — 공간 대비 의미 없음(2026-08-17 사용자 지시, - // 필요 시 별도 설명 페이지로). - - // 측점 그룹과 옵션 나열 사이 구분선(2026-08-17 사용자 지시 2). - const positionDivider = document.createElement("hr"); - positionDivider.className = "b05-structure__divider"; - - body.append(typeRow, positionRow, positionDivider, optionRow, facilityOptions.root, actions); - let types: StructureType[] = []; let structures: StructureInstance[] = []; let pipeFacilities: PipeFacilityItem[] = [];