700줄 한도 대응. 순수 이동으로 동작 불변. - B06_Section_UI_Page_Design_Sync.ts — 설계 선택 반영·낡음 재계산·소단 동기화·[전체 반영] - B06_Section_UI_Page_Grade_Edit.ts — 종단 계획선 ▲▼ 제공자 - 남은 B06_Section_UI_Page.ts 644줄 소스 문자열 감시 시험 셋이 분리 모듈까지 읽게 함. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
219 lines
11 KiB
TypeScript
219 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Page_Design_Sync.ts
|
|
* 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
|
* 동작은 그대로다(2026-09-13 분리).
|
|
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
|
|
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
|
|
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
|
|
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
|
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
|
* ========================================================================== */
|
|
|
|
import { writeCrossDesignChoice } from "./B06_Section_Cross_Design_Session";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
|
|
import {
|
|
bermSpansFromStructures,
|
|
readBermSpans,
|
|
writeBermSpans,
|
|
} from "./B06_Section_UI_Page_Persist";
|
|
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
|
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
|
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";
|
|
|
|
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>;
|
|
}
|
|
|
|
export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
|
const { projectId } = ctx;
|
|
|
|
/**
|
|
* 측점 설계 버튼 변경 처리: (1) 선택을 즉시 로컬 반영해 해당 카드만 리프레시(버튼 즉시 반응),
|
|
* (2) 서버에서 단면적을 계산·저장하고 최신 요청이면 그 카드만 다시 갱신한다. 전체 재렌더 없음.
|
|
* 설정 패널 편집값을 요청에 실어 요청값 → DB 저장 옵션 → 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;
|
|
|
|
// (1) 즉시 로컬 반영: 선택 버튼만 갱신(숫자·설계선은 기존값 유지) → 해당 카드만 교체.
|
|
if (target.design) {
|
|
target.design = {
|
|
...target.design,
|
|
ground_type: change.ground_type,
|
|
section_mode: change.section_mode,
|
|
ditch_side: change.ditch_side ?? target.design.ditch_side,
|
|
ditch_type: change.ditch_type,
|
|
paved: change.paved,
|
|
two_stage_slope: change.two_stage_slope,
|
|
ditch_choice: change.ditch_choice,
|
|
};
|
|
ctx.view().refreshCard(chainageM);
|
|
}
|
|
|
|
// (2) 선택은 **세션 초안**으로 남긴다 — 화면을 오가거나 새로고침해도 남고,
|
|
// [저장]·[확정] 때 한 번에 정본으로 나간다(2026-09-06 사용자 확정: 캐시가 저절로
|
|
// 영구저장소로 새면 안 된다). 예전에는 여기서 서버가 계산하고 바로 저장했다.
|
|
writeCrossDesignChoice(projectId, currentRouteId, chainageM, {
|
|
ground_type: change.ground_type,
|
|
section_mode: change.section_mode,
|
|
ditch_side: change.ditch_side ?? null,
|
|
ditch_type: change.ditch_type,
|
|
paved: change.paved,
|
|
two_stage_slope: change.two_stage_slope,
|
|
ditch_choice: change.ditch_choice,
|
|
});
|
|
// (3) 계산은 브라우저 안에서 — B05·B06 이 같이 쓰는 창구 하나로 돌린다.
|
|
await reconcileStaleDesigns({ force: true });
|
|
}
|
|
|
|
/** 현재 design 값에서 재계산용 change를 복원한다(암 경계 오프셋 변경 시 재계산 트리거). */
|
|
function changeFromDesign(chainageM: number): CrossDesignChange | null {
|
|
const target = ctx
|
|
.detail()
|
|
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01);
|
|
const design = target?.design;
|
|
if (!design) return null;
|
|
return {
|
|
ground_type: design.ground_type,
|
|
section_mode: design.section_mode,
|
|
ditch_side: design.ditch_side ?? null,
|
|
ditch_type: design.ditch_type ?? "standard",
|
|
paved: design.paved,
|
|
two_stage_slope: design.two_stage_slope ?? true,
|
|
ditch_choice: design.ditch_choice ?? null,
|
|
};
|
|
}
|
|
|
|
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 서버 재계산한다. */
|
|
function recomputeIfRock(chainageM: number): void {
|
|
const change = changeFromDesign(chainageM);
|
|
if (change && change.ground_type !== "soil") void handleDesignChange(chainageM, change);
|
|
}
|
|
|
|
/**
|
|
* 로드 시 stale design을 최신 엔진·최신 종단 계획고로 자동 재계산한다(E-1 + N-6).
|
|
* 대상: (1) 2단계 경사 필드(`two_stage_slope`)가 없는 옛 암 측점, (2) B05에서 종단이
|
|
* 변경·확정돼 저장된 계산 기준 계획고(`design.design_elevation_m`)가 현재 계획선
|
|
* (`design_profiles`) 보간값과 어긋난 측점. 종단이 안 바뀐 측점은 0건이라 불필요한 API
|
|
* 호출이 없다.
|
|
*
|
|
* 재계산은 측점별 순차 호출이 아니라 **일괄 프리뷰 1회**로 한다(2026-08-04 사용자 확인
|
|
* — 예전 for-await 루프는 stale 측점 수만큼 왕복하며 카드가 하나씩 바뀌어 "이력 재생"처럼
|
|
* 보였고 제일 느렸다). 편집 델타는 B05 세션 초안이 있으면 그것을(두 화면 동일 계획선),
|
|
* 없으면 저장분(profile_alignment.edits)을 쓴다. 측점별 사용자 선택값(지반유형·단면유형·
|
|
* 측구·암 경계)은 서버가 저장분에서 유지하고, 세션에만 있는 암 경계 오프셋은 함께 실어 보낸다.
|
|
*/
|
|
async function reconcileStaleDesigns(options?: { force?: boolean }): Promise<void> {
|
|
const sectionDetail = ctx.detail();
|
|
const currentRouteId = ctx.routeId();
|
|
if (!sectionDetail || !projectId || currentRouteId === null) return;
|
|
const draft = readAlignmentDraft(currentRouteId);
|
|
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 판정 — 계획고 어긋남 +
|
|
// 옛 암 2단계 필드 누락). 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다.
|
|
//
|
|
// 다만 **미저장 세션 편집이 있으면 판정을 건너뛰고 무조건 맞춘다**. 서버에서 갓 받은
|
|
// 저장분끼리는 늘 일치해 「낡지 않음」으로 나오는데, B05가 세션에 남긴 편집은 그 안에
|
|
// 없어 B06이 재계산을 통째로 건너뛰었다 — 같은 시점에 B05 절토 4,774.1㎥ ↔ B06
|
|
// 4,515.0㎥ 로 갈렸다(2026-09-03 실측). 재계산은 브라우저 안에서 끝나 값싸다.
|
|
if (!options?.force && !draft && !hasStaleDesigns(sectionDetail)) return;
|
|
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
|
|
// (CLAUDE.md 5장).
|
|
try {
|
|
const alignment = sectionDetail.longitudinal.profile_alignment as
|
|
| {
|
|
edits?: {
|
|
station_offsets?: Record<string, number>;
|
|
curve_radii?: Record<string, number>;
|
|
};
|
|
}
|
|
| undefined;
|
|
const edits = draft ?? {
|
|
station_offsets: alignment?.edits?.station_offsets ?? {},
|
|
curve_radii: alignment?.edits?.curve_radii ?? {},
|
|
};
|
|
// 재계산은 B05와 **같은 창구**를 쓴다 — 인자가 갈리면 같은 데이터가 두 화면에서
|
|
// 다른 값이 된다(2026-09-03 사용자 지시로 일원화).
|
|
const updated = await refreshCrossDesigns({
|
|
projectId,
|
|
routeId: currentRouteId,
|
|
detail: sectionDetail,
|
|
edits,
|
|
});
|
|
// 카드는 한꺼번에 갈아 끼운다 — 측점마다 `refreshCard` 를 부르면 그때마다 종단
|
|
// 그래프·유토곡선까지 다시 그려 측점 수만큼 화면이 멈췄다(2026-09-12).
|
|
ctx.view().refreshCards(updated);
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 구조물 목록의 **소단**(C군 사면안정)을 세션 사본으로 편다 — 재계산이 측점마다 읽는 값이다.
|
|
*
|
|
* 사용자는 「구조물 배치」에서 놓고(2026-09-07 확정), 계산은 그 사본만 본다. 달라졌을 때만
|
|
* 다시 계산한다 — 목록은 화면을 열 때도 오므로 매번 돌리면 진입이 느려진다.
|
|
*/
|
|
function syncBermSpans(structures: ReadonlyArray<StructureInstance>): void {
|
|
const currentRouteId = ctx.routeId();
|
|
if (!projectId || currentRouteId === null) return;
|
|
const next = bermSpansFromStructures(structures);
|
|
if (JSON.stringify(next) === JSON.stringify(readBermSpans(projectId, currentRouteId))) return;
|
|
writeBermSpans(projectId, currentRouteId, next);
|
|
void reconcileStaleDesigns({ force: true });
|
|
}
|
|
|
|
/** 패널 [전체 반영](N-2-1): design 보유 전 측점을 패널 최신값으로 순차 재계산한다.
|
|
* handleDesignChange가 standardPanel.getValues()를 실어 보내므로 표준단면 수치만
|
|
* 갱신되고 측점별 버튼 선택값은 보존된다. 순차 await로 동시 API 호출 수를 제한한다. */
|
|
let applyingAll = false;
|
|
async function applyPanelToAll(): Promise<void> {
|
|
const sectionDetail = ctx.detail();
|
|
if (applyingAll || !sectionDetail || !projectId || ctx.routeId() === null) return;
|
|
const targets = sectionDetail.cross_sections.filter((section) => section.design);
|
|
if (!targets.length) return;
|
|
// 화면을 잠그지 않는다 — 카드가 하나씩 갱신되는 것이 곧 진행 표시다(CLAUDE.md 5장).
|
|
// 대신 도는 동안 다시 누르는 것만 막는다.
|
|
applyingAll = true;
|
|
try {
|
|
for (const section of targets) {
|
|
const change = changeFromDesign(section.chainage_m);
|
|
if (change) await handleDesignChange(section.chainage_m, change);
|
|
}
|
|
showToast(L("B06_Std_ApplyAll_Success"), "success");
|
|
} finally {
|
|
applyingAll = false;
|
|
}
|
|
}
|
|
|
|
return {
|
|
handleDesignChange,
|
|
recomputeIfRock,
|
|
reconcileStaleDesigns,
|
|
syncBermSpans,
|
|
applyPanelToAll,
|
|
};
|
|
}
|