From c8b5a8bf38e06f8950a5060f2f055a44cd114c05 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 00:15:53 +0900 Subject: [PATCH 01/34] =?UTF-8?q?fix(B05):=20=EC=A2=85=EB=8B=A8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EB=B9=88=20=EC=B9=B8=EC=9D=98=20=EC=84=B8=EB=A1=9C?= =?UTF-8?q?=EC=84=A0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조물 측점(세월교·BOX암거 등) 빈 칸의 border-left가 이웃 규칙 측점 값 한가운데를 지나고, 곡선 L/R 칸 경계와 x가 정확히 겹쳐 절토고~곡선 R를 관통하는 한 줄로 보였다. 값 행에서만 선을 지우고 곡선 행 구분선은 유지한다. Co-Authored-By: Claude Opus 5 (1M context) --- B05_Profile/B05_Profile_UI_Style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/B05_Profile/B05_Profile_UI_Style.css b/B05_Profile/B05_Profile_UI_Style.css index cef484d2..665e004c 100644 --- a/B05_Profile/B05_Profile_UI_Style.css +++ b/B05_Profile/B05_Profile_UI_Style.css @@ -636,6 +636,13 @@ border-right: 1px solid var(--color-border); } +/* 구조물 측점 빈 칸의 세로선은 이웃 규칙 측점의 값 한가운데를 지나고, 곡선 L/R 칸 + 경계와 x가 겹쳐 곡선 행 구분선이 위 행들까지 이어진 것처럼 보인다. 곡선 행에만 + 남기고 값 행에서는 지운다(2026-08-30 사용자 지시). */ +.b05-profile-table__cell.is-structure { + border-left-color: transparent; +} + .b05-profile-table__row.is-cut .b05-profile-table__cell { color: rgb(220 38 38); } From c8ce7ae02a53e24ccab91d407ca2d5099e31ce04 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 00:29:43 +0900 Subject: [PATCH 02/34] =?UTF-8?q?fix(B06):=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=EC=84=A0=ED=83=9D=20?= =?UTF-8?q?=EC=8B=9C=20=EB=B6=80=EB=AA=A8=20=EC=B9=B8=EC=9D=98=20=EB=8B=A8?= =?UTF-8?q?=20=EC=88=98=C2=B7=EC=98=B5=EC=85=98=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단 수·옵션 행은 조정창 사본에서 DOM째 빌려온 행이라, 다단 벽을 고르면 clearSideSlots()가 되돌린 뒤 다시 채우지 못해 유입구·유출구 칸이 비었다. 다단 벽에서도 tierChannel을 부모 채널로 열어 두 행을 살리고(revetWall에 isExtra 포함), 이식은 부모 칸으로 보낸다. 추가 기슭막이 칸에는 형태· 높이·구간값만 남긴다. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Adjust_Dock.ts | 40 +++++++++++-------- .../B06_Section_UI_Cross_Structure_Panel.ts | 13 +++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index 52ddbb81..18a83ce5 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -30,13 +30,12 @@ export function setAdjustSideSlots(next: SideSlots): void { sideSlots = next; } -/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값까지 전부 온다. */ +/** 다단 벽에서 옮겨 오는 행 — 폼에 자기 칸이 없어 형태·높이·구간값이 온다. + * 단 수·옵션은 부모 벽의 값이라 여기 오지 않고 부모 칸에 남는다(2026-08-30 사용자). */ const EXTRA_ROWS = [ ".b06-structure-panel__struct--form", ".b06-structure-panel__struct--height", ".b06-structure-panel__struct--span", - ".b06-structure-panel__struct--tier", - ".b06-structure-panel__struct--options", ].join(", "); /** 유입구·유출구는 폼이 이미 가진 항목 말고 단 수·옵션만 받는다. */ @@ -97,6 +96,18 @@ function clearSideSlots(): void { } } +/** 행 DOM을 제자리 표식과 함께 옮긴다 — 숨은 행은 그대로 둔다. */ +function moveRows(panelRoot: HTMLElement, selector: string, target: HTMLElement): void { + panelRoot.querySelectorAll(selector).forEach((row) => { + if (row.classList.contains("is-hidden")) return; + // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. + const anchor = document.createComment("adjust-row"); + row.parentNode?.insertBefore(anchor, row); + movedRows.push({ row, anchor }); + target.append(row); + }); +} + /** * 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을 * 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다. @@ -111,21 +122,18 @@ function relocateSideRows(panelRoot: HTMLElement): void { if (side === "extra") { const group = target.closest("fieldset"); if (!group) return; - const owner = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet").closest( - "fieldset", - ); + const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet"); + const owner = ownerSlot.closest("fieldset"); if (owner && owner !== group) owner.after(group); group.hidden = false; - } else if (!slotUsable(target)) return; - const rows = panelRoot.querySelectorAll(side === "extra" ? EXTRA_ROWS : SIDE_ROWS); - rows.forEach((row) => { - if (row.classList.contains("is-hidden")) return; - // 제자리에 표식을 남기고 옮긴다 — 선택이 바뀌면 이 자리로 되돌린다. - const anchor = document.createComment("adjust-row"); - row.parentNode?.insertBefore(anchor, row); - movedRows.push({ row, anchor }); - target.append(row); - }); + moveRows(panelRoot, EXTRA_ROWS, target); + // 단 수·옵션은 그 단이 딸린 부모 벽의 값이라 부모 칸에 그대로 세운다 — 다단을 + // 고를 때 그 두 행이 사라지던 것을 막는다(2026-08-30 사용자). + if (slotUsable(ownerSlot)) moveRows(panelRoot, SIDE_ROWS, ownerSlot); + } else { + if (!slotUsable(target)) return; + moveRows(panelRoot, SIDE_ROWS, target); + } // 고른 구조물이 어느 쪽인지 그룹 테두리로 알린다(2026-08-29 사용자 지시 3). const group = target.closest("fieldset"); if (!group) return; diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index 3349554f..b360e64d 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -569,7 +569,15 @@ export function buildStructurePanel( // 유출 벽과 독립 기슭막이 유입 벽에서는 **항상** 보인다(2026-08-22 사용자 ⑤ — // 상태 따라 나타났다 사라지면 레이아웃이 널뛴다). 집수정 계류측은 둘 자리가 // 있을 때만 뜬다(배관 유입 기슭막이는 다단 채널 자체가 없다). - tierChannel = key === "outlet" ? "outlet" : key === "inlet" ? "basin" : null; + // 다단 벽을 고른 동안에도 단 수는 **그 단이 딸린 부모 벽**의 값이다 — 채널을 + // 부모 쪽으로 열어 두고 행은 부모 칸에 남긴다(2026-08-30 사용자: 추가 기슭막이를 + // 고르면 단 수·옵션이 사라지던 것). + tierChannel = + key === "outlet" || key.startsWith("extra") + ? "outlet" + : key === "inlet" || key.startsWith("bextra") + ? "basin" + : null; // 좌측 [횡단 조정] 사본의 단 수·옵션 행이 어느 그룹(유입구·유출구)으로 옮겨 // 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다. // 다단(추가) 벽은 유입·유출 어느 칸에도 자기 자리가 없다 — 유입구·유출구 아래 @@ -596,7 +604,8 @@ export function buildStructurePanel( // 기슭막이 기준벽(배관 유입·유출, 독립 좌·우)에서는 **항상** 보인다(2026-08-30 // 사용자). 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다. // 집수정 유입은 계류측 단이 설 자리가 있을 때만 — 벽이 아니라 구체다. - const revetWall = (key === "inlet" || key === "outlet") && !isBasin; + // 다단 벽도 포함한다 — 행은 부모 칸에 서 있고, 그 값(부모 단 수·옵션)을 그대로 쓴다. + const revetWall = ((key === "inlet" || key === "outlet") && !isBasin) || isExtra; extraRow.classList.toggle( "is-hidden", !revetWall && !(isBasin && tierChannel === "basin" && (tiers.canAdd || tiers.count > 0)), From 03840ff3d670977d82ea28dcb8eb9baf1af403e5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 11:44:54 +0900 Subject: [PATCH 03/34] =?UTF-8?q?feat(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=E3=80=8C=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=E3=80=8D=20=ED=8F=BC=EC=9D=84=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=EC=B0=BD=20=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 좌측 폼의 세월교 상세 항목(관종·관경·수량·월류 폭·날개벽 유입/유출)이 도면에 전혀 반영되지 않았다. `applyPipeOptionsToCache`가 `section.culvert`만 보고 세월교 측점(`section.ford`)에서는 즉시 return했기 때문이다. 프론트는 구조물 배치의 상세 UI를 쓰고 로직은 조정창 제어기 것을 쓴다 (2026-08-30 사용자 확정). - `applyFordFormOptions()` 신설 — 폼 옵션 키를 `FordControl.setPipe`/`setWing` 인자로 옮긴다. 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기에 이미 있어 새로 만들지 않았다. - `FordControl.setPipe` 확장 — `pipe_kind`(관종)·`ford_width_m`(월류 폭 = `span_m`) 추가. 두 값은 조정창에 칸이 없고 폼만 보낸다. - `withFordSpec()` — 폼 기본값을 `section.ford` 스펙에서 읽는다. 두 창이 같은 스펙을 보므로 어느 쪽에서 만졌든 값이 갈리지 않는다(수량(련) 빈 칸 해소). 월류 높이(`ford_height_m`)는 백엔드 `_ford_set`이 읽지 않는다 — 세월교는 구체 위 노면이라 파임이 없고, 물넘이포장 전용 값이다. 폼에는 남되 도면 반영처는 없다. 자체검증(공용 브라우저 5174, 측점 7+9.7 세월교): - tsc --noEmit 통과. - 폼 날개 길이(유입) 2→4 → 조정창 「날개 길이 4.0m」, 바닥판 7.63m→9.04m (= +2×cos45°). 수정 전에는 둘 다 그대로였다. - 폼 수량 3 → 조정창 「3련」, 폼 관경 Ø1200 → 조정창 select 1200. - 재선택 왕복: 월류폭 12·수량 3·관경 1200·날개길이 4 그대로 되읽힘(스펙 정본). - 진입 시 수량(련) 칸 = 1(수정 전 빈 칸), 관종 파형강관. - 조작값은 원래 값(10/1/1000/2)으로 되돌려 놓았다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Ford_Panel.ts | 13 ++++- B06_Section/B06_Section_UI_Page.ts | 12 ++++- .../B06_Section_UI_Page_Ford_Controls.ts | 48 +++++++++++++++++++ .../B06_Section_UI_Page_Structures_Panel.ts | 30 ++++++++++++ 4 files changed, 99 insertions(+), 4 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts index 96bd8a1a..ca395b77 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Panel.ts @@ -277,8 +277,17 @@ export interface FordControl { adjustFor: (chainageM: number) => FordAdjust; update: (chainageM: number, role: FordWallRole, patch: Partial) => void; reset: (chainageM: number, role: FordWallRole) => void; - /** 관경(mm)·수량(련) 저장 — B05 정본(`pipe_points`)으로 간다. */ - setPipe: (chainageM: number, patch: { pipe_diameter_mm?: number; pipe_count?: number }) => void; + /** 관경(mm)·수량(련)·관종·월류 폭 저장 — B05 정본(`pipe_points`)으로 간다. + * 관종·월류 폭은 조정창에 칸이 없고 좌측 「구조물 배치」 폼만 보낸다(2026-08-30 사용자). */ + setPipe: ( + chainageM: number, + patch: { + pipe_diameter_mm?: number; + pipe_count?: number; + pipe_kind?: string; + ford_width_m?: number; + }, + ) => void; /** 날개벽 제원 저장(2026-08-25 사용자 — 조정창에서 직접 제어). 같은 정본으로 간다. */ setWing: ( chainageM: number, diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 8ae7da6f..85535789 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -31,6 +31,7 @@ import { } from "./B06_Section_Api_Fetch"; import { flushPendingStructures } from "../B05_Profile/B05_Profile_Api_Structures"; import { createStationControls } from "./B06_Section_UI_Page_Station_Controls"; +import { applyFordFormOptions } from "./B06_Section_UI_Page_Ford_Controls"; import { buildCrossPatches } from "./B06_Section_UI_Page_Patches"; import { maxToeFitHalfWidth } from "./B06_Section_UI_Cross_Fit"; import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit"; @@ -194,8 +195,15 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const owner = sectionDetail?.cross_sections.find( (section) => Math.abs(section.chainage_m - chainageM) < 0.51, ); - const culvert = owner?.culvert; - if (!owner || !culvert) return; + if (!owner) return; + // 세월교는 스펙 자리(`section.ford`)가 배수관과 달라 조정창 제어기로 보낸다 — + // 로직은 그쪽 것을 쓰고 프론트만 폼 UI다(2026-08-30 사용자 확정). + if (owner.ford) { + applyFordFormOptions(stationControls.ford, owner.chainage_m, patch); + return; + } + const culvert = owner.culvert; + if (!culvert) return; const num = (key: string): number | undefined => { const value = Number(patch[key]); return Number.isFinite(value) ? value : undefined; diff --git a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts index ef97eaec..4be840fb 100644 --- a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts @@ -114,6 +114,9 @@ export function createFordControls(deps: FordControlDeps): FordControls { // 캐시를 먼저 고쳐 즉시 반영한다 — 저장은 늦게 묶어서 간다. if (patch.pipe_diameter_mm) spec.diameter_m = patch.pipe_diameter_mm / 1000; if (patch.pipe_count) spec.pipe_count = patch.pipe_count; + if (patch.pipe_kind) spec.pipe_kind = patch.pipe_kind; + // 월류 폭 = 구체의 도로 진행 방향 길이(`span_m`) — 백엔드 `_ford_set`과 같은 자리. + if (patch.ford_width_m) spec.span_m = patch.ford_width_m; } deps.queuePipeOptions(chainageM, patch); deps.refreshCard(chainageM); @@ -160,6 +163,51 @@ export function createFordControls(deps: FordControlDeps): FordControls { }; } +/** + * 좌측 「구조물 배치」 폼이 낸 세월교 옵션을 **조정창 제어기**로 흘려보낸다 + * (2026-08-30 사용자 확정: 프론트는 구조물 배치의 상세 UI, 로직은 조정창 것을 쓴다). + * + * 폼 옵션 키(`pipe_*`·`ford_width_m`·`wing_in*`·`wing_out*`)를 제어기 인자로 옮기기만 + * 한다 — 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기 안에 이미 있다. + * 값이 온 항목만 보낸다(빈 patch는 부르지 않는다). + */ +export function applyFordFormOptions( + control: FordControl, + chainageM: number, + patch: Record, +): void { + const num = (key: string): number | undefined => { + if (patch[key] === undefined) return undefined; + const value = Number(patch[key]); + return Number.isFinite(value) ? value : undefined; + }; + const pipe: Parameters[1] = {}; + const diameterMm = num("pipe_diameter_mm"); + if (diameterMm !== undefined) pipe.pipe_diameter_mm = diameterMm; + const count = num("pipe_count"); + if (count !== undefined) pipe.pipe_count = count; + if (typeof patch.pipe_kind === "string") pipe.pipe_kind = patch.pipe_kind; + const widthM = num("ford_width_m"); + if (widthM !== undefined) pipe.ford_width_m = widthM; + if (Object.keys(pipe).length) control.setPipe(chainageM, pipe); + + for (const [role, prefix] of [ + ["inlet", "wing_in"], + ["outlet", "wing_out"], + ] as const) { + const wing: Parameters[2] = {}; + // 설치 값은 폼이 "있음"/"없음" 문자열로 낸다(백엔드 `_wing_spec`과 같은 규약). + if (patch[prefix] !== undefined) wing.installed = patch[prefix] !== "없음"; + const heightM = num(`${prefix}_height_m`); + if (heightM !== undefined) wing.height_m = heightM; + const lengthM = num(`${prefix}_length_m`); + if (lengthM !== undefined) wing.length_m = lengthM; + const angleDeg = num(`${prefix}_angle_deg`); + if (angleDeg !== undefined) wing.angle_deg = angleDeg; + if (Object.keys(wing).length) control.setWing(chainageM, role, wing); + } +} + /** * BOX암거 구체 조작값 제어 — 세월교와 같은 흐름(세션 사본 + 캐시 `design.box_adjust`). * 좌·우 끝을 따로 잡으며 길이는 바깥으로만, 표고는 양방향으로 움직인다. diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 60543377..f8da0eae 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -102,6 +102,32 @@ function coversChainage(structure: StructureInstance, chainageM: number): boolea return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M; } +/** + * 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이 + * 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다 + * (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다). + */ +function withFordSpec( + options: Record | 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 [ + [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; +} + /** * 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이 * 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고, @@ -117,6 +143,10 @@ function withSpecDefaults( 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); const culvert = owner?.culvert; if (!culvert) return options; const merged: Record = { ...(options ?? {}) }; From e78feab5ed433832f20673d5ebead5fa0da79125 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 30 Aug 2026 12:05:22 +0900 Subject: [PATCH 04/34] =?UTF-8?q?feat(B06):=20=EC=84=B8=EC=9B=94=EA=B5=90?= =?UTF-8?q?=20=EC=A2=8C=EC=B8=A1=20=ED=8F=BC=EC=9D=84=20=EB=B0=B0=EA=B4=80?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=90=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=96=91?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=A7=9E=EC=B6=98=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 지시 3건(2026-08-30). 1) 횡단도에서 세월교 측벽을 고르면 좌측 폼의 그 측 칸이 선다 — 배관 기슭막이가 쓰던 경로를 그대로 탄다. `SideSlots`에 `wingInlet`·`wingOutlet`을 더하고 `slotOf()`가 배관 칸 → 독립 기슭막이 칸 → 날개벽 칸 순으로 지금 서 있는 쪽을 고른다. 세월교 조정창은 `dataset.side`·`dataset.panelTitle`을 세워 어느 칸을 가리키는지 알린다. 벽 선택 뒤 `refreshAdjustSlots()`도 배관과 같게 붙였다. 2) 월류 폭·높이·수량·개략 단면 결과를 관경 바로 다음으로 옮겼다(바닥 경사도 같은 묶음에 둔다 — 물넘이포장 전용이라 세월교에서는 숨는다). 개략 단면 안내문은 읽는 글이라 10px·opacity 0.75로 줄였다. 3) 세월교·날개벽 숫자 칸을 기슭막이·집수정과 같은 [-][값][+] 묶음으로 바꿨다 (월류 폭·높이·바닥 경사·수량·날개 높이·길이·각도). `stepper()`에 표시 자릿수 인자를 더해 련·각도는 정수로 보인다("1.0련"·"45.0°" 방지). 기본값 1이라 기존 호출부는 그대로다. 자체검증(공용 브라우저 5174, 측점 7+9.7 세월교): - tsc --noEmit 통과. - 유입 측벽 선택 → `날개벽(유입) (세월교)` 칸 강조, 유출 측벽 → `날개벽(유출) (세월교)`. 한 번에 한 칸만 선다. - 폼 항목 순서 실측: 관종 > 관경 > 월류 폭 > 월류 높이 > 수량 > 날개벽(유입) … 안내문 font-size 10px, 보이는 스텝 묶음 9개. - 수량 + → 폼 "2"·조정창 "2련", 각도 + → 폼 "50"·조정창 "50°" (소수점 없음). - 회귀: 배관 측점 기슭막이 선택 → `유출구 (기슭막이(유출))` 강조 + 높이·길이· 기준측점 전/후 행 이식 정상. - 조작값은 원래대로(수량 1·각도 45) 되돌려 놓았다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_UI_Drainage_Facility.ts | 26 +++++++++++++------ ...B05_Profile_UI_Drainage_Facility_Fields.ts | 25 +++++++++++++----- .../B05_Profile_UI_Style_Structures.css | 9 +++++++ B06_Section/B06_Section_UI_Adjust_Dock.ts | 16 +++++++++--- .../B06_Section_UI_Cross_Ford_Panel.ts | 5 ++++ .../B06_Section_UI_Page_Structures_Panel.ts | 5 ++++ 6 files changed, 68 insertions(+), 18 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 6a9c33ad..8469ecd8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -187,6 +187,10 @@ export interface FacilityOptionsForm { /** 독립 기슭막이 좌·우 칸의 이식 자리 — 배관 유입구·유출구 자리와 같은 몫이다. */ revetInletSlot: HTMLElement; revetOutletSlot: HTMLElement; + /** 세월교·BOX암거 날개벽 칸의 이식 자리 — 세월교 유입·유출 측벽을 고르면 이 칸이 + * 선다(2026-08-30 사용자 지시 1: 배관 측점·기슭막이와 같게). */ + wingInSlot: HTMLElement; + wingOutSlot: HTMLElement; /** 유입구 "구조"에 합쳐진 B06 형식 값(2026-08-29 지시 5). B06이 조정창 값과 맞춘다. */ inletStructure: () => InletStructureKind; setInletStructure: (value: InletStructureKind) => void; @@ -356,7 +360,9 @@ export function createFacilityOptionsForm( // ── 세월교 — 구체 내 배관은 배수관과 같은 관종·관경 칸을 쓰고(2026-08-17 사용자 // 지시) 수량만 따로 받는다. const fordCount = numberInput("1", "1", "련"); - const fordRow = grid(labeled("수량 (련)", fordCount)); + // 숫자 칸은 기슭막이·집수정과 같은 [-][값][+] 묶음(2026-08-30 사용자 지시 3). + // 련은 정수라 소수 자릿수를 두지 않는다. + const fordRow = grid(labeled("수량 (련)", stepper(fordCount, 1, 0))); // ── 물넘이·세월교 개략 단면 — 월류 폭 + 월류 높이 한 행(2026-08-18 사용자 지시). // 폭 기본값은 세월교 10m·물넘이 포장 5m(사용자 확정 — 지식DB 폭 수치 근거 없음). @@ -365,14 +371,14 @@ export function createFacilityOptionsForm( const fordWidth = numberInput("0.1"); const fordHeight = numberInput("0.01"); const fordWidthRow = grid( - labeled("월류 폭 (m)", fordWidth), - labeled("월류 높이 (m)", fordHeight), + labeled("월류 폭 (m)", stepper(fordWidth, 0.1)), + labeled("월류 높이 (m)", stepper(fordHeight, 0.1)), ); // 물넘이 바닥은 유입(상류)이 높고 유출이 낮게 기운다(2026-08-28 사용자 확정). // 비우면 그 측점의 **노면 횡단경사**를 그대로 쓴다 — 횡단도가 판단한다. const fordSlope = numberInput("0.1"); fordSlope.placeholder = "노면 기울기"; - const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", fordSlope)); + const fordSlopeRow = grid(labeled("바닥 경사 유입→유출 (%)", stepper(fordSlope, 0.1))); const fordSummary = document.createElement("p"); fordSummary.className = "b05-drainage__facility-note"; /** 담당 유역 설계유량(㎥/s) — 개략 단면의 입력. 유역이 없으면 null. */ @@ -423,8 +429,14 @@ export function createFacilityOptionsForm( emit(); }); + // 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) — + // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( pipeRow, + fordWidthRow, + fordSlopeRow, + fordRow, + fordSummary, inletGroup.root, outletGroup.root, extraGroup.root, @@ -434,10 +446,6 @@ export function createFacilityOptionsForm( boxWrap, wingInFields.root, wingOutFields.root, - fordWidthRow, - fordSlopeRow, - fordRow, - fordSummary, ); let current: PipeFacility | null = null; @@ -537,6 +545,8 @@ export function createFacilityOptionsForm( extraSlot, revetInletSlot: revetInlet.slot, revetOutletSlot: revetOutlet.slot, + wingInSlot: wingInFields.slot, + wingOutSlot: wingOutFields.slot, setRevetSideLabels(labels) { revetSideLabels = labels; syncVisibility(); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts index 803c24a8..0bead6cd 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -84,7 +84,9 @@ export function numberInput(step: string, min = "0", placeholder = ""): HTMLInpu /** 스텝 묶음 입력에 붙일 일련번호 — 라벨이 가리킬 대상을 명시하는 데 쓴다. */ let stepperSeq = 0; -export function stepper(input: HTMLInputElement, stepM: number): HTMLElement { +/** `decimals` — 표시 자릿수. 련·각도처럼 정수로 세는 칸은 0을 준다(2026-08-30 사용자: + * 세월교 상세도 같은 양식으로 맞추되 "1.0련"·"45.0°"로 보이면 안 된다). */ +export function stepper(input: HTMLInputElement, stepM: number, decimals = 1): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b05-structure__stepper"; // 라벨이 이 입력을 가리키게 id를 붙인다. 없으면