좌측 폼의 세월교 상세 항목(관종·관경·수량·월류 폭·날개벽 유입/유출)이 도면에 전혀 반영되지 않았다. `applyPipeOptionsToCache`가 `section.culvert`만 보고 세월교 측점(`section.ford`)에서는 즉시 return했기 때문이다. 프론트는 구조물 배치의 상세 UI를 쓰고 로직은 조정창 제어기 것을 쓴다 (2026-08-30 사용자 확정). - `applyFordFormOptions()` 신설 — 폼 옵션 키를 `FordControl.setPipe`/`setWing` 인자로 옮긴다. 캐시 반영·바닥판 연장 재계산·정본 예약·카드 갱신은 제어기에 이미 있어 새로 만들지 않았다. - `FordControl.setPipe` 확장 — `pipe_kind`(관종)·`ford_width_m`(월류 폭 = `span_m`) 추가. 두 값은 조정창에 칸이 없고 폼만 보낸다. - `withFordSpec()` — 폼 기본값을 `section.ford` 스펙에서 읽는다. 두 창이 같은 스펙을 보므로 어느 쪽에서 만졌든 값이 갈리지 않는다(수량(련) 빈 칸 해소). 월류 높이(`ford_height_m`)는 백엔드 `_ford_set`이 읽지 않는다 — 세월교는 구체 위 노면이라 파임이 없고, 물넘이포장 전용 값이다. 폼에는 남되 도면 반영처는 없다. 자체검증(공용 브라우저 5174, 측점 7+9.7 세월교): - tsc --noEmit 통과. - 폼 날개 길이(유입) 2→4 → 조정창 「날개 길이 4.0m」, 바닥판 7.63m→9.04m (= +2×cos45°). 수정 전에는 둘 다 그대로였다. - 폼 수량 3 → 조정창 「3련」, 폼 관경 Ø1200 → 조정창 select 1200. - 재선택 왕복: 월류폭 12·수량 3·관경 1200·날개길이 4 그대로 되읽힘(스펙 정본). - 진입 시 수량(련) 칸 = 1(수정 전 빈 칸), 관종 파형강관. - 조작값은 원래 값(10/1/1000/2)으로 되돌려 놓았다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
453 lines
22 KiB
TypeScript
453 lines
22 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,
|
|
refreshAdjustSlots,
|
|
resetAdjustDock,
|
|
setAdjustSideSlots,
|
|
} 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_ADD_GUIDE = "계곡 통과 시설의 추가·삭제는 B05(종단) 화면에서 합니다.";
|
|
const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다.";
|
|
/** 고른 것 없이 폼만 만졌을 때 — 값이 어디로도 가지 않으므로 이유를 알린다. */
|
|
const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세요 — 고른 것에만 값이 반영됩니다.";
|
|
const PIPE_MOVE_NOTICE =
|
|
"기준 측점을 옮겼습니다 — 세부 배수유역과 횡단도는 [저장]·[확정] 뒤에 다시 계산됩니다.";
|
|
|
|
export interface B06StructuresPanelDeps {
|
|
projectId: string | null;
|
|
/** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다
|
|
* (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */
|
|
detail?: () => SectionDetailResponse | null;
|
|
/** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본
|
|
* 높이도 이 값을 쓴다(조정창 표시값과 같다). */
|
|
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null;
|
|
/** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */
|
|
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null;
|
|
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
|
|
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
|
|
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
|
|
stationInterval: () => number;
|
|
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
|
|
focusChainage: (chainageM: number) => void;
|
|
/** 폼 [수정]으로 바뀐 관 옵션을 캐시에 예약한다 — [저장]·[확정]이 정본에 쓴다
|
|
* (2026-08-29: 조정창 구간값과 같은 경로). 없으면 안내만 한다. */
|
|
queuePipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
|
/** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 —
|
|
* 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */
|
|
applyPipeOptions?: (chainageM: number, patch: Record<string, number | string>) => void;
|
|
/** 기준 측점 이동을 예약한다 — 세부 배수유역 재분할은 [저장]·[확정] 때 서버가
|
|
* 관 목록을 다시 받아 처리한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). */
|
|
movePipe?: (fromChainageM: number, toChainageM: number) => void;
|
|
}
|
|
|
|
export interface B06StructuresPanel {
|
|
/** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */
|
|
root: HTMLElement;
|
|
/** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */
|
|
listRoot: HTMLElement;
|
|
/** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */
|
|
load: () => Promise<void>;
|
|
/** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */
|
|
showPipeAt: (chainageM: number | null) => void;
|
|
/** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인
|
|
* 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */
|
|
overrideOptions: (chainageM: number, patch: Record<string, string | number>) => void;
|
|
/** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고,
|
|
* 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). */
|
|
showAtChainage: (chainageM: number | null) => void;
|
|
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
|
|
currentChainage: () => number | null;
|
|
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
|
|
facility: {
|
|
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
|
|
onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void;
|
|
};
|
|
}
|
|
|
|
/** 관 매칭 허용 오차(m) — 관은 계획선에 스냅돼 측점과 살짝 어긋난다(B05와 같은 기준). */
|
|
const PIPE_MATCH_M = 0.51;
|
|
|
|
/** 이 구조물이 그 측점을 덮는가 — 점형은 근사 일치, 구간형은 시작~종료 안. */
|
|
function coversChainage(structure: StructureInstance, chainageM: number): boolean {
|
|
if (structure.placement === "interval" && structure.start_m != null && structure.end_m != null) {
|
|
return chainageM >= structure.start_m - 0.01 && chainageM <= structure.end_m + 0.01;
|
|
}
|
|
return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M;
|
|
}
|
|
|
|
/**
|
|
* 세월교 폼 옵션을 **지금 그려진 스펙**(`section.ford`)으로 덮는다 — 조정창 조작이
|
|
* 이 스펙을 제자리에서 고치므로, 폼이 저장된 옵션을 붙들면 두 창이 갈린다
|
|
* (2026-08-30 사용자: 수량(련) 칸이 비어 있고 조정창은 1련이었다).
|
|
*/
|
|
function withFordSpec(
|
|
options: Record<string, string | number> | undefined,
|
|
ford: NonNullable<SectionDetailResponse["cross_sections"][number]["ford"]>,
|
|
): Record<string, string | number> {
|
|
const merged: Record<string, string | number> = { ...(options ?? {}) };
|
|
if (ford.pipe_kind) merged.pipe_kind = ford.pipe_kind;
|
|
merged.pipe_diameter_mm = Math.round(ford.diameter_m * 1000);
|
|
merged.pipe_count = ford.pipe_count;
|
|
merged.ford_width_m = ford.span_m;
|
|
for (const [wing, prefix] of [
|
|
[ford.wing_in, "wing_in"],
|
|
[ford.wing_out, "wing_out"],
|
|
] as const) {
|
|
merged[prefix] = wing.installed ? "있음" : "없음";
|
|
if (wing.height_m !== null) merged[`${prefix}_height_m`] = wing.height_m;
|
|
if (wing.length_m !== null) merged[`${prefix}_length_m`] = wing.length_m;
|
|
if (wing.angle_deg !== null) merged[`${prefix}_angle_deg`] = wing.angle_deg;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* 관 지점 옵션에 **횡단 스펙 값**을 채워 넣는다 — 조정창이 보여주던 값과 폼이
|
|
* 어긋나지 않게 한다(2026-08-29 사용자). 옵션에 이미 값이 있으면 그대로 두고,
|
|
* 비어 있을 때만 스펙(서버 계산·저장분)에서 가져온다.
|
|
*/
|
|
function withSpecDefaults(
|
|
options: Record<string, string | number> | undefined,
|
|
detail: SectionDetailResponse | null,
|
|
chainageM: number,
|
|
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null,
|
|
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null,
|
|
): Record<string, string | number> | undefined {
|
|
const owner = detail?.cross_sections.find(
|
|
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
|
|
);
|
|
// 세월교는 폼과 조정창이 **같은 캐시**(`section.ford`)를 본다 — 어느 쪽에서 만졌든
|
|
// 그 스펙이 지금 그려진 값이라 저장된 옵션보다 앞선다(2026-08-30 사용자: 프론트는
|
|
// 구조물 배치 상세 UI, 로직·값은 조정창 것).
|
|
if (owner?.ford) return withFordSpec(options, owner.ford);
|
|
const culvert = owner?.culvert;
|
|
if (!culvert) return options;
|
|
const merged: Record<string, string | number> = { ...(options ?? {}) };
|
|
const put = (key: string, value: string | number | null | undefined): void => {
|
|
if (merged[key] === undefined && value !== null && value !== undefined) merged[key] = value;
|
|
};
|
|
put("pipe_kind", culvert.pipe_kind);
|
|
put("pipe_diameter_mm", Math.round(culvert.diameter_m * 1000));
|
|
for (const [side, prefix] of [
|
|
[culvert.inlet, "inlet"],
|
|
[culvert.outlet, "outlet"],
|
|
] as const) {
|
|
put(`${prefix}_type`, side.structure);
|
|
// 형태·높이는 **지금 그려진 값**이 이긴다 — 조정값(조작)이 저장된 옵션보다
|
|
// 앞서기 때문이다. 저장값을 그대로 두면 폼과 그림이 갈린다(2026-08-30 사용자:
|
|
// 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다).
|
|
const drawnForm = wallForm?.(chainageM, prefix);
|
|
if (drawnForm) merged[`${prefix}_revet_form`] = drawnForm;
|
|
else put(`${prefix}_revet_form`, side.revet_form);
|
|
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);
|
|
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);
|
|
}
|
|
put("inlet_basin_length_m", culvert.inlet.basin_length_m);
|
|
put("inlet_basin_before_m", culvert.inlet.basin_before_m);
|
|
put("inlet_basin_after_m", culvert.inlet.basin_after_m);
|
|
return merged;
|
|
}
|
|
|
|
export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel {
|
|
let structures: StructureInstance[] = [];
|
|
let pipeFacilities: PipeFacilityItem[] = [];
|
|
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리 — 유입측 형식 반영 대상. */
|
|
let currentChainageM: number | null = null;
|
|
|
|
/** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야
|
|
* 하고, 서버는 빈 값일 때만 새로 발급한다(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_ADD_GUIDE, "error"),
|
|
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
|
|
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
|
|
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
|
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
|
|
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
|
|
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
|
|
const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M;
|
|
if (moved) {
|
|
if (deps.movePipe) {
|
|
deps.movePipe(fromChainageM, toChainageM);
|
|
const hit = pipeFacilities.find(
|
|
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
|
);
|
|
if (hit) hit.chainage_m = toChainageM;
|
|
currentChainageM = toChainageM;
|
|
section.setPipeFacilities(pipeFacilities);
|
|
showToast(PIPE_MOVE_NOTICE, "success");
|
|
} else {
|
|
showToast(PIPE_MOVE_GUIDE, "error");
|
|
}
|
|
}
|
|
const patch = attributes.options;
|
|
if (!patch || !Object.keys(patch).length) return;
|
|
if (!deps.queuePipeOptions) {
|
|
showToast(PIPE_ADD_GUIDE, "error");
|
|
return;
|
|
}
|
|
// 단 수는 옵션이자 **조작 채널**이다 — 조정창에서 세우던 그 자리로 보내야
|
|
// 횡단도에 단이 선다(2026-08-30 사용자 지시 3: 배수관 측점과 동일하게).
|
|
// 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다.
|
|
deps.queuePipeOptions(fromChainageM, patch as Record<string, number | string>);
|
|
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
|
|
// 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.
|
|
const hit = pipeFacilities.find(
|
|
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
|
|
);
|
|
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
|
section.setPipeFacilities(pipeFacilities);
|
|
},
|
|
onPipeRemove: () => showToast(PIPE_ADD_GUIDE, "error"),
|
|
onPipeSelect: (chainageM) => {
|
|
if (chainageM !== null) deps.focusChainage(chainageM);
|
|
},
|
|
});
|
|
|
|
// 고른 것 없이 좌측 폼을 만지면 값이 아무 데도 가지 않는다(패널 실시간 반영이
|
|
// 선택된 항목에만 걸린다) — 조용히 무시되던 자리라 이유를 알린다(2026-08-30 사용자:
|
|
// "형태에 따라 모양을 넣어주는 기능이 누락되는 경우가 있어"의 원인).
|
|
section.facility.root.addEventListener("change", () => {
|
|
if (!section.hasSelection()) showToast(PIPE_PICK_GUIDE, "error");
|
|
});
|
|
|
|
// 하위 [횡단 조정] 컨테이너 — 폼 본문 맨 아래, 기존 항목과 경계 구분(2026-08-29
|
|
// 사용자: 병합 항목은 하위 컨테이너로 담아 혼동 방지).
|
|
resetAdjustDock();
|
|
section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot());
|
|
// 단 수·옵션 행은 유입구·유출구 그룹으로 옮겨 붙는다(2026-08-29 지시 4).
|
|
setAdjustSideSlots({
|
|
inlet: section.facility.inletSlot,
|
|
outlet: section.facility.outletSlot,
|
|
extra: section.facility.extraSlot,
|
|
revetInlet: section.facility.revetInletSlot,
|
|
revetOutlet: section.facility.revetOutletSlot,
|
|
});
|
|
|
|
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 detail = deps.detail?.() ?? null;
|
|
pipeFacilities = 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: withSpecDefaults(
|
|
pipe.options,
|
|
detail,
|
|
pipe.chainage_m,
|
|
deps.wallHeight,
|
|
deps.wallForm,
|
|
),
|
|
design_flow_m3s: null,
|
|
}));
|
|
section.setPipeFacilities(pipeFacilities);
|
|
} catch (error) {
|
|
showToast(
|
|
error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.",
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
|
|
/** 폼에 올리기 직전에 좌·우 이름표를 그 측점 방향으로 맞춘다(독립 기슭막이 칸). */
|
|
function syncSideLabels(chainageM: number): void {
|
|
const labels = deps.wallSideLabels?.(chainageM);
|
|
if (labels) section.facility.setRevetSideLabels(labels);
|
|
}
|
|
|
|
/** 폼에 올리기 직전에 그 관의 옵션을 최신 스펙으로 보강한다(빈 칸만 채운다). */
|
|
function refreshSpecDefaults(chainageM: number): void {
|
|
const hit = pipeFacilities.find(
|
|
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
|
|
);
|
|
if (!hit) return;
|
|
syncSideLabels(hit.chainage_m);
|
|
hit.options = withSpecDefaults(
|
|
hit.options,
|
|
deps.detail?.() ?? null,
|
|
hit.chainage_m,
|
|
deps.wallHeight,
|
|
deps.wallForm,
|
|
);
|
|
section.setPipeFacilities(pipeFacilities);
|
|
}
|
|
|
|
return {
|
|
root: section.root,
|
|
listRoot: section.listRoot,
|
|
load,
|
|
currentChainage: () => currentChainageM,
|
|
facility: {
|
|
setInletStructure: (value) => section.facility.setInletStructure(value),
|
|
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
|
|
},
|
|
overrideOptions: (chainageM, patch) => {
|
|
const hit = pipeFacilities.find(
|
|
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
|
|
);
|
|
if (!hit) return;
|
|
hit.options = { ...(hit.options ?? {}), ...patch };
|
|
section.setPipeFacilities(pipeFacilities);
|
|
// 폼을 그 값으로 다시 세운다 — 조정창이 한계를 알려 주던 것과 같은 자리다.
|
|
section.selectPipeByChainage(hit.chainage_m);
|
|
},
|
|
showPipeAt: (chainageM) => {
|
|
currentChainageM = chainageM;
|
|
if (chainageM !== null) refreshSpecDefaults(chainageM);
|
|
section.selectPipeByChainage(chainageM);
|
|
},
|
|
showAtChainage: (chainageM) => {
|
|
currentChainageM = null;
|
|
if (chainageM === null) {
|
|
section.selectById(null);
|
|
return;
|
|
}
|
|
// 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에
|
|
// 그려지는 것은 이쪽이다.
|
|
const pipe = pipeFacilities.find(
|
|
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
|
|
);
|
|
if (pipe) {
|
|
currentChainageM = pipe.chainage_m;
|
|
refreshSpecDefaults(pipe.chainage_m);
|
|
section.selectPipeByChainage(pipe.chainage_m);
|
|
return;
|
|
}
|
|
const hit = structures.find((entry) => coversChainage(entry, chainageM));
|
|
section.selectById(hit?.structure_id ?? null);
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·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,
|
|
): { 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) => {
|
|
markWallSelecting();
|
|
origRevet(chainageM, key);
|
|
panel.showPipeAt(key ? ownerChainageOf(chainageM) : null);
|
|
if (key) syncInletStructure();
|
|
// 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다.
|
|
refreshAdjustSlots();
|
|
};
|
|
|
|
const origFord = stationControls.ford.select;
|
|
stationControls.ford.select = (chainageM, role) => {
|
|
markWallSelecting();
|
|
origFord(chainageM, role);
|
|
panel.showPipeAt(role ? chainageM : null);
|
|
};
|
|
const origBox = stationControls.box.select;
|
|
stationControls.box.select = (chainageM, role) => {
|
|
markWallSelecting();
|
|
origBox(chainageM, role);
|
|
panel.showPipeAt(role ? chainageM : null);
|
|
};
|
|
return { syncInletStructure };
|
|
}
|