Files
Aislo/B05_Profile/B05_Profile_UI_Page_Latest.ts
T
eomsangdonandClaude Opus 5.5 990832d629 refactor(472-5 5-C 값 주인 하나): 종단 편집 · 페이지 설정 = 설계 가게 칸 — ↶ ↷ 뒤 두 보기 · 폼 · 기울기 한도 같은 값(N6 · N7)
- 종단 편집: 모듈 사본 EditState 걷음 — 지금 값 = 가게 alignmentDraft(세션) ?? 새 칸 alignmentSaved(노선별 서버 저장분 · 메모리)
  · ↶ ↷ = 다시 읽기만(resync · 같은 값이면 다시 셈 없음) · [저장] 뒤 ↶ 가 저장 전 서버 값으로 안 돌아감 · 노선 없는 창구는 그 안 메모리
- 페이지 설정: 가게 칸 pageSettings(세션 pagesettings) · ↶ ↷ 훅이 가게로 다시 알림 → 폼(B05) · 기울기 한도 · 시작 측점(종단도 한 벌)이 들음(N6)
- 최소고 강제: 결과 통 사본 칸 profile-enforce-min-cover 지움 — 편집 가드가 페이지 설정(초안 ?? 저장 노선)을 바로 읽음(N7)
- 시험: test_472_5_value_owner(옛 코드 빨강 3) · 헬퍼 271 · 442_2 가게 창구 따라감 · 최소고 가드 글 검사 줄
- 위키 design_page 값 · 조작 · 세션 열쇠 · 규칙 줄

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:35:58 +09:00

