From 8b385a62b0ea14ae0aea89d28986e7ddfcd45a10 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 19:15:07 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(B06):=20=EC=B9=B4=EB=93=9C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EB=8B=A4=EB=A5=B8=20clipPath=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EC=9D=84=20=EC=8D=A8=20=ED=95=B4=EC=B9=AD=EC=9D=B4=20=EC=82=AC?= =?UTF-8?q?=EB=9D=BC=EC=A7=80=EC=A7=80=20=EC=95=8A=EA=B2=8C=20=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clipPath id는 문서 전역인데 이름을 벽 키 + 배면 offset으로 지어, 같은 벽을 소유 측점과 연동 측점이 함께 그리면 이름이 겹쳤다. url(#id)는 문서에서 처음 만난 clipPath를 쓰므로 뒤 카드의 해칭이 앞 카드 좌표로 잘려 통째로 사라졌다. 벽을 옮기면 두 카드가 같은 offset으로 다시 겹쳐 증상이 재발했다. 측정(14+0.0 화면): 6개 중 4개 id가 중복 → 수정 후 10개 전부 고유, 모든 카드의 해칭 그룹이 화면에 폭을 갖는다. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts index 85c9982a..717aa941 100644 --- a/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts +++ b/B06_Section/B06_Section_UI_Cross_Wall_Hatch.ts @@ -22,6 +22,14 @@ import type { WallLayout } from "./B06_Section_UI_Cross_Culvert_Types"; const SVG_NS = "http://www.w3.org/2000/svg"; const HATCH_CLASS = "b06-chart__culvert-stone"; +/** + * clipPath id 일련번호 — id는 **문서 전역**이라 같은 벽을 소유 측점과 연동 측점이 + * 함께 그리면 이름이 겹쳤다. `url(#id)`는 문서에서 **처음 만난** clipPath를 쓰므로, + * 뒤 카드의 해칭이 앞 카드 좌표로 잘려 통째로 사라졌다(2026-08-30 사용자: 기준 + * 측점 벽을 옮기니 연동 측점 형태 표현이 다 없어졌다). 카드·다시 그리기마다 새 번호. + */ +let clipSeq = 0; + /** 형태 문자열 → 표현 갈래. 판정 순서는 좁은 말(돌망태·바자)부터. */ export type WallHatch = "dry" | "wet" | "concrete" | "gabion" | "log" | "wattle"; @@ -174,7 +182,8 @@ export function appendWallHatch( ), ); - const clipId = `b06-revet-clip-${keyId}-${Math.round(wall.backOffset * 100)}`; + clipSeq += 1; + const clipId = `b06-revet-clip-${keyId}-${clipSeq}`; const clip = document.createElementNS(SVG_NS, "clipPath"); clip.setAttribute("id", clipId); const clipShape = document.createElementNS(SVG_NS, "polygon"); From 2ee71eff191866cae41b10d48dbbcf68835648e4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 19:17:07 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(B05,B06):=20=EB=8F=85=EB=A6=BD=20?= =?UTF-8?q?=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=ED=8F=BC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=8B=A8=20=EC=88=98=EB=A5=BC=20=EB=BA=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조정창에서 옮겨 오는 [추가 기슭막이(단)] 행과 같은 값이라 두 벌이었다. 단 수 정본은 횡단 설계 patch(extra_wall_counts)이고 조작은 그 행이 한다 — 폼 칸과 그 값을 나르던 배선(tierCount·setTierCount)을 함께 걷어낸다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B05_Profile_UI_Drainage_Facility.ts | 24 ++++--------------- ...B05_Profile_UI_Drainage_Facility_Fields.ts | 9 +++---- B06_Section/B06_Section_UI_Page.ts | 16 ------------- .../B06_Section_UI_Page_Structures_Panel.ts | 14 ----------- 4 files changed, 8 insertions(+), 55 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 89b86d98..6a9c33ad 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -287,8 +287,8 @@ export function createFacilityOptionsForm( extraGroup.root.hidden = true; // ── 독립 기슭막이(2026-08-28 이관: 배관처럼 pipe_points 관리) ───────────── - // 설치 측은 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원과 **단 수**는 좌·우 - // 두 칸이 각자 갖는다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다. + // 설치 측은 **컨테이너 밖**(배관의 관종·관경과 같은 층), 벽 제원은 좌·우 두 칸으로 + // 나눈다(2026-08-30 사용자). 설치 측이 한쪽이면 그 칸만 뜬다. // 좌·우가 저장 채널(유입/유출) 중 어느 쪽인지는 측점 지형이 정하므로 이름표만 // 바꿔 단다(`setRevetSideLabels`) — 값은 언제나 그 채널 키로 오간다. const revetSide = optionalSelect("양쪽", ["양쪽", "좌", "우"]); @@ -511,9 +511,7 @@ export function createFacilityOptionsForm( outletRevet.onChange(emit); revetInlet.fields.onChange(emit); revetOutlet.fields.onChange(emit); - [revetSide, revetInlet.tiers, revetOutlet.tiers].forEach((input) => - input.addEventListener("change", emit), - ); + revetSide.addEventListener("change", emit); [ pipeMaterial, pipeDiameter, @@ -609,10 +607,6 @@ export function createFacilityOptionsForm( fordHeight.value = text("ford_height_m"); fordSlope.value = text("ford_slope_pct"); revetSide.value = text("side") || "양쪽"; - // 단 수 — 좌·우 각자 키가 먼저고, 옛 한 벌 값(`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); @@ -667,16 +661,8 @@ export function createFacilityOptionsForm( wingOutFields.read(options); } else if (current === "revetment") { options.side = revetSide.value; - 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); + // 단 수는 폼이 갖지 않는다 — 횡단 설계 patch(extra_wall_counts)가 정본이고 + // 조작은 [추가 기슭막이(단)] 행이 한다(2026-08-30 사용자: 중복이라 삭제). // 좌·우가 각자 값을 갖는다 — 저장 채널은 유입/유출 키 그대로다(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 0877a2b7..803c24a8 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts @@ -211,8 +211,6 @@ export interface RevetSideGroup { root: HTMLFieldSetElement; legend: HTMLLegendElement; fields: RevetmentFields; - /** 단 수(다단) — 좌·우가 각자 갖는다(2026-08-30 사용자). */ - tiers: HTMLInputElement; /** 조정창 행이 옮겨 붙는 자리 — 배관 유입구·유출구 칸과 같은 규칙. */ slot: HTMLElement; } @@ -222,20 +220,19 @@ 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 }); - 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))), + // 단 수는 여기 두지 않는다 — 조정창에서 옮겨 오는 [추가 기슭막이(단)] 행과 + // 같은 값이라 두 벌이면 헷갈린다(2026-08-30 사용자). slot, ); const legend = box.root.querySelector("legend")!; // 이름표는 좌·우가 정해 준다 — 고른 구조물 이름을 덧붙이지 않는다(2026-08-30). legend.dataset.fixedLabel = "1"; - return { root: box.root, legend, fields, tiers, slot }; + return { root: box.root, legend, fields, slot }; } export interface RevetmentFields { diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index c0c9343a..8f1dc02a 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -173,22 +173,6 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { const inletLeft = (owner.uphill_side ?? "left") === "left"; return inletLeft ? { inlet: "좌", outlet: "우" } : { inlet: "우", outlet: "좌" }; }, - // 폼 단 수 ↔ 다단 채널(유출 벽 = extra, 유입·계류측 = bextra) — 좌·우가 각자 쓴다. - setTierCount: (chainageM, role, tiers) => { - const extras = Math.max(0, Math.round(tiers) - 1); - 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, queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch), diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 54868d13..60543377 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -52,10 +52,6 @@ export interface B06StructuresPanelDeps { wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null; /** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */ wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; - /** 폼의 단 수를 다단 채널에 반영한다 — 조정창에서 세우던 것과 같은 자리. */ - setTierCount?: (chainageM: number, role: "inlet" | "outlet", tiers: number) => void; - /** 지금 선 단 수(기준벽 포함) — 폼이 도면과 같은 값을 보이게 한다. */ - tierCount?: (chainageM: number, role: "inlet" | "outlet") => number | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ @@ -117,7 +113,6 @@ 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, @@ -144,9 +139,6 @@ 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); @@ -210,10 +202,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc } // 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야 // 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게). - 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); @@ -277,7 +265,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc pipe.chainage_m, deps.wallHeight, deps.wallForm, - deps.tierCount, ), design_flow_m3s: null, })); @@ -309,7 +296,6 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc hit.chainage_m, deps.wallHeight, deps.wallForm, - deps.tierCount, ); section.setPipeFacilities(pipeFacilities); } From f0af311d4b8b4d4f401835bf3ee56f3cd2774d6d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 19:18:08 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(B06):=20=EC=B6=94=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EC=8A=AD=EB=A7=89=EC=9D=B4=20=EC=B9=B8=EC=9D=84=20=EA=B7=B8=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=B4=20=EB=94=B8=EB=A6=B0=20=EC=AA=BD=20=EC=95=84?= =?UTF-8?q?=EB=9E=98=EB=A1=9C=20=EB=B6=99=EC=9D=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다단은 유출 채널(extra)과 유입·계류측 채널(bextra)로 갈린다. 칸이 늘 유출구 아래 고정이라 어느 쪽 단인지 자리로 읽히지 않았다. 고른 단의 채널을 창이 알려 주고, 칸을 그 쪽(배관 유입구·유출구 / 독립 좌·우) 바로 아래로 옮겨 세운다. Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_UI_Adjust_Dock.ts | 23 ++++++++++++------- .../B06_Section_UI_Cross_Structure_Panel.ts | 2 ++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/B06_Section/B06_Section_UI_Adjust_Dock.ts b/B06_Section/B06_Section_UI_Adjust_Dock.ts index dd3a9c1a..52ddbb81 100644 --- a/B06_Section/B06_Section_UI_Adjust_Dock.ts +++ b/B06_Section/B06_Section_UI_Adjust_Dock.ts @@ -52,6 +52,14 @@ function slotUsable(target: HTMLElement | undefined): target is HTMLElement { return !!group && !group.hidden; } +/** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽을 준다. */ +function slotOf(side: "inlet" | "outlet"): HTMLElement { + const slots = sideSlots!; + return slotUsable(slots[side]) + ? slots[side] + : slots[side === "inlet" ? "revetInlet" : "revetOutlet"]; +} + /** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다. * 표식 없이 지우면 행 DOM이 사라져, 같은 창에서 다른 벽을 고를 때 그 행이 다시는 * 나오지 않는다(2026-08-29 사용자 보고: 단 수·옵션이 사라짐). */ @@ -97,17 +105,16 @@ function relocateSideRows(panelRoot: HTMLElement): void { clearSideSlots(); const side = panelRoot.dataset.side; if (!sideSlots || (side !== "inlet" && side !== "outlet" && side !== "extra")) return; - // 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸 — 지금 서 있는 쪽으로 간다. - const target = - side === "extra" - ? sideSlots.extra - : slotUsable(sideSlots[side]) - ? sideSlots[side] - : sideSlots[side === "inlet" ? "revetInlet" : "revetOutlet"]; - // 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 여기서 세운다. + const target = side === "extra" ? sideSlots.extra : slotOf(side); + // 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 **그 단이 딸린 칸 바로 + // 아래**로 옮겨 세운다(2026-08-30 사용자: 좌/우 각각의 하단으로). if (side === "extra") { const group = target.closest("fieldset"); if (!group) return; + const owner = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet").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); diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index 51a13fdb..dd2c5a01 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -575,6 +575,8 @@ export function buildStructurePanel( // 붙을지 알린다(2026-08-29 지시 4). 다단 벽은 자기 채널 쪽으로 간다. // 다단(추가) 벽은 유입·유출 어느 칸에도 자기 자리가 없다 — 유입구·유출구 아래 // **전용 칸**으로 보낸다(2026-08-30 사용자 지시 4). + // 다단 벽이 어느 쪽 채널의 단인지 — 그 쪽 칸 **바로 아래**에 붙인다(2026-08-30). + root.dataset.extraSide = isExtra ? (key.startsWith("bextra") ? "inlet" : "outlet") : ""; root.dataset.side = isExtra ? "extra" : tierChannel === "outlet" From 4a587012a96b0017d34c0f7201c2cf32078eb400 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 19:21:46 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(B06):=20=EA=B8=B0=EC=8A=AD=EB=A7=89?= =?UTF-8?q?=EC=9D=B4=20=EA=B8=B0=EC=A4=80=EB=B2=BD=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A8=20=EC=88=98=C2=B7=EC=98=B5=EC=85=98=20=ED=96=89?= =?UTF-8?q?=EC=9D=84=20=ED=95=AD=EC=83=81=20=EC=84=B8=EC=9A=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 배관 유입·유출과 독립 좌·우 기준벽에서는 [추가 기슭막이(단)]과 [옵션]이 상태와 무관하게 자리를 지킨다. 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다. 집수정 유입은 종전대로 계류측 단 자리가 있을 때만 뜬다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_Section_UI_Cross_Structure_Panel.ts | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts index dd2c5a01..3349554f 100644 --- a/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts +++ b/B06_Section/B06_Section_UI_Cross_Structure_Panel.ts @@ -525,9 +525,8 @@ export function buildStructurePanel( : movable ? L("B06_Cross_Revet_Inlet") : L("B06_Cross_Struct_InletBasin"); - // 독립 기슭막이(관 숨김)는 유입·유출이 같은 성토 벽이다 — 집수정 형식 선택을 - // 숨기고 유입측에도 단 수 행을 준다(2026-08-29 사용자: 좌우 조작 통일). - const ownTiers = deps.hiddenPipe() && key === "inlet"; + // 독립 기슭막이(관 숨김)는 유입·유출이 같은 성토 벽이다 — 집수정 형식 선택만 숨긴다 + // (단 수 행은 아래에서 기슭막이 기준벽이면 언제나 준다). // 형식 선택은 유입측에서만, 이동·높이 조작은 기슭막이(집수정 제외)만. structureRow.classList.toggle("is-hidden", key !== "inlet" || deps.hiddenPipe()); if (key === "inlet" && !deps.hiddenPipe()) { @@ -594,14 +593,13 @@ export function buildStructurePanel( : tierChannel === "basin" ? deps.basinExtraState() : { canAdd: false, count: 0 }; + // 기슭막이 기준벽(배관 유입·유출, 독립 좌·우)에서는 **항상** 보인다(2026-08-30 + // 사용자). 링크 카드에서 만져도 제어기가 소유 측점 값을 고치므로 안전하다. + // 집수정 유입은 계류측 단이 설 자리가 있을 때만 — 벽이 아니라 구체다. + const revetWall = (key === "inlet" || key === "outlet") && !isBasin; extraRow.classList.toggle( "is-hidden", - deps.isLinkedCard() || - !( - tierChannel === "outlet" || - (tierChannel === "basin" && - (ownTiers || (isBasin && (tiers.canAdd || tiers.count > 0)))) - ), + !revetWall && !(isBasin && tierChannel === "basin" && (tiers.canAdd || tiers.count > 0)), ); currentCount = tiers.count; countValue.textContent = `${tiers.count}단`; @@ -622,7 +620,12 @@ export function buildStructurePanel( linkToggle.button.title = levelled ? LINK_LOCKED_TITLE : LINK_TITLE; gradeToggle.button.classList.toggle("is-hidden", currentSpan === null); gradeToggle.set(!levelled); - optionParts.row.classList.toggle("is-hidden", linkState === null && currentSpan === null); + // 옵션(연동·경사)도 기슭막이 기준벽에서는 항상 자리를 지킨다 — 상태 따라 + // 나타났다 사라지면 아래 행들이 널뛴다(2026-08-30 사용자). + optionParts.row.classList.toggle( + "is-hidden", + !revetWall && linkState === null && currentSpan === null, + ); const adjust = deps.adjustFor(key); // 정보는 항목당 한 행(2026-08-22 사용자) — 0이면 "자동", 좌우는 안/바깥, // 상하는 위/아래로 적는다.