- 다시 셈(refreshCrossDesigns) 부르는 곳 = 묶음 cross 단계 하나(design_shared savedCross) - 종단 미리 셈 · B06 설계 맞춤 · 3D 업데이트 = pipelineCross · reconcilePagePipeline(▲ 한 번 = cross 한 번) - 입력 칸 밖 세션 고침(노견 · 소단 · 단면 유형 · 조정창)은 forceCross 로 같은 입력이어도 한 번 더 - 카드 그리기 · 횡단 보기 면적 쓰기 지움 · 다시 셈 창구가 면적을 늘 얹음(바뀐 측점만 알림) - 저장 안 한 조정창 값을 상세 가게가 읽을 때 얹음(새 Section_Adjusts · B05 만 열어도 같은 면적) - 새 test_384_4_cross_window · 옛 약속 시험 넷 새 약속으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
182 lines
8.8 KiB
TypeScript
182 lines
8.8 KiB
TypeScript
/* =============================================================================
|
||
* B06_Section_UI_Page_Design_Sync.ts
|
||
* 측점 설계 선택 반영·재계산 창구. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며
|
||
* 동작은 그대로다(2026-09-13 분리).
|
||
* - `handleDesignChange` : 버튼 선택 즉시 반영 + 세션 초안 기록 + 재계산
|
||
* - `recomputeIfRock` : 암 경계·암 절토 경사 변경 후 암 측점만 재계산
|
||
* - `reconcileStaleDesigns`: 횡단 다시 셈을 계산 묶음에 부탁(cross 단계 한 곳 · 387-4)
|
||
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
||
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
||
* 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2).
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
writeBermDepth,
|
||
writeCrossDesignChoice,
|
||
writeShoulderExtend,
|
||
} from "./B06_Section_Cross_Design_Session";
|
||
import {
|
||
bermSpansFromStructures,
|
||
readBermSpans,
|
||
writeBermSpans,
|
||
} from "./B06_Section_UI_Page_Persist";
|
||
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>;
|
||
}
|
||
|
||
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 {
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 구조물 목록의 **소단**(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,
|
||
};
|
||
}
|