/* ============================================================================= * B06_Section_UI_Page_Grade_Edit.ts * 종단 계획선 편집(▲/▼) 제공자. `_UI_Page` 700줄 제한 대응으로 떼어낸 것이며 * 동작은 그대로다(2026-09-13 분리). * ========================================================================== */ import { createProfileEditStore, type ProfileEditStore, } from "../B05_Profile/B05_Profile_UI_Profile_Edit"; import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data"; import { adjustStation, buildAlignment, toAlignmentBase, } from "../B05_Profile/B05_Profile_UI_Profile_Alignment"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { SectionViewController } from "./B06_Section_UI_Section_View"; type GradeEditProvider = NonNullable[0]>; export interface GradeEditContext { routeId: () => number | null; detail: () => SectionDetailResponse | null; view: () => SectionViewController; /** 전 측점 횡단 재계산 — 손을 뗀 뒤 한 번만 돈다. */ reconcile: () => Promise; } /** * 계획선 편집(▲/▼) — B05 와 **같은 세션 초안**에 쌓는다(2026-09-12 사용자: B06 에만 * 버튼이 없었다). 누르면 그 자리에서 계획선·전 측점 횡단을 다시 풀고(`reconcile`), * 영구저장은 [저장]·[확정]에서만 한다(CLAUDE.md 5장). */ export function createGradeEdit(ctx: GradeEditContext): GradeEditProvider { let gradeStore: ProfileEditStore | null = null; let gradeRouteId: number | null = null; /** ▲▼ 길게 누르기는 초당 10회(`HOLD_INTERVAL_MS`) 들어온다 — 그보다 길게 잡아 * 누르는 동안은 선만 움직이고, 손을 뗀 뒤 재계산이 한 번 돈다. */ const GRADE_RECONCILE_DEBOUNCE_MS = 120; let gradeReconcileTimer = 0; const scheduleGradeReconcile = (): void => { window.clearTimeout(gradeReconcileTimer); gradeReconcileTimer = window.setTimeout(() => { void ctx.reconcile().then(() => ctx.view().setGradeEdit(gradeEditFor)); }, GRADE_RECONCILE_DEBOUNCE_MS); }; const gradeEditFor: GradeEditProvider = () => { const detail = ctx.detail(); const currentRouteId = ctx.routeId(); if (!detail || currentRouteId === null) return null; const stored = readAlignment(detail.longitudinal); if (!stored) return null; // 선형 저장분이 없는 옛 노선 — 편집할 기준선이 없다. if (!gradeStore || gradeRouteId !== currentRouteId) { gradeRouteId = currentRouteId; gradeStore = createProfileEditStore(currentRouteId, stored.edits, () => undefined); } const store = gradeStore; const base = toAlignmentBase(stored); const alignment = buildAlignment(base, store.edits()); return { alignment, stepM: alignment.policy.edit_step_m, onStation: (chainageM, delta) => { store.replace(adjustStation(base, store.edits(), chainageM, delta)); // ① 선은 **그 자리에서** 움직인다 — 그래프가 읽는 계획선(`design_profiles`)만 // 갈아 끼우고 다시 그린다(재계산을 기다리면 누른 뒤 한참 뒤에 움직였다). const current = ctx.detail(); if (current) { current.longitudinal.design_profiles = [ toDesignProfile( buildAlignment(base, store.edits()), current.longitudinal.design_profiles?.[0], ), ]; } ctx.view().setGradeEdit(gradeEditFor); // ② 전 측점 횡단 재계산·카드 갱신은 무겁다 — 마지막 한 번만(B05 프리뷰와 같은 규칙). // 편집분은 세션 초안에 있으므로 재계산이 그것을 그대로 읽는다. scheduleGradeReconcile(); }, }; }; return gradeEditFor; }