From eb387d5f8465ba2a7e1812c6ccbc737bf5babaa5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 29 Sep 2026 14:20:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05=C2=B7B06):=20=E3=80=8C=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=98=B5=EC=85=98=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0=E3=80=8D=EC=97=90=EC=84=9C=20=EB=81=88=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=EC=9D=84=20=EB=B0=B0=EC=88=98=EA=B4=80=20=C2=B7=20BOX?= =?UTF-8?q?=EC=95=94=EA=B1=B0=20=C2=B7=20=EC=84=B8=EC=9B=94=EA=B5=90=20?= =?UTF-8?q?=C2=B7=20=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=ED=95=98?= =?UTF-8?q?=EC=9C=84=20=ED=8F=BC=EC=97=90=EC=84=9C=EB=8F=84=20=EA=B0=80?= =?UTF-8?q?=EB=A6=BC(21-10=20=ED=9D=A0=208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 시설 폼 칸에 옵션 키 표식(data-option-keys) · 끈 키의 칸은 가림 · 칸이 다 가려진 줄은 줄째 - 키가 여럿인 칸(본체 규격 · 유입구 구조)은 키가 다 꺼졌을 때만 가림 · 독립 기슭막이는 유입 · 유출 채널 키로 넓힘 - 숨긴 칸은 칸 값(제안값 채우기 포함)이 아니라 폼이 받은 옛 값을 저장에 그대로 실음 · 이 폼에는 필수 검사가 없음 - 가리기 코드는 새 파일(_Facility_Hidden)로 분리해 Facility.ts 700줄 아래 유지 - 시험 3개 추가 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF --- .../B05_Profile_UI_Drainage_Facility.ts | 47 +++++++++--- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 26 ++++--- .../B05_Profile_UI_Drainage_Facility_Ford.ts | 12 ++-- ...B05_Profile_UI_Drainage_Facility_Hidden.ts | 59 +++++++++++++++ .../B05_Profile_UI_Drainage_Facility_Merge.ts | 22 ++++++ .../B05_Profile_UI_Structures_Panel.ts | 9 ++- .../B05_Profile_UI_Style_Structures.css | 5 ++ .../helper_b05_structures_form_summary.cjs | 71 +++++++++++++++++++ .../test_b05_structures_form_summary.py | 36 ++++++++++ 9 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_Drainage_Facility_Hidden.ts diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index b72f1ca8..2e38ce36 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -60,6 +60,7 @@ import { import { pipeWallMinHeightM } from "../common_util/common_util_culvert_sets"; import type { SummaryItem } from "../common_util/common_util_summary_item"; import { createFordFields } from "./B05_Profile_UI_Drainage_Facility_Ford"; +import { createHiddenOptions } from "./B05_Profile_UI_Drainage_Facility_Hidden"; export type { InletStructureKind }; // 목록은 700줄 한계로 `_Fields` 로 옮김 — 부르던 곳은 그대로 @@ -204,8 +205,11 @@ export interface FacilityOptionsForm { /** 담당 유역의 설계유량(㎥/s) — 물넘이·세월교 개략 단면의 입력. 없으면 안내만. */ designFlow?: number | null, ) => void; - /** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). */ + /** 현재 필드 값을 시설 옵션으로 읽는다(빈 값 생략). 숨긴 칸은 폼이 받은 옛 값 그대로. */ readOptions: () => Record; + /** 「구조물 옵션 보이기」에서 끈 레지스트리 옵션 키 — 그 칸을 가린다(값은 옛 값 보존). + * `setFacility` 뒤에 부른다. */ + setHiddenOptions: (facility: PipeFacility | null, registryKeys: Iterable) => void; /** 설계유량만 갱신한다 — 필드 값은 건드리지 않는다. 임시 배치(2026-08-18)의 * 유역 재계산 결과를 입력 중인 폼에 살짝 반영하는 용도. */ setDesignFlow: (designFlow: number | null) => void; @@ -249,8 +253,11 @@ export function createFacilityOptionsForm( // 관보호공 날개벽 — **개소당 원단위**가 형식마다 붙박이라 고르는 칸이 있어야 물량이 선다 // (2026-09-09, 계획서 4-13 · 소광리 원본 탭 다섯). 비우면 날개벽을 안 센다. const wingWall = optionalSelect("안 놓음", ["A-TYPE", "C-TYPE", "A-TYPE+집수정"]); - const pipeRow = grid(labeled("관종", pipeMaterial), labeled("관경", pipeDiameter)); - const wingRow = grid(labeled("관보호공 날개벽", wingWall)); + const pipeRow = grid( + labeled("관종", pipeMaterial, "pipe_kind"), + labeled("관경", pipeDiameter, "pipe_diameter_mm"), + ); + const wingRow = grid(labeled("관보호공 날개벽", wingWall, "wing_wall_type")); // ── 유입구 — 집수정 또는 기슭막이 택일(사용자 지시 5~9) ───────────────── const inletGroup = group("유입구"); @@ -273,11 +280,14 @@ export function createFacilityOptionsForm( "돌쌓기(찰)", "돌쌓기(메)", ]); - const basinMaterialField = labeled("형태", basinMaterial); + const basinMaterialField = labeled("형태", basinMaterial, "inlet_basin_material"); // 집수정 종방향 길이 — 3D 예상형상용, 전후 동일 배분·기본 2m(2026-08-23 사용자). const basinLength = numberInput("0.1", "0", "제안 2"); const basinRows = [ - grid(labeled("형식", basinForm), labeled("길이 (m)", stepper(basinLength, 1))), + grid( + labeled("형식", basinForm, "inlet_basin_form"), + labeled("길이 (m)", stepper(basinLength, 1), "inlet_basin_length_m"), + ), ]; const height = (): string => String(pipeWallMinHeightM(Number(pipeDiameter.value || "1000") / 1000)); @@ -294,7 +304,11 @@ export function createFacilityOptionsForm( inletSlot.className = "b05-structure__adjust-slot"; const outletSlot = inletSlot.cloneNode() as HTMLDivElement; inletGroup.body.append( - grid(labeled("구조", inletType), basinMaterialField, inletRevet.formField), + grid( + labeled("구조", inletType, ["inlet_type", "inlet_structure"]), + basinMaterialField, + inletRevet.formField, + ), ...basinRows, ...inletRevet.rows, inletSlot, @@ -309,7 +323,7 @@ export function createFacilityOptionsForm( height, }); outletGroup.body.append( - grid(labeled("구조", outletType), outletRevet.formField), + grid(labeled("구조", outletType, "outlet_type"), outletRevet.formField), ...outletRevet.rows, outletSlot, ); @@ -329,7 +343,7 @@ export function createFacilityOptionsForm( // 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만 // 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다. const revetSide = optionalSelect("— 안 정함 (제안 양쪽) —", ["양쪽", "좌", "우"]); - const revetTopRow = grid(labeled("설치 측", revetSide)); + const revetTopRow = grid(labeled("설치 측", revetSide, "side")); const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)"); const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)"); /** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */ @@ -372,12 +386,20 @@ export function createFacilityOptionsForm( ]); const boxWidth = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][1]}`); const boxHeight = numberInput("0.1", "0", `제안 ${BOX_SIZE_PRESETS[0][2]}`); - const boxCustomRow = grid(labeled("본체 폭 (m)", boxWidth), labeled("본체 높이 (m)", boxHeight)); + const boxCustomRow = grid( + labeled("본체 폭 (m)", boxWidth, "body_width_m"), + labeled("본체 높이 (m)", boxHeight, "body_height_m"), + ); const boxWrap = document.createElement("div"); - boxWrap.append(grid(labeled("본체 규격 (m)", boxSize)), boxCustomRow); + boxWrap.append( + grid(labeled("본체 규격 (m)", boxSize, ["body_width_m", "body_height_m"])), + boxCustomRow, + ); const wingInFields = createWingFields("날개벽(유입)", WING_IN_KEYS); const wingOutFields = createWingFields("날개벽(유출)", WING_OUT_KEYS); + const hiddenOptions = createHiddenOptions(root); + /** 고른 프리셋을 폭·높이 칸에 실어 둔다 — 저장도, "사용자 지정"으로 바꿨을 때의 * 출발값도 이 두 칸이 정본이다. 사용자 지정 상태에서는 손대지 않는다. */ function syncBoxSize(): void { @@ -551,8 +573,12 @@ export function createFacilityOptionsForm( onInletStructureChange(handler) { inletStructureHandler = handler; }, + setHiddenOptions(facility, registryKeys) { + hiddenOptions.set(facility, registryKeys); + }, setFacility(facility, options = {}, designFlow = null) { current = facility; + hiddenOptions.remember(options); ford.setDesignFlow(designFlow ?? null); if (facility === null) { syncVisibility(); @@ -658,6 +684,7 @@ export function createFacilityOptionsForm( revetInlet.fields.read(options); revetOutlet.fields.read(options); } + hiddenOptions.restore(options); // 숨긴 칸은 폼이 받은 옛 값 그대로 return options; }, }; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index da745045..a56de443 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -81,9 +81,15 @@ export function fordSection(designFlowM3s: number | null, widthM: number): FordS /* ── 폼 조각 ────────────────────────────────────────────────────────────── */ -export function labeled(text: string, input: HTMLElement): HTMLLabelElement { +export function labeled( + text: string, + input: HTMLElement, + /** 이 칸이 적는 옵션 키 — 「구조물 옵션 보이기」에서 끈 키의 칸을 가리는 표식(21-10 흠 8). */ + optionKeys: string | readonly string[] = [], +): HTMLLabelElement { const wrapper = document.createElement("label"); wrapper.className = "b05-route__field"; + if (optionKeys.length) wrapper.dataset.optionKeys = [optionKeys].flat().join(" "); const caption = document.createElement("span"); caption.textContent = text; wrapper.append(caption, input); @@ -355,11 +361,11 @@ export function createRevetmentFields( }; // 눈금은 조정창과 같은 값이다 — 길이 1m, 전/후 0.5m, 높이 0.1m(2026-08-29 일원화). - const formField = labeled("형태", form); - const lengthField = length ? labeled("길이 (m)", stepper(length, 1)) : null; - const heightField = labeled("높이 (m)", stepper(height, 0.1)); - const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5)) : null; - const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5)) : null; + const formField = labeled("형태", form, keys.form); + const lengthField = length ? labeled("길이 (m)", stepper(length, 1), keys.length) : null; + const heightField = labeled("높이 (m)", stepper(height, 0.1), keys.height); + const beforeField = before ? labeled("기준측점 전 (m)", stepper(before, 0.5), keys.before) : null; + const afterField = after ? labeled("기준측점 후 (m)", stepper(after, 0.5), keys.after) : null; // [제안값 넣기] — **누른 때만** 빈 칸에 제안값을 넣음(적은 칸은 안 건드림) · 층따기 단추와 같은 모양. const fill = document.createElement("button"); @@ -494,15 +500,15 @@ export function createWingFields(title: string, keys: WingKeys): WingFields { const angle = numberInput("1", "0", `제안 ${WING_DEFAULTS.angle}`); // 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음으로 맞춘다(2026-08-30 사용자). - const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1)); + const heightField = labeled("짧은쪽 높이 (m)", stepper(height, 0.1), keys.height); const dimsRow = grid( - labeled("길이 (m)", stepper(length, 0.1)), - labeled("각도 (°)", stepper(angle, 5, 0)), + labeled("길이 (m)", stepper(length, 0.1), keys.length), + labeled("각도 (°)", stepper(angle, 5, 0), keys.angle), ); const box = group(title); const slot = document.createElement("div"); slot.className = "b05-structure__adjust-slot"; - box.body.append(grid(labeled("설치", install), heightField), dimsRow, slot); + box.body.append(grid(labeled("설치", install, keys.install), heightField), dimsRow, slot); function syncVisibility(): void { const off = install.value === "없음"; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts index 641479b5..11e0edb3 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Ford.ts @@ -41,12 +41,12 @@ export interface FordFields { export function createFordFields(emit: () => void): FordFields { // 세월교 — 구체 내 배관 수량(련). 련은 정수라 소수 자릿수를 두지 않는다. const count = numberInput("1", "1", "련"); - const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0))); + const countRow = grid(labeled("수량 (련)", stepper(count, 1, 0), "pipe_count")); const width = numberInput("0.1"); const height = numberInput("0.01"); const widthRow = grid( - labeled("월류 폭 (m)", stepper(width, 0.1)), - labeled("월류 높이 (m)", stepper(height, 0.1)), + labeled("월류 폭 (m)", stepper(width, 0.1), "ford_width_m"), + labeled("월류 높이 (m)", stepper(height, 0.1), "ford_height_m"), ); // 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정). // 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다. @@ -56,9 +56,9 @@ export function createFordFields(emit: () => void): FordFields { const thickness = numberInput("1"); const length = numberInput("0.1"); const slopeRow = grid( - labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1)), - labeled("포장 두께 (㎝)", stepper(thickness, 1, 0)), - labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1)), + labeled("바닥 경사 유입→유출 (%)", stepper(slope, 0.1), "ford_slope_pct"), + labeled("포장 두께 (㎝)", stepper(thickness, 1, 0), "thickness_cm"), + labeled("포장 길이 노폭 방향 (m)", stepper(length, 0.1), "length_m"), ); const summary = document.createElement("p"); summary.className = "b05-drainage__facility-note"; diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Hidden.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Hidden.ts new file mode 100644 index 00000000..9cfc15fa --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Hidden.ts @@ -0,0 +1,59 @@ +/* ============================================================================= + * B05_Profile_UI_Drainage_Facility_Hidden.ts + * 계곡 통과 시설 폼의 **끈 옵션 가리기** — M02 「구조물 옵션 보이기」에서 끈 옵션 + * (`GET /api/projects/structure-types` 의 `visible: false`)의 칸을 시설 폼에서도 안 그린다 + * (PLAN 21-10 흠 8). + * + * · 칸은 `labeled(..., 옵션키)` 가 붙인 `data-option-keys` 표식으로 찾는다 — 키가 여럿인 칸(본체 규격 · + * 유입구 구조)은 **키가 다 꺼졌을 때** 가린다(하나만 꺼지면 남은 키를 적는 칸이라 둠). + * · 숨긴 칸의 값은 칸이 아니라 **폼이 받은 옛 값**(`remember`)을 저장에 그대로 싣는다 — + * 칸에 제안값을 채워도 숨긴 값이 몰래 바뀌지 않는다. + * · 필수 검사는 이 폼에 없다(레지스트리 옵션 칸은 `renderOptionFields` 가 숨긴 칸을 뺌). + * ========================================================================== */ + +import { hiddenFormKeys } from "./B05_Profile_UI_Drainage_Facility_Merge"; + +export interface HiddenOptions { + /** 폼이 받은 옵션 — 숨긴 칸의 옛 값 보존용. `setFacility` 가 부른다. */ + remember: (options: Record) => void; + /** 끈 레지스트리 키를 받아 칸을 가린다. */ + set: (facility: string | null, registryKeys: Iterable) => void; + /** 읽은 옵션에서 숨긴 칸을 칸 값이 아닌 옛 값으로 바꾼다. */ + restore: (options: Record) => void; +} + +export function createHiddenOptions(root: HTMLElement): HiddenOptions { + let hidden: ReadonlySet = new Set(); + let stored: Record = {}; + const apply = (): void => { + root.querySelectorAll("[data-option-keys]").forEach((node) => { + const keys = (node.dataset.optionKeys ?? "").split(" "); + node.classList.toggle( + "is-option-hidden", + keys.every((key) => hidden.has(key)), + ); + }); + // 칸이 다 가려진 줄은 줄째(빈 줄 간격이 안 남게) + root.querySelectorAll(".b05-structure__grid").forEach((row) => { + const kids = [...row.children]; + const all = + kids.length > 0 && kids.every((kid) => kid.classList.contains("is-option-hidden")); + row.classList.toggle("is-option-hidden", all); + }); + }; + return { + remember(options) { + stored = { ...options }; + }, + set(facility, registryKeys) { + hidden = hiddenFormKeys(facility, registryKeys); + apply(); + }, + restore(options) { + for (const key of hidden) { + delete options[key]; + if (stored[key] !== undefined) options[key] = stored[key]; + } + }, + }; +} diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts index 601e06ed..1d55eee9 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Merge.ts @@ -53,6 +53,28 @@ export const FORM_OPTION_KEYS: Readonly> = { revetment: ["side", ...revetKeys("inlet"), ...revetKeys("outlet")], }; +/** 독립 기슭막이 레지스트리 키 → 폼은 유입·유출 두 채널 키로 적는다. */ +const REVETMENT_FIELDS = ["form", "length_m", "height_m", "before_m", "after_m"]; + +/** 「구조물 옵션 보이기」에서 끈 레지스트리 키 → 시설 폼이 가릴 칸 키(21-10 흠 8). + * · 독립 기슭막이는 채널 키(`inlet_revet_*` · `outlet_revet_*`)로 넓힘 + * · 유입구 「구조」한 칸이 `inlet_type` 과 `inlet_structure` 를 함께 적음 */ +export function hiddenFormKeys( + facility: string | null, + registryHidden: Iterable, +): Set { + const hidden = new Set(registryHidden); + if (facility === "revetment") { + for (const field of REVETMENT_FIELDS) { + if (!hidden.has(field)) continue; + hidden.add(`inlet_revet_${field}`); + hidden.add(`outlet_revet_${field}`); + } + } + if (hidden.has("inlet_type")) hidden.add("inlet_structure"); + return hidden; +} + interface MergeableAttributes { facility: string; start_m?: number; diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index a8786505..10be07e9 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -38,6 +38,7 @@ import { loadPipeForm as loadPipeFormInto, } from "./B05_Profile_UI_Structures_Panel_Commit"; import { renderStructureList } from "./B05_Profile_UI_Structures_List"; +import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary"; import { stationLabel as formatStation } from "../common_util/common_util_station"; /** 공개 타입·표시 상수는 700줄 제한으로 `_Structures_Panel_Types` 로 옮겼다 — @@ -228,7 +229,13 @@ export function createStructuresSection( options: Record = {}, designFlow: number | null = null, ): void { - facilityOptions.setFacility(facilityFormKind(currentType()), options, designFlow); + const kind = facilityFormKind(currentType()); + facilityOptions.setFacility(kind, options, designFlow); + // 「구조물 옵션 보이기」에서 끈 옵션은 시설 폼에서도 가림(값은 옛 값 그대로 저장) + facilityOptions.setHiddenOptions( + kind, + (currentType()?.options ?? []).filter((o) => !isOptionVisible(o)).map((o) => o.key), + ); syncOptionLock(); } diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index ef31e1d8..d1a617cc 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -484,3 +484,8 @@ .b05-route__irregular-item.is-assigned span { color: color-mix(in srgb, var(--color-success) 65%, var(--color-text)); } + +/* 「구조물 옵션 보이기」에서 끈 옵션의 시설 폼 칸 */ +.b05-drainage__facility .is-option-hidden { + display: none; +} diff --git a/resources/tester/helper_b05_structures_form_summary.cjs b/resources/tester/helper_b05_structures_form_summary.cjs index 0d3fa761..76e35638 100644 --- a/resources/tester/helper_b05_structures_form_summary.cjs +++ b/resources/tester/helper_b05_structures_form_summary.cjs @@ -66,6 +66,20 @@ class FakeNode { (this.listeners.click || []).forEach((h) => h({ target: this })); } scrollIntoView() {} + // 시험에 필요한 두 가지 선택자만 — `[data-option-keys]` · `.클래스` + querySelectorAll(selector) { + const match = selector.startsWith("[") + ? (n) => n.dataset && n.dataset.optionKeys !== undefined + : (n) => n.classList.contains(selector.slice(1)); + const found = []; + const walk = (node) => + node.children.forEach((kid) => { + if (kid.classList && match(kid)) found.push(kid); + walk(kid); + }); + walk(this); + return found; + } } globalThis.document = { createElement: (tag) => new FakeNode(tag) }; globalThis.Option = function Option(text, value) { @@ -101,6 +115,9 @@ function loadTs(file) { if (STUBS[request]) return STUBS[request]; // 화면 공용 부품(@ui/…) — 이 시험이 안 쓰는 것이라 뭘 불러도 아무 일 안 하는 함수로 막아 둠 if (request.startsWith("@ui/")) return new Proxy({}, { get: () => () => {} }); + if (request.startsWith(".") && request.endsWith(".json")) { + return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8")); + } return request.startsWith(".") ? loadTs(path.join(path.dirname(full), `${request}.ts`)) : require(request); @@ -118,6 +135,8 @@ const { buildSummaryRow } = B05("B05_Profile_UI_Structures_Summary_Row"); const { renderStructureList } = B05("B05_Profile_UI_Structures_List"); const { renderOptionFields } = B05("B05_Profile_UI_Structures_Panel_Options"); const commitModule = B05("B05_Profile_UI_Structures_Panel_Commit"); +const { createHiddenOptions } = B05("B05_Profile_UI_Drainage_Facility_Hidden"); +const { labeled } = B05("B05_Profile_UI_Drainage_Facility_Fields"); const out = {}; const A = { @@ -358,6 +377,58 @@ const prev = () => ({ summary_item: A, }); +// 시설 폼 끈 옵션 가리기(PLAN 21-10 흠 8) +{ + const tagged = labeled("관종", new FakeNode("select"), "pipe_kind"); + const multi = labeled("구조", new FakeNode("select"), ["inlet_type", "inlet_structure"]); + const plain = labeled("메모", new FakeNode("select")); + out.labelTag = [tagged.dataset.optionKeys, multi.dataset.optionKeys, plain.dataset.optionKeys]; + const root = new FakeNode("div"); + const rowA = new FakeNode("div"); + rowA.className = "b05-structure__grid"; + const kind = labeled("관종", new FakeNode("select"), "pipe_kind"); + const dia = labeled("관경", new FakeNode("select"), "pipe_diameter_mm"); + rowA.append(kind, dia); + const rowB = new FakeNode("div"); + rowB.className = "b05-structure__grid"; + const inlet = labeled("구조", new FakeNode("select"), ["inlet_type", "inlet_structure"]); + const free = labeled("메모", new FakeNode("select")); // 표식 없는 칸은 안 가림 + rowB.append(inlet, free); + root.append(rowA, rowB); + const hiddenOf = () => ({ + kind: kind.classList.contains("is-option-hidden"), + dia: dia.classList.contains("is-option-hidden"), + rowA: rowA.classList.contains("is-option-hidden"), + inlet: inlet.classList.contains("is-option-hidden"), + free: free.classList.contains("is-option-hidden"), + rowB: rowB.classList.contains("is-option-hidden"), + }); + const hidden = createHiddenOptions(root); + hidden.set("pipe", ["pipe_kind"]); + out.hideOne = hiddenOf(); + hidden.set("pipe", ["pipe_kind", "pipe_diameter_mm"]); + out.hideRow = hiddenOf(); // 줄의 칸이 다 가려지면 줄째 + hidden.set("pipe", ["inlet_structure"]); + out.hideInletPartial = hiddenOf(); // 키가 여럿인 칸은 하나만 꺼지면 그대로 + hidden.set("pipe", ["inlet_type"]); + out.hideInlet = hiddenOf(); // 유입구 「구조」는 inlet_type 을 끄면 함께 가려짐(구조 키 포함) + hidden.set("pipe", []); + out.showAll = hiddenOf(); // [모두 보임] 뒤 되돌아옴 + // 숨긴 칸은 폼이 받은 옛 값 그대로 — 칸에서 읽은 값(제안값 채우기 포함)이 아니라 + hidden.remember({ pipe_kind: "흄관", pipe_diameter_mm: 1000, other: 1 }); + hidden.set("pipe", ["pipe_kind", "inlet_type"]); + const read = { pipe_kind: "VR관", pipe_diameter_mm: 800, inlet_type: "제안", other: 2 }; + hidden.restore(read); + out.restored = read; + hidden.remember({}); // 옛 값이 없던 숨긴 칸은 값이 안 실림 + const read2 = { pipe_kind: "VR관", pipe_diameter_mm: 800 }; + hidden.restore(read2); + out.restoredEmpty = read2; + out.revetKeys = [...merge.hiddenFormKeys("revetment", ["form", "height_m", "side"])].sort(); + out.pipeKeys = [...merge.hiddenFormKeys("pipe", ["inlet_type"])].sort(); + out.otherKeys = [...merge.hiddenFormKeys("box_culvert", ["form"])].sort(); +} + async function main() { const fresh = await saveWith({ previous: null, summary: null, formOptions: { H: 3 } }); out.commitNew = { hasKey: "summary_item" in fresh, options: fresh.options }; diff --git a/resources/tester/test_b05_structures_form_summary.py b/resources/tester/test_b05_structures_form_summary.py index 2b7f8d03..457227f5 100644 --- a/resources/tester/test_b05_structures_form_summary.py +++ b/resources/tester/test_b05_structures_form_summary.py @@ -136,3 +136,39 @@ def test_배수유역_화면은_들어올_때_세션_초안_관을_먼저_씀(): body = source.split("const loadSaved", 1)[1].split("/**", 1)[0] assert "readPendingPipes(" in body and "computeDetailBasins(" in body assert body.index("readPendingPipes(") < body.index("fetchDetailPipePoints(") + + +def test_시설_폼_끈_옵션_가리기(out): + # 칸 표식 — 키 하나 · 여럿 · 없음 + assert out["labelTag"] == ["pipe_kind", "inlet_type inlet_structure", None] + none = dict(kind=False, dia=False, rowA=False, inlet=False, free=False, rowB=False) + assert out["hideOne"] == {**none, "kind": True} + # 줄의 칸이 다 가려지면 줄째 · 표식 없는 칸은 그대로 + assert out["hideRow"] == {**none, "kind": True, "dia": True, "rowA": True} + assert out["hideInletPartial"] == none + assert out["hideInlet"] == {**none, "inlet": True} + assert out["showAll"] == none # [모두 보임] 뒤 되돌아옴 + + +def test_숨긴_칸은_옛_값_그대로_저장에_실림(out): + # 칸에서 읽은 값(VR관 · 제안)이 아니라 폼이 받은 옛 값 — 안 숨긴 칸은 칸 값 + assert out["restored"] == {"pipe_kind": "흄관", "pipe_diameter_mm": 800, "other": 2} + # 옛 값이 없던 숨긴 칸은 값이 안 실림(칸의 제안값이 몰래 확정되지 않음) + assert out["restoredEmpty"] == {"pipe_diameter_mm": 800} + + +def test_끈_키를_폼_칸_키로_넓힘(out): + # 독립 기슭막이는 유입 · 유출 채널 키로 · 유입구 구조는 inlet_structure 까지 + assert out["revetKeys"] == sorted( + [ + "form", + "height_m", + "side", + "inlet_revet_form", + "outlet_revet_form", + "inlet_revet_height_m", + "outlet_revet_height_m", + ] + ) + assert out["pipeKeys"] == ["inlet_structure", "inlet_type"] + assert out["otherKeys"] == ["form"]