From d5c5fe7b40e14c816f06bc942d1f96f128d4e6af Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 09:34:31 +0900 Subject: [PATCH] =?UTF-8?q?fix(b06):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=EC=9D=B4=20=EC=8A=A4=ED=8E=99=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=EC=9D=84=20=EA=B4=80=20=EC=98=B5=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EC=B1=84=EC=9A=B0=EC=A7=80=20=EC=95=8A=EC=9D=8C=20?= =?UTF-8?q?=E2=80=94=20=EC=A1=B0=EC=9E=91=C2=B7=EC=A0=80=EC=9E=A5=EB=B6=84?= =?UTF-8?q?=EB=A7=8C(=ED=8F=BC=20=E2=91=A3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 페이지를 열기만 해도 관종·관경·구조·벽 길이·전/후·집수정 길이 기본값이 옵션에 박혀 임시 저장·[저장]에 확정으로 굳던 자리 - 벽 형태·높이는 조정창에서 조작했을 때만 싣음(앞 조각 c4eccee5 auto 커밋에 먼저 쓸려 올라감) - 세월교·BOX: 저장돼 있던 칸이거나 조정창 조작으로 등록부 기본값과 달라진 칸만 스펙 값을 싣음 - ORCA 936be972 B06: 관 폼 관종 「안 정함」 · 저장된 관경 1000 만 · 벽 칸 빔+제안 · 임시 저장에 기본값 안 생김 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- .../B06_Section_UI_Page_Structures_Panel.ts | 183 +++++++++++------- 1 file changed, 117 insertions(+), 66 deletions(-) diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 4f53a3de..af64050f 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -31,6 +31,7 @@ import { writeStructurePick, } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; import { showToast } from "@ui/ui_template_elements"; +import { FORD_BRIDGE_DEFAULT_WIDTH_M } from "@config/config_frontend"; import { adjustDockRoot, refreshAdjustSlots, @@ -125,105 +126,153 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M; } +type Merged = Record; + +/** 그 종류의 등록부 기본값 `{키: 기본값}` — 스펙 값이 기본값에서 왔는지 가르는 데 씀. */ +function registryDefaults(types: StructureType[], typeId: string): Record { + const options = types.find((type) => type.type_id === typeId)?.options ?? []; + return Object.fromEntries(options.map((option) => [option.key, option.default])); +} + /** - * 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이 - * 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다 - * (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다). + * 스펙 값 한 칸을 옵션에 싣는 규칙 — **저장돼 있던 칸이거나, 조작해 기본값과 달라진 칸만.** + * 기본값에서 온 값을 실으면 페이지를 열기만 해도 임시 저장·[저장]에 기본값이 확정으로 굳음 + * (2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 바꾸지 않는다」). 폼은 빈 칸에 제안을 보임. + */ +function keepSpec( + merged: Merged, + stored: Merged | undefined, + key: string, + spec: string | number | null | undefined, + fromDefault: unknown, +): void { + if (spec === null || spec === undefined) return; + if (stored?.[key] !== undefined || String(spec) !== String(fromDefault ?? "")) merged[key] = spec; +} + +type WingSpec = { + installed: boolean; + height_m: number | null; + length_m: number | null; + angle_deg: number | null; +}; + +function keepWings( + merged: Merged, + stored: Merged | undefined, + defaults: Record, + wings: ReadonlyArray, +): void { + for (const [wing, prefix] of wings) { + // 백엔드 `_wing_spec` 과 같은 채움 — 설치는 「없음」이 아니면 있음 · 치수는 기본 0/0/45. + const install = String(defaults[prefix] ?? "") === "없음" ? "없음" : "있음"; + keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install); + keepSpec( + merged, + stored, + `${prefix}_height_m`, + wing.height_m, + defaults[`${prefix}_height_m`] ?? 0, + ); + keepSpec( + merged, + stored, + `${prefix}_length_m`, + wing.length_m, + defaults[`${prefix}_length_m`] ?? 0, + ); + keepSpec( + merged, + stored, + `${prefix}_angle_deg`, + wing.angle_deg, + defaults[`${prefix}_angle_deg`] ?? 45, + ); + } +} + +/** + * 세월교 폼 옵션 — **지금 그려진 스펙**(`section.ford`) 중 조작·저장분만 싣는다. 조정창 조작이 + * 스펙을 제자리에서 고치므로 그 값은 폼에 보여야 함(2026-08-30 사용자: 수량(련) 칸이 비고 조정창은 + * 1련) — 다만 1련이 **기본값에서 온 값이면** 제안으로만(판정 ④). */ function withFordSpec( - options: Record | undefined, + options: Merged | undefined, ford: NonNullable, -): Record { - const merged: Record = { ...(options ?? {}) }; - if (ford.pipe_kind) merged.pipe_kind = ford.pipe_kind; - merged.pipe_diameter_mm = Math.round(ford.diameter_m * 1000); - merged.pipe_count = ford.pipe_count; - merged.ford_width_m = ford.span_m; - for (const [wing, prefix] of [ + defaults: Record, +): Merged { + const merged: Merged = { ...(options ?? {}) }; + keepSpec(merged, options, "pipe_kind", ford.pipe_kind, defaults.pipe_kind); + const diameter = Math.round(ford.diameter_m * 1000); + keepSpec(merged, options, "pipe_diameter_mm", diameter, defaults.pipe_diameter_mm ?? 1000); + const count = Number(defaults.pipe_count ?? 0); + keepSpec( + merged, + options, + "pipe_count", + ford.pipe_count, + count > 0 ? Math.max(Math.trunc(count), 1) : 1, + ); + keepSpec( + merged, + options, + "ford_width_m", + ford.span_m, + defaults.ford_width_m ?? FORD_BRIDGE_DEFAULT_WIDTH_M, + ); + keepWings(merged, options, defaults, [ [ford.wing_in, "wing_in"], [ford.wing_out, "wing_out"], - ] as const) { - merged[prefix] = wing.installed ? "있음" : "없음"; - if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m; - if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m; - if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg; - } + ]); return merged; } -/** - * BOX암거 폼 옵션을 **지금 그려진 스펙**(`section.box`)으로 덮는다 — 세월교와 같은 - * 규칙이다(2026-08-30 사용자). 본체 규격은 내공 폭·높이가 정본이고, 구체 길이·표고는 - * 조정 채널이라 여기 오지 않는다. - */ +/** BOX암거 폼 옵션 — 세월교와 같은 규칙(조작·저장분만). 구체 길이·표고는 조정 채널이라 여기 안 옴. */ function withBoxSpec( - options: Record | undefined, + options: Merged | undefined, box: NonNullable, -): Record { - const merged: Record = { ...(options ?? {}) }; - merged.body_width_m = box.inner_width_m; - merged.body_height_m = box.inner_height_m; - for (const [wing, prefix] of [ + defaults: Record, +): Merged { + const merged: Merged = { ...(options ?? {}) }; + keepSpec(merged, options, "body_width_m", box.inner_width_m, defaults.body_width_m ?? 2); + keepSpec(merged, options, "body_height_m", box.inner_height_m, defaults.body_height_m ?? 2); + keepWings(merged, options, defaults, [ [box.wing_in, "wing_in"], [box.wing_out, "wing_out"], - ] as const) { - merged[prefix] = wing.installed ? "있음" : "없음"; - if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m; - if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m; - if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg; - } + ]); return merged; } /** - * 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이 - * 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고, - * 비어 있을 때만 스펙(서버 계산·저장분)에서 가져온다. + * 관 지점 옵션에 **조정창에서 조작한 값**만 싣는다 — 조정창과 폼이 어긋나지 않게(2026-08-29 사용자). + * ⚠ 스펙의 기본값(관종·관경·구조·벽 길이·전/후·집수정 길이)은 **안 실음** — 페이지를 열기만 해도 + * 임시 저장에 기본값이 확정으로 굳던 자리(2026-09-14 브레인 판정 ④). 폼 빈 칸은 제안을 보임. */ function withSpecDefaults( - options: Record | undefined, + options: Merged | undefined, detail: SectionDetailResponse | null, chainageM: number, + types: StructureType[], wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null, wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null, -): Record | undefined { +): Merged | undefined { const owner = detail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M, ); - // 세월교는 폼과 조정창이 **같은 캐시**(`section.ford`)를 본다 — 어느 쪽에서 만졌든 - // 그 스펙이 지금 그려진 값이라 저장된 옵션보다 앞선다(2026-08-30 사용자: 프론트는 - // 구조물 배치 상세 UI, 로직·값은 조정창 것). - if (owner?.ford) return withFordSpec(options, owner.ford); - if (owner?.box) return withBoxSpec(options, owner.box); + // 세월교·BOX는 폼과 조정창이 **같은 캐시**를 봄 — 조작한 값은 저장된 옵션보다 앞섬(2026-08-30). + if (owner?.ford) return withFordSpec(options, owner.ford, registryDefaults(types, "ford_bridge")); + if (owner?.box) return withBoxSpec(options, owner.box, registryDefaults(types, "box_culvert")); const culvert = owner?.culvert; if (!culvert) return options; - const merged: Record = { ...(options ?? {}) }; - const put = (key: string, value: string | number | null | undefined): void => { - if (merged[key] === undefined && value !== null && value !== undefined) merged[key] = value; - }; - put("pipe_kind", culvert.pipe_kind); - put("pipe_diameter_mm", Math.round(culvert.diameter_m * 1000)); - for (const [side, prefix] of [ - [culvert.inlet, "inlet"], - [culvert.outlet, "outlet"], - ] as const) { - put(`${prefix}_type`, side.structure); - // 형태·높이는 **지금 그려진 값**이 이긴다 — 조정값(조작)이 저장된 옵션보다 - // 앞서기 때문이다. 저장값을 그대로 두면 폼과 그림이 갈린다(2026-08-30 사용자: - // 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다). + const merged: Merged = { ...(options ?? {}) }; + for (const prefix of ["inlet", "outlet"] as const) { + // 형태·높이는 **조작한 값**이 이긴다 — 조정값이 저장된 옵션보다 앞서기 때문(2026-08-30 사용자: + // 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다). 조작이 없으면 부르는 쪽이 null 을 줌. const drawnForm = wallForm?.(chainageM, prefix); if (drawnForm) merged[`${prefix}_revet_form`] = drawnForm; - else put(`${prefix}_revet_form`, side.revet_form); const drawnHeight = wallHeight?.(chainageM, prefix); if (drawnHeight != null) merged[`${prefix}_revet_height_m`] = Number(drawnHeight.toFixed(1)); - else put(`${prefix}_revet_height_m`, side.revet_height_m); - put(`${prefix}_revet_length_m`, side.revet_length_m); - put(`${prefix}_revet_before_m`, side.revet_before_m); - put(`${prefix}_revet_after_m`, side.revet_after_m); } - put("inlet_basin_length_m", culvert.inlet.basin_length_m); - put("inlet_basin_before_m", culvert.inlet.basin_before_m); - put("inlet_basin_after_m", culvert.inlet.basin_after_m); return merged; } @@ -374,6 +423,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc pipe.options, detail, pipe.chainage_m, + types, deps.wallHeight, deps.wallForm, ), @@ -418,6 +468,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc hit.options, deps.detail?.() ?? null, hit.chainage_m, + markTypes, deps.wallHeight, deps.wallForm, );