128 lines
6.2 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Page_Latest.ts
* 종단 보기의 **최신 노선 · 설정값** 창구 — 세션 캐시 우선 읽기 · 뒤에서 신선도 확인 · 좌측 폼 되살리기
* 값. 종단 보기 본체(`design_view_profile`)가 700줄을 넘어 여기로 뗐다(계획서 24-3b).
* ========================================================================== */
import {
fetchLatestRoute,
readRouteLatestCache,
writeRouteLatestCache,
type RouteLatestResponse,
} from "./B05_Profile_Api_Fetch";
import { panelValuesOfDraft, readPageSettings } from "./B05_Profile_Api_PageSettings";
import type { RoutePanelValues } from "./B05_Profile_UI_Panel";
import { SPEED_CHOICES, gradeLimitOf } from "./B05_Profile_UI_Panel_Fields";
import type { DesignProfile } from "../A00_Common/design/design_profile";
/**
* 확정 이력이 있으면 매 진입마다 DB(latest) 조회 대신 브라우저 세션 캐시를 우선 사용해
* 응답속도를 높인다. 캐시 미스면 latest를 조회해 적재하고, solve·확정 성공 시 신선한 값으로
* 갱신한다(세션 = 탭 단위, 탭 종료 시 소멸).
*/
export function createLatestSource(projectId: string) {
const read = (): RouteLatestResponse | null => readRouteLatestCache(projectId);
const write = (value: RouteLatestResponse): void => writeRouteLatestCache(projectId, value);
/** 캐시 우선 latest 로드. forceFresh=true(solve/확정 직후)는 항상 DB를 읽고 캐시를 갱신한다. */
async function load(forceFresh = false): Promise<RouteLatestResponse> {
if (!forceFresh) {
const cached = read();
if (cached) return cached;
}
const fresh = await fetchLatestRoute(projectId);
write(fresh);
return fresh;
}
/** 이 응답이 "같은 자료"인지 가리는 서명 — 노선 번호·지표면 모델·지표면 설정. */
const signature = (value: RouteLatestResponse): string =>
[
value.route?.id ?? "",
value.route?.surface_model_id ?? "",
value.surface_params?.source_filter,
value.surface_params?.method,
String(value.surface_params?.smooth),
].join("|");
/**
* 캐시로 먼저 그린 뒤 **뒤에서** 신선도를 확인한다(2026-09-06 캐시·세션 일원화).
* 서명(노선번호·지표면)이 달라졌을 때만 `onChanged` 로 다시 그린다.
*/
async function verify(
shown: RouteLatestResponse,
onChanged: (fresh: RouteLatestResponse) => Promise<void>,
): Promise<void> {
try {
const fresh = await fetchLatestRoute(projectId);
write(fresh);
if (signature(fresh) === signature(shown)) return;
await onChanged(fresh);
} catch {
/* 확인 실패는 조용히 넘긴다 — 화면은 캐시로 이미 서 있다. */
}
}
return { read, write, load, verify };
}
/** 저장된 노선 설정값 → 좌측 폼 되살리기 값 — 세션 페이지 설정 초안이 있으면 그 위에(448).
* 시 · 종점 계획고 조정은 종단 편집이 비추므로 여기서 안 채움(옛 params 값은 자동 선형에 이미 듦). */
export function panelValuesOf(next: RouteLatestResponse): Partial<RoutePanelValues> {
const options = next.route_params?.options ?? {};
const draft = readPageSettings(next.project_id);
return {
contourInterval: next.surface_params?.contour_interval_m, // 없음(옛 · 시험 노선) = 패널 값 그대로
gradeClass: options.grade_class as RoutePanelValues["gradeClass"] | undefined,
stationInterval: next.route_params?.station_interval_m ?? undefined,
crossSampleInterval: next.route_params?.cross_sample_interval_m ?? undefined,
longSampleInterval: next.route_params?.long_sample_interval_m ?? undefined,
terrainType: options.terrain_type as RoutePanelValues["terrainType"] | undefined,
designSpeed: options.design_speed_kph as RoutePanelValues["designSpeed"] | undefined,
maxGradePct: next.route_params?.max_grade_pct ?? undefined,
minVerticalRadius: next.route_params?.min_vertical_radius_m ?? undefined,
minTangentLength: next.route_params?.min_tangent_length_m ?? undefined,
enforcePipeClearance:
(next.route_params?.enforce_pipe_clearance as boolean | undefined) ?? undefined,
stationDisplay: next.route_params?.station_display ?? undefined,
...(draft ? panelValuesOfDraft(draft) : {}),
};
}
/** 폼의 페이지 설정 칸만 — 초안 ?? 저장 노선(빈 기준값 = 빈 칸 · 강제 = 해제) · ↶ ↷ 뒤 폼에 다시 얹음(N6 · 472-5). */
export function pageSettingsPanelValues(projectId: string): Partial<RoutePanelValues> | null {
const latest = readRouteLatestCache(projectId);
const draft = readPageSettings(projectId);
if (!latest) return draft ? panelValuesOfDraft(draft) : null;
const all = panelValuesOf(latest);
return {
designSpeed: all.designSpeed,
terrainType: all.terrainType,
maxGradePct: all.maxGradePct ?? null,
minVerticalRadius: all.minVerticalRadius ?? null,
minTangentLength: all.minTangentLength ?? null,
enforcePipeClearance: all.enforcePipeClearance === true,
stationDisplay: all.stationDisplay ?? { station: 0, cumulative: 0 },
};
}
/** 페이지 설정 한 칸(463 · 요구 271-5) — 종단도 기울기 상한 · 시작 측점 = 세션 초안(448) ?? 저장 노선
* 설정(빈 칸 = 좌측 패널 기본). 어느 보기로 서도 종단도를 그린 뒤 얹음 · B05 패널은 세션에 쓰고 고친 즉시 얹음. */
export function applyPageSettings(
profile: Pick<DesignProfile, "setGradeLimit" | "setStationDisplay">,
projectId: string,
latest: RouteLatestResponse | null = readRouteLatestCache(projectId),
): void {
const draft = readPageSettings(projectId);
const values = latest ? panelValuesOf(latest) : draft ? panelValuesOfDraft(draft) : null;
if (!values) return; // 모름 — 종단도 그대로(서버 경고 기준)
const allowed = SPEED_CHOICES[values.gradeClass ?? "trunk"] ?? [20];
const speed = allowed.includes(values.designSpeed ?? 20)
? (values.designSpeed ?? 20)
: allowed[0];
profile.setGradeLimit(
gradeLimitOf(speed, values.terrainType ?? "special", values.maxGradePct ?? null),
);
profile.setStationDisplay(values.stationDisplay ?? { station: 0, cumulative: 0 });
}