- 열쇠 규칙 한 벌 = common_util_station_key(ts · py 짝 · 거울 시험) — 일반 grid:k · 관 pipe:<id> · 구조물 structure:<id> · 시종점 end:BP/EP · id 모름 at:<0.01 m> - 세션 칸 다섯(카드 선택 · 노견 확장 · 소단 깊이 · 암 경계 · 암 경사) = 측점 열쇠로 담음 · 부르는 쪽은 그대로 누가거리로 읽고 씀 · 칸마다 쓴 때 자리를 함께 담아 구조물 id 를 아직 모를 때도 그 측점 고침을 읽음 - 옛 초안(누가거리 열쇠)은 읽을 때 측점 열쇠로 옮김 · 지금 측점에 없는 고침은 지우지 않고 남김 - 측점 목록 = 상세가 초안 측점을 얹을 때 · 다시 셈 때 세움(관 id = 관 장부 · 구조물 id = 측점 계획 · 구조물 목록) - 노선 바뀜 넘김 carryStationDrafts — 카드 선택 넷(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사) · 암 경계 · 암 경사만 · 단면 유형 · 측구 방향은 새 지형이 정함 · 사라진 측점 = 「노선 밖 고침 N 건」 한 줄(부르는 자리 = 3파) - 옛 시험 셋 새 창구로 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
231 lines
12 KiB
TypeScript
231 lines
12 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Page_Design_Sync.ts
|
||
* 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
||
* 동작은 그대로다(2026-09-13 분리).
|
||
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
|
||
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
|
||
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
|
||
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
||
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
writeBermDepth,
|
||
writeCrossDesignChoice,
|
||
writeShoulderExtend,
|
||
} from "./B06_Section_Cross_Design_Session";
|
||
import { showToast } from "@ui/ui_template_elements";
|
||
import { refreshCrossDesigns, staleAgainstScreen } 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 { 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";
|
||
|
||
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;
|
||
// 횡단 [상세] 모달(138-4)이 쓰는 프로젝트 · 노선 · 상세 · 칸 크기 그림(173-4)
|
||
if (projectId)
|
||
setCrossDetailHost({
|
||
projectId,
|
||
routeId: ctx.routeId,
|
||
detail: ctx.detail,
|
||
chart: (stationId, widthPx, heightPx) => ctx.view().detailChart(stationId, widthPx, heightPx),
|
||
});
|
||
|
||
/**
|
||
* 측점 설계 버튼 변경 처리: (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;
|
||
|
||
// 설계선 쉬프트(139-6) — 그 쪽 노견 확장만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구)
|
||
const shift = change.shoulder_extend;
|
||
if (shift) {
|
||
writeShoulderExtend(projectId, currentRouteId, chainageM, shift.side, shift.valueM);
|
||
if (target.design) target.design[`shoulder_extend_${shift.side}_m`] = shift.valueM;
|
||
await reconcileStaleDesigns({ force: true });
|
||
return;
|
||
}
|
||
// 소단 높이(194-1) — 그 쪽 깊이만 세션에 · 캐시에 바로 · 브라우저 재계산(같은 창구)
|
||
const berm = change.berm_depth;
|
||
if (berm) {
|
||
writeBermDepth(projectId, currentRouteId, chainageM, berm.side, berm.valueM);
|
||
if (target.design) target.design[`berm_depth_${berm.side}_m`] = berm.valueM;
|
||
await reconcileStaleDesigns({ force: true });
|
||
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 });
|
||
}
|
||
|
||
/** 암 경계 오프셋 변경 후 암 지반이면 2단계 무릎·단면적을 다시 셈 — 지금 값을 「사용자 선택」으로
|
||
* 박지 않음(자동 판정이 굳으면 노선이 바뀌어도 재판정 안 됨 · 346-2). 암 경계는 세션에서 읽음. */
|
||
function recomputeIfRock(chainageM: number): void {
|
||
const design = ctx
|
||
.detail()
|
||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design;
|
||
if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true });
|
||
}
|
||
|
||
/**
|
||
* 로드 시 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 실측). 재계산은 브라우저 안에서 끝나 값싸다.
|
||
// 판정 · 다시 셈 모두 종단도가 쥔 선(측점 한 벌) 기준 — 저장분 선끼리 견주면 화면과 갈림(342-1).
|
||
if (!options?.force && !draft && !staleAgainstScreen(sectionDetail, currentRouteId)) 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;
|
||
useStructureIds(projectId, structures); // 구조물 측점 열쇠 = 구조물 id(344-3)
|
||
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 · 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,
|
||
};
|
||
}
|