/* ============================================================================= * B06_Section_UI_Adjust_Dock.ts * 좌측 [구조물 배치] 섹션 안 **[횡단 조정] 하위 컨테이너**(2026-08-29 사용자: * B05/B06 일원화 — 오버레이 조정창 항목을 좌측에도 보이되, 기존 폼과 구분되게 * 접이식 하위 컨테이너에 담는다. 9키·십자 위치제어는 오버레이 전용). * * 카드가 조정창을 만들 때 같은 deps로 **사본 창**을 하나 더 만들어 이 슬롯에 * 세운다(adopt). 값·동작이 같은 함수라 오버레이·좌측 어느 쪽을 만져도 카드 * 재렌더에서 두 창이 함께 새 값으로 그려진다 — 별도 동기화 코드가 없다. * 카드가 여럿이어도 슬롯은 하나 — 마지막으로 고른 구조물의 사본만 선다. * ========================================================================== */ let dockRoot: HTMLDetailsElement | null = null; let slot: HTMLElement | null = null; let placeholder: HTMLElement | null = null; /** 유입구·유출구·추가 기슭막이 그룹의 이식 자리(2026-08-29 지시 4, 2026-08-30 지시 4). * B06 패널이 등록한다. */ interface SideSlots { inlet: HTMLElement; outlet: HTMLElement; extra: HTMLElement; /** 독립 기슭막이 좌·우 칸 — 배관 유입구·유출구 칸이 숨어 있을 때 이쪽으로 간다. */ revetInlet: HTMLElement; revetOutlet: HTMLElement; /** 세월교 날개벽(유입·유출) 칸 — 세월교 측점에서는 배관·독립 칸이 모두 숨어 있어 * 이쪽이 그 측의 자리다(2026-08-30 사용자 지시 1). */ wingInlet: HTMLElement; wingOutlet: HTMLElement; } let sideSlots: SideSlots | null = null; /** 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구·추가 기슭막이). */ 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", ].join(", "); /** 유입구·유출구는 폼이 이미 가진 항목 말고 단 수·옵션만 받는다. */ const SIDE_ROWS = [ ".b06-structure-panel__struct--tier", ".b06-structure-panel__struct--options", ].join(", "); /** 이식 자리가 지금 화면에 서 있는지 — 숨겨진 그룹(다른 시설)이면 옮기지 않는다. */ function slotUsable(target: HTMLElement | undefined): target is HTMLElement { if (!target) return false; const group = target.closest("fieldset"); return !!group && !group.hidden; } /** 배관이면 유입구·유출구 칸, 독립 기슭막이면 좌·우 칸, 세월교면 날개벽 칸 — * 지금 서 있는 쪽을 준다(같은 측의 자리가 시설 종류마다 다른 칸에 있다). */ function slotOf(side: "inlet" | "outlet"): HTMLElement { const slots = sideSlots!; const revet = side === "inlet" ? slots.revetInlet : slots.revetOutlet; const wing = side === "inlet" ? slots.wingInlet : slots.wingOutlet; if (slotUsable(slots[side])) return slots[side]; return slotUsable(revet) ? revet : wing; } /** 옮겨 온 행과 그 **제자리 표식** — 되돌릴 때 원래 순서로 꽂기 위해 짝으로 둔다. * 표식 없이 지우면 행 DOM이 사라져, 같은 창에서 다른 벽을 고를 때 그 행이 다시는 * 나오지 않는다(2026-08-29 사용자 보고: 단 수·옵션이 사라짐). */ const movedRows: Array<{ row: HTMLElement; anchor: Comment }> = []; /** 그룹 이름표 — 원래 문구를 한 번만 적어 두고, 선택에 따라 괄호 표기를 붙였다 뗀다. */ function setGroupLabel(group: HTMLElement, suffix: string): void { 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; } /** 옮겨 온 행을 **원래 자리로 되돌리고** 슬롯을 비운다. 그룹 강조도 끈다. */ function clearSideSlots(): void { while (movedRows.length) { const { row, anchor } = movedRows.pop()!; anchor.parentNode?.replaceChild(row, anchor); } for (const target of [ sideSlots?.inlet, sideSlots?.outlet, sideSlots?.extra, sideSlots?.revetInlet, sideSlots?.revetOutlet, sideSlots?.wingInlet, sideSlots?.wingOutlet, ]) { target?.replaceChildren(); const group = target?.closest("fieldset"); if (!group) continue; group.classList.remove("is-active"); setGroupLabel(group, ""); // 추가 기슭막이 칸은 고른 다단 벽이 있을 때만 서 있는다 — 비면 그룹째 내린다. if (target === sideSlots?.extra) group.hidden = true; } } /** 행 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을 * 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다. */ 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 : slotOf(side); // 추가 기슭막이 칸은 평소 숨어 있다 — 고른 벽이 다단이면 **그 단이 딸린 칸 바로 // 아래**로 옮겨 세운다(2026-08-30 사용자: 좌/우 각각의 하단으로). if (side === "extra") { const group = target.closest("fieldset"); if (!group) return; const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet"); const owner = ownerSlot.closest("fieldset"); if (owner && owner !== group) owner.after(group); group.hidden = false; 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; group.classList.add("is-active"); // 이름표에 지금 만지는 구조물을 적는다 — "유입구 (기슭막이)", "유출구 (기슭막이(독립))". // 추가 기슭막이 칸은 이름 자체가 그 단이라 괄호 안에 단 번호가 온다(2026-08-30 지시 1). setGroupLabel(group, panelRoot.dataset.panelTitle ?? ""); // 다단은 칸이 새로 서는 것이라 화면 밖일 수 있다 — 그 자리로 스크롤한다(지시 2). if (side === "extra") group.scrollIntoView({ block: "nearest", behavior: "smooth" }); } function ensureDock(): void { if (dockRoot) return; dockRoot = document.createElement("details"); dockRoot.className = "b06-adjust-dock"; dockRoot.open = true; const summary = document.createElement("summary"); summary.textContent = "횡단 조정"; placeholder = document.createElement("p"); placeholder.className = "b06-adjust-dock__empty"; placeholder.textContent = "횡단도에서 구조물(벽·구체)을 고르면 조정 항목이 여기에 뜹니다."; slot = document.createElement("div"); slot.className = "b06-adjust-dock__slot"; dockRoot.append(summary, placeholder, slot); } /** 하위 컨테이너 루트 — 페이지가 [구조물 배치] 섹션 본문에 붙인다. */ export function adjustDockRoot(): HTMLElement { ensureDock(); return dockRoot!; } /** 페이지 진입 시 초기 상태(빈 슬롯 + 안내문)로 되돌린다. */ export function resetAdjustDock(): void { ensureDock(); slot!.replaceChildren(); clearSideSlots(); placeholder!.hidden = false; } /** 조정창 사본을 슬롯에 세운다 — 이미 다른 사본이 있으면 갈아 끼운다. */ export function adoptAdjustPanel(panelRoot: HTMLElement): void { ensureDock(); panelRoot.classList.add("b06-structure-panel--dock"); if (slot!.firstChild !== panelRoot || slot!.childNodes.length !== 1) { slot!.replaceChildren(panelRoot); } placeholder!.hidden = true; relocateSideRows(panelRoot); } /** * 이식을 **다시 한 번** 돌린다 — 폼이 다른 시설로 갈아 끼워진 뒤에 부른다. * 벽 선택은 카드가 먼저 처리하고 폼 교체가 뒤따르므로, 처음 이식할 때는 목적지 * 칸(배관 유입구·유출구 / 독립 좌·우)이 아직 이전 시설 것이다(2026-08-30 사용자: * 다른 기슭막이를 고른 뒤 배관 유출을 고르면 강조가 없다). */ export function refreshAdjustSlots(): void { const panel = slot?.firstElementChild as HTMLElement | null; if (panel) relocateSideRows(panel); } /** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */ export function releaseAdjustPanel(panelRoot: HTMLElement): void { if (!slot || !slot.contains(panelRoot)) return; slot.replaceChildren(); clearSideSlots(); placeholder!.hidden = false; }