- design_view_profile_sections: 노선마다 상세 갈아끼움을 듣고 종단도 · 표 · 3D 측점선 다시(보일 때만 · 숨으면 flushSwap 이 다시 보일 때) · 가게 stations 도 들어 횡단 보기를 안 열어도 갈아끼움(먼저 부른 쪽만 · 다음은 「같은 목록」) - design_view_profile: 받는 줄만(보임 · 떠날 때 끊음 · 다시 보일 때 flushSwap) - 갈아끼움 알림판 B06_Section_Swap_Bus(가져오기 없음 · 돌고 도는 가져오기 막음) — 조정창 값 · 암 경계 옮김을 어느 보기가 갈아끼웠든 한 번 · 횡단 보기는 알림으로 다시 그림 - 시험: 종단 보기 보임 7 → 8 · 숨음 = 안 그림 · 다시 보임 = 6 · 3D 측점선 같은 차례 · 옛 시험 둘(342-1 화면 선 = listedBase · 323-9 열쇠 모듈 진짜로) 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
231 lines
10 KiB
TypeScript
231 lines
10 KiB
TypeScript
/* =============================================================================
|
|
* design_view_profile_sections.ts
|
|
* 종단 보기의 **노선 · 종단면 그리기** — 최신 노선 · 측점선 · 종단면 자료(불러오기 · 그리기 ·
|
|
* 옛 측점 이관). `design_view_profile.ts` 가 700줄을 넘어 뗀 조각(345-1). 상태는 본체가 쥔
|
|
* `ProfileViewState` 한 벌을 같이 본다.
|
|
* ========================================================================== */
|
|
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import type { SurfaceConfirmedResponse } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import type { RouteLatestResponse } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
|
import type { RoutePanel } from "../../B05_Profile/B05_Profile_UI_Panel";
|
|
import type { createRouteProfilePanel } from "../../B05_Profile/B05_Profile_UI_Profile_Panel";
|
|
import type { createStructuresBridge } from "../../B05_Profile/B05_Profile_UI_Page_Structures";
|
|
import type { RouteViewer } from "../../B05_Profile/B05_Profile_UI_Viewer";
|
|
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
|
import {
|
|
loadSectionDetail,
|
|
subscribeSectionDetail,
|
|
swapSectionStations,
|
|
} from "../../B06_Section/B06_Section_Section_Store";
|
|
import { useDesignStore } from "./design_store";
|
|
import { migrateLegacyStations } from "../../B05_Profile/B05_Profile_Api_Structures";
|
|
import { loadCorridorIfFresh } from "../../B05_Profile/B05_Profile_UI_Corridor";
|
|
import { stationLinesOf } from "../../B05_Profile/B05_Profile_UI_Page_Stations";
|
|
import { restorePoints, type RoadWidths } from "../../B05_Profile/B05_Profile_UI_Page_Helpers";
|
|
import { panelValuesOf } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
|
|
|
|
/** 종단 보기 본체 · 조각이 함께 보는 값 한 벌. */
|
|
export interface ProfileViewState {
|
|
/** 확정 지표면 — 모델 id 와 3D 로딩에 필요한 범위만 들고 있는다(2026-09-06 호출 정리). */
|
|
confirmedSurface: SurfaceConfirmedResponse | null;
|
|
latest: RouteLatestResponse | null;
|
|
roadWidths: RoadWidths;
|
|
detail: SectionDetailResponse | null;
|
|
/** [예상형상] 표시 상태 — 측점 바를 계획고 위로 올릴지 판단한다(패널 기본 ON). */
|
|
corridorVisible: boolean;
|
|
/** 3D 길 모형이 지금 정본보다 낡았나 — [3D 업데이트] 표시와 한 값 · 모달을 열 때 다시 짓는 기준(345-4). */
|
|
corridorPending: boolean;
|
|
routeReady: boolean;
|
|
restoring: boolean;
|
|
}
|
|
|
|
export interface ProfileSectionsDeps {
|
|
projectId: string;
|
|
state: ProfileViewState;
|
|
viewer: RouteViewer;
|
|
panel: () => RoutePanel;
|
|
profilePanel: () => ReturnType<typeof createRouteProfilePanel>;
|
|
bridge: () => ReturnType<typeof createStructuresBridge>;
|
|
/** 종단 보기가 보이나 — 숨은 동안 갈아끼운 상세는 다시 보일 때 그림(346-2 · 없으면 늘 보임). */
|
|
visible?: () => boolean;
|
|
}
|
|
|
|
/** 구 구조물 측점 이관을 이미 시도한 노선 — 탭 수명 동안 유지한다. */
|
|
const migratedRoutes = new Set<string>();
|
|
|
|
export function createProfileSections(deps: ProfileSectionsDeps) {
|
|
const { projectId, state, viewer } = deps;
|
|
|
|
function restorePanel(next: RouteLatestResponse): void {
|
|
deps.panel().restore(panelValuesOf(next));
|
|
viewer.markers.setPoints(restorePoints(next));
|
|
}
|
|
|
|
function renderStationLines(detail: SectionDetailResponse): void {
|
|
const lines = stationLinesOf({
|
|
detail,
|
|
irregular: deps.bridge().irregularStations(),
|
|
routePoints: state.latest?.route_points ?? [],
|
|
liveSamples: deps.profilePanel().alignmentSamples(),
|
|
corridorVisible: state.corridorVisible,
|
|
});
|
|
viewer.renderStationLines(lines, state.roadWidths[deps.panel().values().gradeClass] / 2);
|
|
}
|
|
|
|
/** 지금 종단면 자료로 측점선만 다시 그린다(자료가 없으면 그대로). */
|
|
function redrawStationLines(): void {
|
|
if (state.detail) renderStationLines(state.detail);
|
|
}
|
|
|
|
function renderSections(detail: SectionDetailResponse, routeId?: number): void {
|
|
const panel = deps.panel();
|
|
const profilePanel = deps.profilePanel();
|
|
state.detail = detail;
|
|
profilePanel.render(detail, panel.values().stationInterval ?? undefined, routeId);
|
|
// 저장된 계획선의 정책은 만들 당시 기준이라 지금 설정(임도 종류·설계속도·지형)과
|
|
// 다를 수 있다 — 그릴 때마다 현재 기준으로 맞춘다(2026-08-19).
|
|
profilePanel.setGradeLimit(panel.gradeLimitPct());
|
|
profilePanel.setEnforceMinCover(panel.values().enforcePipeClearance);
|
|
profilePanel.setStationDisplay(panel.stationDisplayOffset());
|
|
profilePanel.setIrregularStations(deps.bridge().irregularStations());
|
|
renderStationLines(detail);
|
|
// 예상형상 코리도 — 저장본이 지금 정본과 맞을 때만 올린다. 낡았으면 [3D 업데이트] 대기
|
|
// 표시만 켠다(2026-09-06 실측: 받아놓고 버리는 데다 다시 만드느라 전환이 16.7초까지 갔다).
|
|
const routePoints = state.latest?.route_points ?? [];
|
|
if (routePoints.length > 1) {
|
|
void loadCorridorIfFresh(
|
|
viewer,
|
|
projectId,
|
|
routeId ?? state.latest?.route?.id,
|
|
detail,
|
|
routePoints,
|
|
profilePanel.alignmentSamples() ?? undefined,
|
|
).then((fresh) => setCorridorPending(!fresh));
|
|
}
|
|
}
|
|
|
|
async function restoreSections(routeId: number): Promise<void> {
|
|
const profilePanel = deps.profilePanel();
|
|
let detail: SectionDetailResponse;
|
|
profilePanel.setLoading("종단면 자료를 불러오는 중…");
|
|
try {
|
|
// 공유 캐시 — 횡단 보기가 이미 받아 뒀으면 같은 객체를 재사용해 두 보기가 같은 값을 본다.
|
|
detail = await loadSectionDetail(projectId, routeId);
|
|
} catch {
|
|
// 종횡단 데이터 자체가 없는 경우(생성 실패·최초 진입)는 빈 안내로 둔다.
|
|
state.detail = null;
|
|
profilePanel.setLoading(null);
|
|
profilePanel.clear();
|
|
viewer.renderStationLines([], 0);
|
|
return;
|
|
}
|
|
profilePanel.setLoading(null);
|
|
followSwap(routeId);
|
|
try {
|
|
renderSections(detail, routeId);
|
|
// 구 확정분에 남은 비정규 측점(자유 텍스트)을 구조물 정본으로 1회 이관한다 — 멱등이지만
|
|
// 탭 수명 동안 노선마다 한 번만 시도한다(2026-08-17 · 09-06 실측).
|
|
const legacy = detail.longitudinal.stations
|
|
.filter((station) => station.kind === "irregular")
|
|
.map((station) => ({
|
|
chainage_m: station.chainage_m,
|
|
structure: station.structure ?? "",
|
|
}));
|
|
const migrateKey = `${projectId}:${routeId}`;
|
|
if (legacy.length && !migratedRoutes.has(migrateKey)) {
|
|
migratedRoutes.add(migrateKey);
|
|
void migrateLegacyStations(projectId, legacy)
|
|
.then((result) => {
|
|
if (result.migrated > 0) {
|
|
showToast(`구 구조물 측점 ${result.migrated}건을 구조물 목록으로 옮겼습니다.`);
|
|
return deps
|
|
.bridge()
|
|
.refreshFromServer()
|
|
.then(() => undefined);
|
|
}
|
|
return undefined;
|
|
})
|
|
.catch(() => {
|
|
/* 이관 실패는 치명적이지 않다 — 다음 진입에서 다시 시도된다. */
|
|
});
|
|
}
|
|
} catch (error) {
|
|
// 렌더 실패를 "데이터 없음"으로 감추면 원인을 알 수 없게 된다 — 실패 사실을 드러낸다.
|
|
console.error("B05 종단면도 렌더 실패", error);
|
|
showToast(
|
|
error instanceof Error
|
|
? `종단면도 표시 실패: ${error.message}`
|
|
: "종단면도 표시에 실패했습니다.",
|
|
"error",
|
|
);
|
|
}
|
|
}
|
|
|
|
/* 측점 수가 바뀜(346-2 · 묶음 측점 → 횡단 쪽이 상세를 갈아끼움) — 종단도 · 표 · 3D 측점선을 새 목록으로
|
|
* 다시(보일 때만 · 숨었으면 다시 보일 때 `flushSwap`). 노선마다 한 번 듣고, 노선이 바뀌면 옛 것을 끊음. */
|
|
let swap: { routeId: number; stop: () => void } | null = null;
|
|
let swapPending = false;
|
|
function followSwap(routeId: number): void {
|
|
if (swap?.routeId === routeId) return;
|
|
swap?.stop();
|
|
const stopListen = subscribeSectionDetail(projectId, routeId, (next) => {
|
|
state.detail = next;
|
|
swapPending = !(deps.visible?.() ?? true);
|
|
if (!swapPending) renderSections(next, routeId);
|
|
});
|
|
// 묶음 측점(가게)이 바뀌면 갈아끼움 — 횡단 보기를 안 열었어도(그쪽도 부르면 둘째는 「같은 목록」)
|
|
const stopStore = useDesignStore(projectId, null).subscribe("stations", (stations) => {
|
|
if (stations?.length) void swapSectionStations(projectId, routeId, stations);
|
|
});
|
|
swap = {
|
|
routeId,
|
|
stop: () => {
|
|
stopListen();
|
|
stopStore();
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 숨은 동안 받은 새 상세를 그림 — 종단 보기가 다시 보일 때. */
|
|
function flushSwap(): void {
|
|
if (!swapPending || !state.detail || !swap) return;
|
|
swapPending = false;
|
|
renderSections(state.detail, swap.routeId);
|
|
}
|
|
|
|
/** 「3D 늦음」 — [3D 업데이트] 표시(setCorridorPending 그대로)와 상태 한 칸을 같이 맞춘다. */
|
|
function setCorridorPending(pending: boolean): void {
|
|
state.corridorPending = pending;
|
|
deps.panel().setCorridorPending(pending);
|
|
}
|
|
|
|
function renderLatest(next: RouteLatestResponse): void {
|
|
state.latest = next;
|
|
state.routeReady = Boolean(next.route && next.route_points.length > 1);
|
|
deps.profilePanel().setRoutePolyline(next.route_points ?? []);
|
|
if (next.route) {
|
|
const stored = next.route.algorithm_params ?? {};
|
|
viewer.markers.renderRoute(
|
|
next.route_points,
|
|
(stored.curve_warning_segments as Array<{
|
|
polyline_start_index: number;
|
|
polyline_end_index: number;
|
|
}>) ?? [],
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
restorePanel,
|
|
renderStationLines,
|
|
redrawStationLines,
|
|
renderSections,
|
|
restoreSections,
|
|
renderLatest,
|
|
setCorridorPending,
|
|
flushSwap,
|
|
stopSwap: () => swap?.stop(),
|
|
};
|
|
}
|