164 lines
7.9 KiB
TypeScript
164 lines
7.9 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Page_Design_Sync.ts
|
||
* 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
||
* 동작은 그대로다(2026-09-13 분리).
|
||
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
|
||
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
|
||
* - `reconcileStaleDesigns`: 횡단 다시 셈을 계산 묶음에 부탁(cross 단계 한 곳 · 387-4)
|
||
* - `syncBermSpans` : 구조물 측점 열쇠(소단 사본은 페이지 시설 줄이 씀 · 467)
|
||
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
||
* 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2).
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
readCrossDesignChoice,
|
||
writeBermDepth,
|
||
writeCrossDesignChoice,
|
||
writeShoulderExtend,
|
||
type CrossDesignChoice,
|
||
} from "./B06_Section_Cross_Design_Session";
|
||
import { showToast } from "@ui/ui_template_elements";
|
||
import { L } from "./B06_Section_UI_Page_Common";
|
||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||
import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_Section_View";
|
||
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
|
||
import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail";
|
||
import { useStructureIds } from "./B06_Section_Station_Keys";
|
||
import { pipelineCross, reconcilePagePipeline } from "../A00_Common/design/design_pipeline_page";
|
||
import { forceCross } from "../A00_Common/design/design_pipeline_cross";
|
||
|
||
export interface DesignSyncContext {
|
||
projectId: string | null;
|
||
routeId: () => number | null;
|
||
detail: () => SectionDetailResponse | null;
|
||
/** 뷰는 이 모듈보다 **뒤에** 만들어지므로 그때 채워지는 참조를 통해 부른다. */
|
||
view: () => SectionViewController;
|
||
}
|
||
|
||
export interface DesignSyncController {
|
||
handleDesignChange: (chainageM: number, change: CrossDesignChange) => Promise<void>;
|
||
recomputeIfRock: (chainageM: number) => void;
|
||
reconcileStaleDesigns: (options?: { force?: boolean }) => Promise<void>;
|
||
syncBermSpans: (structures: ReadonlyArray<StructureInstance>) => void;
|
||
applyPanelToAll: () => Promise<void>;
|
||
}
|
||
|
||
/** 카드 고름 칸 — 세션 `crossdesign` 에 담는 것(바꾼 칸만 · 272). */
|
||
const CHOICE_KEYS = [
|
||
"ground_type",
|
||
"section_mode",
|
||
"ditch_side",
|
||
"ditch_type",
|
||
"paved",
|
||
"two_stage_slope",
|
||
"ditch_choice",
|
||
] as const satisfies ReadonlyArray<keyof CrossDesignChoice & keyof CrossDesignChange>;
|
||
|
||
export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
||
const { projectId } = ctx;
|
||
// 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세 · 칸 크기 그림(173-4)
|
||
if (projectId)
|
||
setCrossDetailHost({
|
||
projectId,
|
||
routeId: ctx.routeId,
|
||
detail: ctx.detail,
|
||
chart: (stationId, widthPx, heightPx) => ctx.view().detailChart(stationId, widthPx, heightPx),
|
||
});
|
||
|
||
/**
|
||
* 측점 설계 버튼 변경 처리: 선택을 세션 초안에 쓰고 브라우저 다시 셈(바뀐 카드만 다시 그림 · 전체 재렌더 없음).
|
||
* 다시 셈 입력 우선순위 = 세션 고름 → 저장 정본 → config 기본값.
|
||
*/
|
||
async function handleDesignChange(chainageM: number, change: CrossDesignChange): Promise<void> {
|
||
const sectionDetail = ctx.detail();
|
||
const currentRouteId = ctx.routeId();
|
||
if (!projectId || currentRouteId === null || !sectionDetail) return;
|
||
const target = sectionDetail.cross_sections.find(
|
||
(section) => Math.abs(section.chainage_m - chainageM) < 0.01,
|
||
);
|
||
if (!target) return;
|
||
|
||
// 고름은 세션에만 — 메모리 상세 설계를 안 덮음(덮으면 ↶ 로 세션 칸을 지워도 다시 셈이 덮인 값으로 물러섬 ·
|
||
// 467 덧) · 카드는 다시 셈 알림(`onCrossDesignsUpdated`)이 그림
|
||
// 설계선 쉬프트(139-6) — 그 쪽 노견 확장만 세션에 · 브라우저 재계산(같은 창구)
|
||
const shift = change.shoulder_extend;
|
||
if (shift) {
|
||
writeShoulderExtend(projectId, currentRouteId, chainageM, shift.side, shift.valueM);
|
||
await reconcileStaleDesigns({ force: true });
|
||
return;
|
||
}
|
||
// 소단 높이(194-1) — 그 쪽 깊이만 세션에 · 브라우저 재계산(같은 창구)
|
||
const berm = change.berm_depth;
|
||
if (berm) {
|
||
writeBermDepth(projectId, currentRouteId, chainageM, berm.side, berm.valueM);
|
||
await reconcileStaleDesigns({ force: true });
|
||
return;
|
||
}
|
||
|
||
// 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
||
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
||
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
||
// 바꾼 칸만(272) — 같은 측점 앞서 바꾼 칸에 얹음
|
||
const picked = Object.fromEntries(
|
||
CHOICE_KEYS.filter((key) => change[key] !== undefined).map((key) => [key, change[key]]),
|
||
) as CrossDesignChoice;
|
||
if (!Object.keys(picked).length) return;
|
||
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
||
...readCrossDesignChoice(projectId, currentRouteId, chainageM),
|
||
...picked,
|
||
});
|
||
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
||
await reconcileStaleDesigns({ force: true });
|
||
}
|
||
|
||
/** 암 경계 · 암 경사 변경 뒤 다시 셈(암 측점 2단계 무릎 · 단면적) — 지금 값을 「사용자 선택」으로
|
||
* 박지 않음(자동 판정이 굳으면 노선이 바뀌어도 재판정 안 됨 · 346-2). 암 경계는 세션에서 읽음. */
|
||
function recomputeIfRock(_chainageM: number): void {
|
||
void reconcileStaleDesigns({ force: true });
|
||
}
|
||
|
||
/**
|
||
* 횡단 다시 셈 = 계산 묶음 cross 단계 **한 곳**(387-4) — 여기서는 묶음에 부탁하고 그 차례가 끝날 때까지
|
||
* 기다림. 입력이 같으면 안 셈 · `force` = 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)이라 한 번 더.
|
||
* 바뀐 카드는 묶음 알림(`onCrossDesignsUpdated`)이 다시 그림.
|
||
*/
|
||
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
|
||
if (!projectId) return;
|
||
if (options?.force) forceCross();
|
||
pipelineCross(projectId);
|
||
await reconcilePagePipeline(projectId);
|
||
}
|
||
|
||
/** 구조물 측점 열쇠 = 구조물 id(344-3) — 소단 사본(세션 `berm`)은 페이지 시설 줄 한 곳이 씀(B05 만 열어도 · 467). */
|
||
function syncBermSpans(structures: ReadonlyArray<StructureInstance>): void {
|
||
if (!projectId || ctx.routeId() === null) return;
|
||
useStructureIds(projectId, structures);
|
||
}
|
||
|
||
/** 패널 [전체 반영](N-2-1 · 346-2): 표준값으로 전 측점을 **한 번** 다시 셈 — 측점마다 돌면
|
||
* 66 × 66 = 4.1 s 였음. 표준값은 다시 셈이 `effectiveStandardCross` 로 읽고, 측점별 선택은
|
||
* 세션에 있는 것만 씀(지금 값을 「사용자 선택」으로 박지 않음 · 사용자 확정 10-16). */
|
||
let applyingAll = false;
|
||
async function applyPanelToAll(): Promise<void> {
|
||
const sectionDetail = ctx.detail();
|
||
if (applyingAll || !sectionDetail || !projectId || ctx.routeId() === null) return;
|
||
if (!sectionDetail.cross_sections.some((section) => section.design)) return;
|
||
// 화면을 잠그지 않는다(CLAUDE.md 5장) — 도는 동안 다시 누르는 것만 막는다.
|
||
applyingAll = true;
|
||
try {
|
||
await reconcileStaleDesigns({ force: true });
|
||
showToast(L("B06_Std_ApplyAll_Success"), "success");
|
||
} finally {
|
||
applyingAll = false;
|
||
}
|
||
}
|
||
|
||
return {
|
||
handleDesignChange,
|
||
recomputeIfRock,
|
||
reconcileStaleDesigns,
|
||
syncBermSpans,
|
||
applyPanelToAll,
|
||
};
|
||
}
|