B05/B06 인터페이스 일원화 1차 (2026-08-29 사용자 확정 계획): - A00_Common/b_structures_section.ts: B05 구조물 배치 폼·목록 공용 진입점 (재수출 + 스타일 동봉) - B06 좌측 패널에 [구조물 배치] 섹션 + 하단 dock [목록][구분선][저장버튼] (B05 동일 템플릿). 구조물 조작은 세션 pending으로 쌓여 기존 flushPendingStructures([임시저장]·[확정])로 정본 반영 - 선택 연동: 목록·폼 → 최근접 카드 focusStation, 횡단도 벽·세월교·BOX → 좌측 폼에 소유 측점 시설 로드(wireStructureSelection) - [횡단 조정] 하위 컨테이너(Adjust_Dock): 오버레이 조정창을 같은 deps로 사본 인스턴스 생성해 세움 — 로직 이동 0, 카드 재렌더로 자동 동기. 9키·십자 행은 사본에서 숨김(사용자 지시 7), 방향키 이중 입력 방지 - A군 시설 추가·이동·삭제는 B06에서 안내 토스트(배수유역 체인은 B05 몫) 검증: 공용 브라우저 — 구간값 길이 사본 + 10.0→11.0m 양측 동기, 오버레이 - 원복, ✕ 닫기 시 사본 해제. B05 회귀 없음. typecheck 통과.
157 lines
6.8 KiB
TypeScript
157 lines
6.8 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Page_Structures_Panel.ts
|
|
* B06 좌측 「구조물 배치」 — B05와 **같은 컨테이너·목록 템플릿**을 세운다
|
|
* (2026-08-29 사용자: B05/B06 인터페이스 일원화. 페이지 본체는 700줄 제한으로
|
|
* 여기서 조립만 받아 간다).
|
|
*
|
|
* · 폼·목록 구현 = 공용 진입점(`A00_Common/b_structures_section`) — B05 그대로.
|
|
* · 구조물(B~G군) 추가·수정·삭제는 세션 미저장분(`writePendingStructures`)에 쌓고
|
|
* [임시저장]·[확정]의 `flushPendingStructures`가 정본에 쓴다(캐시→저장 원칙).
|
|
* · 계곡 통과 시설(A군, pipe_points 정본)은 **표시·선택만** — 추가·이동·삭제는
|
|
* 배수유역 재분할 체인이 있는 B05 몫이라 안내만 한다(2026-08-29 1차 범위).
|
|
* · 목록·폼 선택 → 해당 측점 카드 선택·스크롤. 횡단도 벽·구체 선택 → 폼에 그 시설
|
|
* 로드(소유 측점 기준). 하위 [횡단 조정] 컨테이너는 Adjust_Dock이 맡는다.
|
|
* ========================================================================== */
|
|
|
|
import { createStructuresSection, type PipeFacilityItem } from "../A00_Common/b_structures_section";
|
|
import {
|
|
fetchStructures,
|
|
fetchStructureTypes,
|
|
readPendingStructures,
|
|
structureAnchorM,
|
|
writePendingStructures,
|
|
type StructureInstance,
|
|
} from "../B05_Profile/B05_Profile_Api_Structures";
|
|
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { adjustDockRoot, resetAdjustDock } from "./B06_Section_UI_Adjust_Dock";
|
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
|
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
|
|
|
|
const PIPE_GUIDE = "계곡 통과 시설의 추가·이동·삭제는 B05(종단) 화면에서 합니다.";
|
|
|
|
export interface B06StructuresPanelDeps {
|
|
projectId: string | null;
|
|
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
|
stationInterval: () => number;
|
|
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
|
focusChainage: (chainageM: number) => void;
|
|
}
|
|
|
|
export interface B06StructuresPanel {
|
|
/** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */
|
|
root: HTMLElement;
|
|
/** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */
|
|
listRoot: HTMLElement;
|
|
/** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */
|
|
load: () => Promise<void>;
|
|
/** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */
|
|
showPipeAt: (chainageM: number | null) => void;
|
|
}
|
|
|
|
export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel {
|
|
let structures: StructureInstance[] = [];
|
|
|
|
/** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야
|
|
* 하고, 서버는 빈 값일 때만 새로 발급한다(B05 다리와 같은 규칙). */
|
|
const withLocalIds = (next: StructureInstance[]): StructureInstance[] =>
|
|
next.map((item) =>
|
|
item.structure_id ? item : { ...item, structure_id: crypto.randomUUID().replace(/-/g, "") },
|
|
);
|
|
|
|
const section = createStructuresSection({
|
|
onChange: (next) => {
|
|
structures = withLocalIds(next);
|
|
section.setStructures(structures);
|
|
if (deps.projectId) writePendingStructures(deps.projectId, structures);
|
|
},
|
|
onSelect: (structure) => {
|
|
if (structure) deps.focusChainage(structureAnchorM(structure));
|
|
},
|
|
getInterval: deps.stationInterval,
|
|
onPipeAdd: () => showToast(PIPE_GUIDE, "error"),
|
|
onPipeUpdate: () => showToast(PIPE_GUIDE, "error"),
|
|
onPipeRemove: () => showToast(PIPE_GUIDE, "error"),
|
|
onPipeSelect: (chainageM) => {
|
|
if (chainageM !== null) deps.focusChainage(chainageM);
|
|
},
|
|
});
|
|
|
|
// 하위 [횡단 조정] 컨테이너 — 폼 본문 맨 아래, 기존 항목과 경계 구분(2026-08-29
|
|
// 사용자: 병합 항목은 하위 컨테이너로 담아 혼동 방지).
|
|
resetAdjustDock();
|
|
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
|
|
|
|
async function load(): Promise<void> {
|
|
if (!deps.projectId) return;
|
|
const projectId = deps.projectId;
|
|
try {
|
|
const [types, stored, pipeResponse] = await Promise.all([
|
|
fetchStructureTypes(),
|
|
fetchStructures(projectId),
|
|
fetchDetailPipePoints(projectId),
|
|
]);
|
|
section.setTypes(types);
|
|
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙).
|
|
structures = readPendingStructures(projectId) ?? stored.structures;
|
|
section.setStructures(structures);
|
|
const pipes: PipeFacilityItem[] = pipeResponse.pipe_points.map((pipe) => ({
|
|
chainage_m: pipe.chainage_m,
|
|
facility: pipe.facility ?? "pipe",
|
|
start_m: pipe.start_m,
|
|
end_m: pipe.end_m,
|
|
source: pipe.source,
|
|
options: pipe.options,
|
|
design_flow_m3s: null,
|
|
}));
|
|
section.setPipeFacilities(pipes);
|
|
} catch (error) {
|
|
showToast(
|
|
error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.",
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
root: section.root,
|
|
listRoot: section.listRoot,
|
|
load,
|
|
showPipeAt: (chainageM) => section.selectPipeByChainage(chainageM),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그
|
|
* 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계).
|
|
* 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다.
|
|
*/
|
|
export function wireStructureSelection(
|
|
stationControls: StationControls,
|
|
detail: () => SectionDetailResponse | null,
|
|
panel: B06StructuresPanel,
|
|
): 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;
|
|
};
|
|
|
|
const origRevet = stationControls.revetOffset.select;
|
|
stationControls.revetOffset.select = (chainageM, key) => {
|
|
origRevet(chainageM, key);
|
|
panel.showPipeAt(key ? ownerChainageOf(chainageM) : null);
|
|
};
|
|
const origFord = stationControls.ford.select;
|
|
stationControls.ford.select = (chainageM, role) => {
|
|
origFord(chainageM, role);
|
|
panel.showPipeAt(role ? chainageM : null);
|
|
};
|
|
const origBox = stationControls.box.select;
|
|
stationControls.box.select = (chainageM, role) => {
|
|
origBox(chainageM, role);
|
|
panel.showPipeAt(role ? chainageM : null);
|
|
};
|
|
}
|