Files
Aislo/B06_Section/B06_Section_UI_Adjust_Dock.ts
T
eomsangdon 5d0d2f0918 feat(B05,B06): 구조물 배치를 실시간 반영으로 바꾸고 이식 행·강조를 정리한다
2026-08-29 사용자 지시 3건:
1. 유입구·유출구로 옮겨 온 단 수·옵션 행을 폼 양식(라벨+한 덩어리 컨트롤)으로
   맞추고 2열 격자로 세운다.
2. 이미 고른 항목은 값이 바뀔 때마다 곧바로 반영한다([수정] 버튼 불필요).
   버튼은 visibility만 꺼 자리를 남겨 삭제·리셋의 위치·크기가 흔들리지 않게 했다.
   정본 주입이 되먹임으로 돌아와도 편집 중인 폼은 닫지 않는다.
3. 도면에서 구조물을 고르면 그쪽 그룹(유입구/유출구)을 붉은 계열로 강조한다.
   카드만 고른 경우는 강조하지 않는다 — 대표 벽은 조용히 딸려 오고, 그 벽을
   다시 눌러 선택을 풀면 강조만 꺼지고 행은 남는다.

검증: 슬롯 grid 2열, 카드 선택 시 강조 없음·행 2개, 다른 벽 클릭 시 유입구 그룹
붉은 테두리(rgb(192,57,43)), 재클릭 시 강조 해제·행 복귀. 길이 + 즉시 캐시
반영(11) 후 - 원복(10), 폼 유지. typecheck 통과.
2026-08-29 16:00:04 +09:00

121 lines
5.1 KiB
TypeScript

/* =============================================================================
* 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). B06 패널이 등록한다. */
let sideSlots: { inlet: HTMLElement; outlet: HTMLElement } | null = null;
/** 단 수·옵션 행을 옮겨 붙일 자리를 등록한다(계곡 통과 시설 폼의 유입구·유출구). */
export function setAdjustSideSlots(next: { inlet: HTMLElement; outlet: HTMLElement }): void {
sideSlots = next;
}
/** 지금 붙는 사본이 **자동 선택**(카드를 골라 대표 벽이 딸려 온 경우)인가.
* 자동이면 그룹 강조를 켜지 않는다 — 강조는 도면에서 구조물을 직접 고를 때만
* 쓴다(2026-08-29 사용자 지시). */
let autoAdopt = false;
export function withAutoAdopt(run: () => void): void {
autoAdopt = true;
try {
run();
} finally {
autoAdopt = false;
}
}
/** 이식 자리가 지금 화면에 서 있는지 — 숨겨진 그룹(다른 시설)이면 옮기지 않는다. */
function slotUsable(target: HTMLElement | undefined): target is HTMLElement {
if (!target) return false;
const group = target.closest("fieldset");
return !!group && !group.hidden;
}
/** 옮겨 놨던 행을 비운다 — 사본이 갈릴 때마다 옛 행을 남기지 않는다. 그룹 강조도 끈다. */
function clearSideSlots(): void {
for (const target of [sideSlots?.inlet, sideSlots?.outlet]) {
target?.replaceChildren();
target?.closest("fieldset")?.classList.remove("is-active");
}
}
/**
* 조정창 사본의 단 수·옵션 행을 고른 벽 쪽(유입/유출) 그룹으로 옮긴다. 행 DOM을
* 그대로 옮기므로 배선·값 동기는 손대지 않는다. 자리가 없으면 사본에 그대로 둔다.
*/
function relocateSideRows(panelRoot: HTMLElement): void {
clearSideSlots();
const side = panelRoot.dataset.side;
if (!sideSlots || (side !== "inlet" && side !== "outlet")) return;
const target = sideSlots[side];
if (!slotUsable(target)) return;
const rows = panelRoot.querySelectorAll<HTMLElement>(
".b06-structure-panel__struct--tier, .b06-structure-panel__struct--options",
);
rows.forEach((row) => {
if (row.classList.contains("is-hidden")) return;
target.append(row);
});
// 도면에서 직접 고른 구조물만 그룹 테두리로 알린다(2026-08-29 사용자 지시 3).
if (!autoAdopt) target.closest("fieldset")?.classList.add("is-active");
}
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);
}
/** 이 사본이 슬롯에 서 있으면 내린다 — 다른 사본이 이미 대신 서 있으면 손대지 않는다. */
export function releaseAdjustPanel(panelRoot: HTMLElement): void {
if (!slot || !slot.contains(panelRoot)) return;
slot.replaceChildren();
clearSideSlots();
placeholder!.hidden = false;
}