Files
Aislo/B06_Section/B06_Section_UI_Page_Structures_Selection.ts
T
eomsangdonandClaude Opus 5.5 a6e7a14217 refactor(b06): 구조물 패널 선택 배선 떼기 — wireStructureSelection 새 파일(152-1)
- B06_Section_UI_Page_Structures_Panel.ts 699 → 540줄 · 동작 그대로
- 선택 배선 = B06_Section_UI_Page_Structures_Selection.ts
- 문자열 시험 3개 새 파일 읽기로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-09 19:01:18 +09:00

169 lines
8.3 KiB
TypeScript

/* =============================================================================
* B06_Section_UI_Page_Structures_Selection.ts
* 횡단도 선택 → 좌측 폼 연동 배선(`wireStructureSelection`) — `_Page_Structures_Panel.ts`
* 에서 떼어 냄(700줄). 폼 · 목록 패널 자체는 그 파일 몫.
* ========================================================================== */
import { refreshAdjustSlots } from "./B06_Section_UI_Adjust_Dock";
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
import type { RevetPick } from "./B06_Section_UI_Cross_Culvert";
import type { B06StructuresPanel } from "./B06_Section_UI_Page_Structures_Panel";
/**
* 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그
* 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계).
* 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다.
*/
/** 지금 처리 중인 선택이 **도면에서 벽·구체를 찍은 것**인가 — 카드 선택 리스너가
* 이 값으로 "카드만 고른 경우"를 가려낸다(2026-08-29 사용자: 측점 선택 때 구조물이
* 자동으로 골라지면 안 된다). 같은 턴에서만 참이다. */
let wallSelecting = false;
export function isWallSelecting(): boolean {
return wallSelecting;
}
function markWallSelecting(): void {
wallSelecting = true;
queueMicrotask(() => {
wallSelecting = false;
});
}
export function wireStructureSelection(
stationControls: StationControls,
detail: () => SectionDetailResponse | null,
panel: B06StructuresPanel,
/** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가
* 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */
remember?: (chainageM: number, key: string) => void,
/** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */
refreshCard?: (chainageM: number) => void,
/** 폼에 올린 시설로 보기를 옮김 — 목록에서 고른 길(54-47). */
reveal?: (chainageM: number) => void,
): { syncInletStructure: () => void } {
const ownerChainageOf = (chainageM: number): number => {
const sectionAt = detail()?.cross_sections.find(
(entry) => Math.abs(entry.chainage_m - chainageM) < 0.01,
);
return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM;
};
// 유입구 "구조" ↔ B06 유입측 형식 — 폼에서 고르면 조정값에 반영하고, 시설을 폼에
// 올릴 때는 조정값을 폼으로 되읽는다(2026-08-29 지시 5 병합).
const sectionAt = (chainageM: number) =>
detail()?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01);
panel.facility.onInletStructureChange((value) => {
const chainageM = panel.currentChainage();
if (chainageM === null) return;
stationControls.inletStructure.set(chainageM, value);
});
/** 폼에 올라온 시설의 유입측 형식을 폼 드롭다운에 되싣는다. */
const syncInletStructure = (): void => {
const chainageM = panel.currentChainage();
const target = chainageM === null ? undefined : sectionAt(chainageM);
if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target));
};
const origRevet = stationControls.revetOffset.select;
stationControls.revetOffset.select = (chainageM, key) => {
// 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1)
const nested = wallSelecting;
// 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1)
if (key) markWallSelecting();
origRevet(chainageM, key);
if (key) remember?.(ownerChainageOf(chainageM), key);
if (key) panel.showAtChainage(ownerChainageOf(chainageM));
else if (!nested) {
remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림
panel.releasePipe();
}
if (key) syncInletStructure();
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다.
refreshAdjustSlots();
};
// 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30)
const keyOf = (box: HTMLElement): RevetPick | null => {
const id = box.dataset.specContainer;
if (id === "inlet" || id === "outlet" || id === "pipe") return id;
return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null;
};
const pickBox = (box: HTMLElement): void => {
const chainageM = panel.currentChainage();
const target = chainageM === null ? undefined : sectionAt(chainageM);
const key = keyOf(box);
if (!key || !target?.culvert || stationControls.revetOffset.highlightFor(target) === key)
return;
stationControls.revetOffset.select(target.chainage_m, key);
refreshCard?.(target.chainage_m);
};
// 그 측점 카드 · 알약으로 감(목록에서 고른 길 · 54-47) — 같은 칸을 잇달아 고치면 한 번만(다시 그려도 칸 이름으로)
const fieldOf = (el: HTMLElement): string => {
const box = el.closest<HTMLElement>("fieldset[data-spec-container]");
const item = el.closest<HTMLElement>("[data-spec-item]")?.dataset.specItem;
return `${box?.dataset.specContainer}:${box?.dataset.specIndex}:${item}`;
};
let revealed: string | null = null;
const revealAt = (chainageM: number, field: string | null): void => {
if (field && field === revealed) return;
revealed = field;
reveal?.(chainageM);
};
panel.root.addEventListener("focusin", (event) => {
if (fieldOf(event.target as HTMLElement) !== revealed) revealed = null;
});
panel.root.addEventListener("change", ({ target: el }) => {
const box = (el as HTMLElement).closest<HTMLElement>("fieldset[data-spec-container]");
const chainageM = panel.currentChainage();
if (!box || chainageM === null) return;
revealAt(chainageM, fieldOf(el as HTMLElement));
// 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지) — 새 단은 다시 그린 뒤 아래에서 고름
if (!(el as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) pickBox(box);
});
// 상자가 새로 서면(단 늘림 · [추가]) 그 상자를 고름 — 같은 시설을 다시 그린 때만
let painted: { chainageM: number | null; keys: Set<string> } | null = null;
panel.root.addEventListener("specpaint", () => {
const chainageM = panel.currentChainage();
const boxes = [...panel.root.querySelectorAll<HTMLElement>("fieldset[data-spec-container]")];
const before = painted?.chainageM === chainageM ? painted.keys : null;
painted = { chainageM, keys: new Set(boxes.map(fieldOf)) };
const born = before ? boxes.filter((box) => !before.has(fieldOf(box))).at(-1) : undefined;
if (!born || chainageM === null) return;
revealAt(chainageM, null);
pickBox(born);
});
const origFord = stationControls.ford.select;
stationControls.ford.select = (chainageM, role) => {
const nested = wallSelecting;
if (role) markWallSelecting();
origFord(chainageM, role);
if (role) remember?.(chainageM, role);
if (role) panel.showAtChainage(chainageM);
else if (!nested) {
remember?.(chainageM, "");
panel.releasePipe();
}
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다
// (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1).
refreshAdjustSlots();
};
const origBox = stationControls.box.select;
stationControls.box.select = (chainageM, role) => {
const nested = wallSelecting;
if (role) markWallSelecting();
origBox(chainageM, role);
if (role) remember?.(chainageM, role);
if (role) panel.showAtChainage(chainageM);
else if (!nested) {
remember?.(chainageM, "");
panel.releasePipe();
}
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다.
refreshAdjustSlots();
};
return { syncInletStructure };
}