Files
Aislo/B06_Section/B06_Section_UI_Page_Design_Sync.ts
T
eomsangdonandClaude Opus 5.5 6a3f9647f3 refactor(387-4): 횡단 다시 셈 창구 하나 · 면적 얹기 한 곳 — 화면은 묶음에 부탁
- 다시 셈(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
2026-10-17 18:19:21 +09:00

182 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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,
};
}