From 091a30286e0b28f9e2b1b997fe91f2e55e57b483 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 19:09:42 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05,B06):=20=EB=8F=85=EB=A6=BD=20=EA=B8=B0?= =?UTF-8?q?=EC=8A=AD=EB=A7=89=EC=9D=B4=20=EB=8B=A8=20=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=A2=8C=C2=B7=EC=9A=B0=EB=A1=9C=20=EB=82=98=EB=88=84=EA=B3=A0?= =?UTF-8?q?=20=EA=B7=B8=20=EC=B9=B8=EC=9D=84=20=EA=B0=95=EC=A1=B0=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단 수를 바깥 행에서 좌·우 칸 안으로 옮긴다. 저장 키도 좌·우 각각 (inlet_revet_tiers·outlet_revet_tiers, 옛 tiers는 폴백·호환 기록). 폼 값은 다단 채널(extra·bextra)로 가고, 폼은 그 채널 값을 되읽는다. - 좌·우 칸에도 조정창 행 이식 자리를 두어, 벽을 고르면 배관 유입구·유출구와 같이 그 칸이 강조되고 조정 항목이 옮겨 온다. 칸 이름표는 좌·우 그대로 둔다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_UI_Drainage_Facility.ts | 37 +++++++++++++------ ...B05_Profile_UI_Drainage_Facility_Fields.ts | 20 +++++++++- B06_Section/B06_Section_UI_Adjust_Dock.ts | 34 ++++++++++++----- B06_Section/B06_Section_UI_Page.ts | 19 +++++++--- .../B06_Section_UI_Page_Structures_Panel.ts | 18 +++++++-- 5 files changed, 97 insertions(+), 31 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index b7677dae..89b86d98 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -184,6 +184,9 @@ export interface FacilityOptionsForm { outletSlot: HTMLElement; /** 다단(추가) 기슭막이 행이 옮겨 붙는 자리 — 비면 그룹째 숨는다(2026-08-30 지시 4). */ extraSlot: HTMLElement; + /** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */ + revetInletSlot: HTMLElement; + revetOutletSlot: HTMLElement; /** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */ inletStructure: () => InletStructureKind; setInletStructure: (value: InletStructureKind) => void; @@ -284,18 +287,13 @@ export function createFacilityOptionsForm( extraGroup.root.hidden = true; // ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ───────────── - // 설치 측·단 수는 **컨테이너 밖**(배관의 관종·관경과 같은 층)에 두고, 벽 제원은 - // 좌·우 두 칸으로 나눈다(2026-08-30 사용자 지시 1). 설치 측이 한쪽이면 그 칸만 뜬다. + // 설치 측은 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원과 **단 수**는 좌·우 + // 두 칸이 각자 갖는다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다. // 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만 // 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다. const revetSide = optionalSelect("양쪽", ["양쪽", "좌", "우"]); revetSide.value = "양쪽"; - const revetTiers = numberInput("1", "1"); - revetTiers.value = "1"; - const revetTopRow = grid( - labeled("설치 측", revetSide), - labeled("단 수(다단)", stepper(revetTiers, 1)), - ); + const revetTopRow = grid(labeled("설치 측", revetSide)); const revetInlet = createRevetSideGroup(INLET_REVET_KEYS, "돌쌓기(메)"); const revetOutlet = createRevetSideGroup(OUTLET_REVET_KEYS, "돌쌓기(메)"); /** 좌·우 이름표 — B06이 그 측점의 벽 방향을 알려 주면 바뀐다. 기본은 유입=좌. */ @@ -513,7 +511,9 @@ export function createFacilityOptionsForm( outletRevet.onChange(emit); revetInlet.fields.onChange(emit); revetOutlet.fields.onChange(emit); - [revetSide, revetTiers].forEach((input) => input.addEventListener("change", emit)); + [revetSide, revetInlet.tiers, revetOutlet.tiers].forEach((input) => + input.addEventListener("change", emit), + ); [ pipeMaterial, pipeDiameter, @@ -537,6 +537,8 @@ export function createFacilityOptionsForm( inletSlot, outletSlot, extraSlot, + revetInletSlot: revetInlet.slot, + revetOutletSlot: revetOutlet.slot, setRevetSideLabels(labels) { revetSideLabels = labels; syncVisibility(); @@ -607,7 +609,10 @@ export function createFacilityOptionsForm( fordHeight.value = text("ford_height_m"); fordSlope.value = text("ford_slope_pct"); revetSide.value = text("side") || "양쪽"; - revetTiers.value = text("tiers") || "1"; + // 단 수 — 좌·우 각자 키가 먼저고, 옛 한 벌 값(`tiers`)이 폴백이다. + const legacyTiers = text("tiers") || "1"; + revetInlet.tiers.value = text("inlet_revet_tiers") || legacyTiers; + revetOutlet.tiers.value = text("outlet_revet_tiers") || legacyTiers; const spread = legacyRevetOptions(options); revetInlet.fields.write(spread); revetOutlet.fields.write(spread); @@ -662,8 +667,16 @@ export function createFacilityOptionsForm( wingOutFields.read(options); } else if (current === "revetment") { options.side = revetSide.value; - const tiers = Number.parseInt(revetTiers.value, 10); - if (Number.isFinite(tiers) && tiers >= 1) options.tiers = tiers; + const tierOf = (input: HTMLInputElement): number | null => { + const value = Number.parseInt(input.value, 10); + return Number.isFinite(value) && value >= 1 ? value : null; + }; + const inletTiers = tierOf(revetInlet.tiers); + const outletTiers = tierOf(revetOutlet.tiers); + if (inletTiers !== null) options.inlet_revet_tiers = inletTiers; + if (outletTiers !== null) options.outlet_revet_tiers = outletTiers; + // 옛 한 벌 키도 남긴다 — 서버 `_revet_set`이 아직 이 값을 읽는다. + options.tiers = Math.max(inletTiers ?? 1, outletTiers ?? 1); // 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(2026-08-30). revetInlet.fields.read(options); revetOutlet.fields.read(options); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 8ce83e2c..0877a2b7 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -211,6 +211,10 @@ export interface RevetSideGroup { root: HTMLFieldSetElement; legend: HTMLLegendElement; fields: RevetmentFields; + /** 단 수(다단) — 좌·우가 각자 갖는다(2026-08-30 사용자). */ + tiers: HTMLInputElement; + /** 조정창 행이 옮겨 붙는 자리 — 배관 유입구·유출구 칸과 같은 규칙. */ + slot: HTMLElement; } export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): RevetSideGroup { @@ -218,8 +222,20 @@ export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): const kind = document.createElement("select"); kind.replaceChildren(new Option("기슭막이", "기슭막이")); const fields = createRevetmentFields(keys, { ...REVET_COMMON_DEFAULTS, form: formDefault }); - box.body.append(grid(labeled("구조", kind), fields.formField), ...fields.rows); - return { root: box.root, legend: box.root.querySelector("legend")!, fields }; + const tiers = numberInput("1", "1"); + tiers.value = "1"; + const slot = document.createElement("div"); + slot.className = "b05-structure__adjust-slot"; + box.body.append( + grid(labeled("구조", kind), fields.formField), + ...fields.rows, + grid(labeled("단 수(다단)", stepper(tiers, 1))), + slot, + ); + const legend = box.root.querySelector("legend")!; + // 이름표는 좌·우가 정해 준다 — 고른 구조물 이름을 덧붙이지 않는다(2026-08-30). + legend.dataset.fixedLabel = "1"; + return { root: box.root, legend, fields, tiers, slot }; } export interface RevetmentFields { diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index e4d0c4fc..be68c9cd 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -15,14 +15,18 @@ let slot: HTMLElement | null = null; let placeholder: HTMLElement | null = null; /** 유입구·유출구·추가 기슭막이 그룹의 이식 자리(2026-08-29 지시 4, 2026-08-30 지시 4). * B06 패널이 등록한다. */ -let sideSlots: { inlet: HTMLElement; outlet: HTMLElement; extra: HTMLElement } | null = null; - -/** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */ -export function setAdjustSideSlots(next: { +interface SideSlots { inlet: HTMLElement; outlet: HTMLElement; extra: HTMLElement; -}): void { + /** 독립 기슭막이 좌·우 칸 — 배관 유입구·유출구 칸이 숨어 있을 때 이쪽으로 간다. */ + revetInlet: HTMLElement; + revetOutlet: HTMLElement; +} +let sideSlots: SideSlots | null = null; + +/** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */ +export function setAdjustSideSlots(next: SideSlots): void { sideSlots = next; } @@ -55,8 +59,8 @@ const movedRows: Array<{ row: HTMLElement; anchor: Comment }> = []; /** 그룹 이름표 — 원래 문구를 한 번만 적어 두고, 선택에 따라 괄호 표기를 붙였다 뗀다. */ function setGroupLabel(group: HTMLElement, suffix: string): void { - const legend = group.querySelector("legend"); - if (!legend) return; + const legend = group.querySelector("legend"); + if (!legend || legend.dataset.fixedLabel) return; legend.dataset.baseLabel ??= legend.textContent ?? ""; const base = legend.dataset.baseLabel; legend.textContent = suffix ? `${base} (${suffix})` : base; @@ -68,7 +72,13 @@ function clearSideSlots(): void { const { row, anchor } = movedRows.pop()!; anchor.parentNode?.replaceChild(row, anchor); } - for (const target of [sideSlots?.inlet, sideSlots?.outlet, sideSlots?.extra]) { + for (const target of [ + sideSlots?.inlet, + sideSlots?.outlet, + sideSlots?.extra, + sideSlots?.revetInlet, + sideSlots?.revetOutlet, + ]) { target?.replaceChildren(); const group = target?.closest("fieldset"); if (!group) continue; @@ -87,7 +97,13 @@ function relocateSideRows(panelRoot: HTMLElement): void { clearSideSlots(); const side = panelRoot.dataset.side; if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return; - const target = sideSlots[side]; + // 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽으로 간다. + const target = + side === "extra" + ? sideSlots.extra + : slotUsable(sideSlots[side]) + ? sideSlots[side] + : sideSlots[side === "inlet" ? "revetInlet" : "revetOutlet"]; // 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 여기서 세운다. if (side === "extra") { const group = target.closest("fieldset"); diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 862398fb..c0c9343a 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -173,12 +173,21 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const inletLeft = (owner.uphill_side ?? "left") === "left"; return inletLeft ? { inlet: "좌", outlet: "우" } : { inlet: "우", outlet: "좌" }; }, - // 폼 단 수 → 다단 채널(유출 = extra, 유입·계류측 = bextra). 독립 기슭막이는 좌·우 - // 두 벽이 같은 단 수를 쓰므로 두 채널에 같이 넣는다. - setTierCount: (chainageM, tiers) => { + // 폼 단 수 ↔ 다단 채널(유출 벽 = extra, 유입·계류측 = bextra) — 좌·우가 각자 쓴다. + setTierCount: (chainageM, role, tiers) => { const extras = Math.max(0, Math.round(tiers) - 1); - stationControls.extraWalls.setCount(chainageM, extras, "outlet"); - stationControls.extraWalls.setCount(chainageM, extras, "basin"); + stationControls.extraWalls.setCount( + chainageM, + extras, + role === "outlet" ? "outlet" : "basin", + ); + }, + tierCount: (chainageM, role) => { + const owner = sectionDetail?.cross_sections.find( + (section) => Math.abs(section.chainage_m - chainageM) < 0.51, + ); + if (!owner) return null; + return stationControls.extraWalls.countFor(owner, role === "outlet" ? "outlet" : "basin") + 1; }, stationInterval: () => stationInterval ?? 20, focusChainage: focusStationAt, diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 31b90a53..5016e46b 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -48,7 +48,9 @@ export interface B06StructuresPanelDeps { /** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */ wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; /** 폼의 단 수를 다단 채널에 반영한다 — 조정창에서 세우던 것과 같은 자리. */ - setTierCount?: (chainageM: number, tiers: number) => void; + setTierCount?: (chainageM: number, role: "inlet" | "outlet", tiers: number) => void; + /** 지금 선 단 수(기준벽 포함) — 폼이 도면과 같은 값을 보이게 한다. */ + tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ @@ -110,6 +112,7 @@ function withSpecDefaults( chainageM: number, wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null, wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null, + tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null, ): Record | undefined { const owner = detail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M, @@ -136,6 +139,9 @@ function withSpecDefaults( 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); + // 단 수는 다단 채널이 정본이다 — 폼이 그 값을 그대로 보인다. + const tiers = tierCount?.(chainageM, prefix); + if (tiers != null) merged[`${prefix}_revet_tiers`] = tiers; 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); @@ -199,8 +205,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc } // 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야 // 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게). - const tiers = Number(patch.tiers); - if (Number.isFinite(tiers)) deps.setTierCount?.(fromChainageM, tiers); + for (const role of ["inlet", "outlet"] as const) { + const tiers = Number(patch[`${role}_revet_tiers`]); + if (Number.isFinite(tiers)) deps.setTierCount?.(fromChainageM, role, tiers); + } // 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다. deps.queuePipeOptions(fromChainageM, patch as Record); deps.applyPipeOptions?.(fromChainageM, patch as Record); @@ -233,6 +241,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc inlet: section.facility.inletSlot, outlet: section.facility.outletSlot, extra: section.facility.extraSlot, + revetInlet: section.facility.revetInletSlot, + revetOutlet: section.facility.revetOutletSlot, }); async function load(): Promise { @@ -262,6 +272,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc pipe.chainage_m, deps.wallHeight, deps.wallForm, + deps.tierCount, ), design_flow_m3s: null, })); @@ -293,6 +304,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc hit.chainage_m, deps.wallHeight, deps.wallForm, + deps.tierCount, ); section.setPipeFacilities(pipeFacilities); }