refactor(B05,B06): 설계 초안·계산 결과도 등록표로 — 이동할 때 다시 부르지 않게

캐시·세션 일원화 2단계 (2026-09-06 사용자 지시).

- ② 설계 초안 이관 — 구조물 미저장분, 상단측 변경분, 페이지 간 넘김값(구조물 선택),
  표준횡단 편집값, 조정창 4축·단수·연동값 8종, 암 경계선, 표시 반폭. 넘김값은 별도
  임시 키를 없애고 초안 한 자리로 흡수.
- ④ 계산 결과 이관 — 최신 노선 응답, 서버가 준 규격 횡단·암 경계 기본값. 횡단 상세는
  메모리 캐시에 더해 세션에도 한 벌 얹어 새로고침 뒤 첫 화면이 서버를 안 기다림
  (용량을 넘으면 조용히 건너뛰고 예전대로 다시 받음).
- B05 진입의 강제 새로읽기를 걷어냄 — 캐시로 먼저 그리고 노선번호·지표면 서명으로
  뒤에서 신선도만 확인, 달라졌을 때만 다시 그림.
- [초기화]의 초안 비우기를 `clearDrafts` 한 곳으로 모음(예전에는 파일마다 따로).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 11:48:40 +09:00
co-authored by Claude Opus 5
parent 05a63c8440
commit a126df25bb
10 changed files with 131 additions and 161 deletions
+23 -13
View File
@@ -108,30 +108,40 @@ export const STATE_REGISTRY = {
rockb: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:rockb:${p}:${r}` },
/** 규격 횡단 측점별 지정. */
"std-cross": { bucket: "draft", scope: "project", legacy: (p) => `b06:std-cross:${p}` },
/** 규격 횡단 기본값(프로젝트 단위). */
"std-cross-default": {
bucket: "draft",
scope: "project",
legacy: (p) => `b06:std-cross-default:${p}`,
},
/** 암 경계선 기본값(프로젝트 단위). */
"rock-boundary-default": {
bucket: "draft",
scope: "project",
legacy: (p) => `b06:rock-boundary-default:${p}`,
},
/** 표시 반폭 — 화면 값이지만 횡단 재생성을 부르므로 노선에 묶는다. */
/** 표시 반폭 — 사용자가 고른 값이라 초안이되, 바꾸면 횡단 재생성(③)을 함께 부른다. */
"cross-display": {
bucket: "draft",
scope: "route",
legacy: (p, r) => `b06:cross-display:${p}:${r}`,
},
/* 측점 조정창이 쌓는 4축·단수·연동 값 — 전부 노선 단위 초안이고 [저장]에서 함께 나간다.
옛 키는 `b06:{이름}:{프로젝트}:{노선}` 한 규칙이었다. */
crossw: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:crossw:${p}:${r}` },
revetx: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:revetx:${p}:${r}` },
revetlink: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:revetlink:${p}:${r}` },
basinadjust: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:basinadjust:${p}:${r}` },
inletstruct: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:inletstruct:${p}:${r}` },
extrawall: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:extrawall:${p}:${r}` },
fordadjust: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:fordadjust:${p}:${r}` },
boxadjust: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:boxadjust:${p}:${r}` },
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
latest: { bucket: "result", scope: "project", legacy: (p) => `b05:latest:${p}` },
/** 횡단 상세 응답 — 예전에는 메모리에만 있어 페이지를 떠나면 사라졌다. */
"section-detail": { bucket: "result", scope: "route" },
/** 서버가 준 규격 횡단 기본값 — 사용자가 만든 값이 아니라 **기억해 둔 서버 값**이다. */
"std-cross-default": {
bucket: "result",
scope: "project",
legacy: (p) => `b06:std-cross-default:${p}`,
},
/** 서버가 준 암 경계선 기본 오프셋 — 위와 같은 성격. */
"rock-boundary-default": {
bucket: "result",
scope: "project",
legacy: (p) => `b06:rock-boundary-default:${p}`,
},
} as const satisfies Record<string, StateEntry>;
export type StateName = keyof typeof STATE_REGISTRY;
+9 -26
View File
@@ -11,6 +11,8 @@
* - 오류 응답 형식 {status:"error", message:"..."}을 Error로 변환.
* ========================================================================== */
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
/** 경로 제어점 (BP/EP/CP) */
@@ -292,39 +294,20 @@ export async function fetchLatestRoute(projectId: string): Promise<RouteLatestRe
});
}
/** B05가 최신 경로·확정 설정값을 탭 세션에 담아 둘 때 쓰는 키(유일한 정의처). */
export const routeLatestCacheKey = (projectId: string): string => `b05:latest:${projectId}`;
/* 최신 경로 응답은 ④ 계산 결과다 — 키·이관은 등록표(`b_page_state`)가 맡는다.
B04에서 지표면을 다시 확정하면 옛 확정값이 남아 B05가 이전 지형을 그리므로, 확정
직후 `clearRouteLatestCache`로 버린다. */
/** 담아 둔 최신 경로 값을 버린다. B04에서 지표면을 다시 확정하면 옛 확정값이 남아
* B05가 이전 지형을 그리게 되므로, 확정 직후 이 값을 지운다. */
/** 세션 캐시에서 최신 경로 응답을 읽는다. 없거나 깨졌으면 null(다음 진입은 DB 조회). */
export function readRouteLatestCache(projectId: string): RouteLatestResponse | null {
try {
const raw = window.sessionStorage.getItem(routeLatestCacheKey(projectId));
return raw ? (JSON.parse(raw) as RouteLatestResponse) : null;
} catch {
return null;
}
return readState<RouteLatestResponse>("latest", projectId);
}
/** 최신 경로 응답을 세션 캐시에 넣는다. 용량 초과 등으로 실패하면 캐시를 비운다. */
/** 최신 경로 응답을 세션 캐시에 넣는다. 용량 초과 등으로 실패해도 화면은 그대로 돈다. */
export function writeRouteLatestCache(projectId: string, value: RouteLatestResponse): void {
const key = routeLatestCacheKey(projectId);
try {
window.sessionStorage.setItem(key, JSON.stringify(value));
} catch {
try {
window.sessionStorage.removeItem(key);
} catch {
/* noop */
}
}
writeState("latest", value, projectId);
}
export function clearRouteLatestCache(projectId: string): void {
try {
window.sessionStorage.removeItem(routeLatestCacheKey(projectId));
} catch {
/* 세션 접근 실패 시에는 다음 진입에서 DB를 읽게 되므로 그대로 둔다. */
}
clearState("latest", projectId);
}
+4 -16
View File
@@ -11,6 +11,7 @@
* 하려는 것이라, 목록은 반드시 서버에서 받아 온다.
* ========================================================================== */
import { readState, writeState } from "../A00_Common/b_page_state";
import { API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
/** 배치형태 — 점형(측점 1개) / 구간형(시~종점) / 부지형(위치+면적). */
@@ -193,26 +194,13 @@ export function defaultOptions(type: StructureType): Record<string, string | num
* 2026-08-29 사용자 확정). B05에서 만지고 B06으로 넘어가 확정하는 경로가 있어
* 읽기·쓰기·내보내기를 여기 한 곳에 둔다. */
const pendingStructuresKey = (projectId: string): string => `b05:structures:${projectId}`;
/** 미저장 조작분. 없으면 null(= 만진 적 없음, 빈 목록과 구분된다). */
/** 미저장 조작분(② 설계 초안). 없으면 null(= 만진 적 없음, 빈 목록과 구분된다). */
export function readPendingStructures(projectId: string): StructureInstance[] | null {
try {
const raw = window.sessionStorage.getItem(pendingStructuresKey(projectId));
return raw ? (JSON.parse(raw) as StructureInstance[]) : null;
} catch {
return null;
}
return readState<StructureInstance[]>("structures", projectId);
}
export function writePendingStructures(projectId: string, next: StructureInstance[] | null): void {
try {
const key = pendingStructuresKey(projectId);
if (next) window.sessionStorage.setItem(key, JSON.stringify(next));
else window.sessionStorage.removeItem(key);
} catch {
/* 세션 저장 실패는 무시 — 값은 화면에 남아 있다. */
}
writeState("structures", next, projectId);
}
/**
+36 -2
View File
@@ -233,6 +233,35 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
return fresh;
}
/** 이 응답이 "같은 자료"인지 가리는 서명 — 노선 번호·지표면 모델·지표면 설정. */
const latestSignature = (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 캐시·세션 일원화).
*
* 예전에는 진입 때마다 `loadLatest(true)`로 DB를 다시 읽었다 — 다른 탭의 재업로드로
* 옛 노선번호가 남을까 봐 넣은 안전장치인데, 그 탓에 B05↔B06을 오갈 때마다 화면이
* 처음부터 다시 섰다. 이제 서명(노선번호·지표면)이 달라졌을 때만 다시 그린다.
*/
async function verifyLatestFreshness(shown: RouteLatestResponse): Promise<void> {
try {
const fresh = await fetchLatestRoute(activeProjectId);
writeLatestCache(fresh);
if (latestSignature(fresh) === latestSignature(shown)) return;
renderLatest(fresh);
if (fresh.route?.id) await restoreSections(fresh.route.id);
} catch {
/* 확인 실패는 조용히 넘긴다 — 화면은 캐시로 이미 서 있다. */
}
}
const panel = createRoutePanel({
onSolve: () => void solveRouteAction(actionContext),
onTempSave: () => void tempSaveAction(actionContext),
@@ -618,9 +647,11 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
try {
// ② 좌측 폼·노선 설정값 — 도착하는 대로 폼과 3D 마커 복원에 쓴다.
// 캐시가 있으면 그것으로 **먼저** 그리고 신선도는 뒤에서 확인한다 — B05↔B06 이동에서
// 서버를 다시 부르지 않으려는 것이다(2026-09-06). 캐시가 없을 때만 DB를 기다린다.
const cachedLatest = readLatestCache();
const [latestResponse, sectionContext, configuredRoadWidths] = await Promise.all([
// 다른 탭의 재업로드로 옛 route_id가 남을 수 있어 진입 때는 DB 최신값을 읽는다.
loadLatest(true),
cachedLatest ? Promise.resolve(cachedLatest) : loadLatest(true),
fetchSectionContext(activeProjectId),
fetchRoadWidths(activeProjectId),
]);
@@ -666,6 +697,9 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
// 구조물 타입 레지스트리·정본 — 노선이 없어도 목록은 보여 준다(추가는 노선 이후).
await bridge.load();
advanceLoading("");
// 캐시로 그렸다면 이제 뒤에서 신선도만 확인한다 — 화면은 이미 서 있으므로 기다리지
// 않는다. 다른 탭이 자료를 갈아 끼웠을 때만 다시 그린다.
if (cachedLatest) void verifyLatestFreshness(latestResponse);
} catch (error) {
showToast(error instanceof Error ? error.message : "화면을 불러오지 못했습니다.", "error");
} finally {
+5 -5
View File
@@ -29,7 +29,7 @@ import {
invalidateSectionDetail,
saveCachedCrossPatches,
} from "../B06_Section/B06_Section_Section_Store";
import { clearStandardCrossSession } from "../B06_Section/B06_Section_UI_Standard_Panel";
import { clearDrafts } from "../A00_Common/b_page_state";
import { saveCorridorIfDirty } from "./B05_Profile_UI_Corridor";
import { circlePoint, routePoint } from "./B05_Profile_UI_Page_Helpers";
import { clearAlignmentDrafts } from "./B05_Profile_UI_Profile_Edit";
@@ -202,11 +202,11 @@ export async function resetDesignAction(ctx: PageActionContext): Promise<void> {
// 옛 경로 기준 캐시를 전부 비우고 페이지를 새로 그린다 — 초기 계산본이 정본이 된다.
clearRouteLatestCache(ctx.projectId);
invalidateSectionDetail(ctx.projectId);
// 프로젝트 단위 세션 값도 함께 버린다 — 키에 route_id가 없어 새 노선에 그대로
// 되붙는다(2026-08-28). 초기화는 "사용자 편집을 전부 버린다"가 규약이다.
// 사용자 조작(② 초안)은 **등록표 한 곳**에서 통째로 버린다(2026-09-06 일원화).
// 예전에는 파일마다 따로 지워 새 값이 늘 때 빠뜨리기 쉬웠다. 노선 범위 초안은
// 노선이 바뀌면 키가 달라져 자연히 딸려 오지 않는다.
clearDrafts(ctx.projectId, ctx.latest()?.route?.id ?? null);
ctx.uphillOverrides.clear();
ctx.persistUphillOverrides();
clearStandardCrossSession(ctx.projectId);
// 계획선 편집 초안도 함께 버린다 — 남기면 초기값 위에 옛 편집이 다시 얹혀 계획선이
// 측점에서 원지반선과 만나지 않는다(2026-09-04 실측: 새로고침 후 최대 6.0m 어긋남).
clearAlignmentDrafts();
+7 -13
View File
@@ -7,6 +7,7 @@
* 전부 상태를 갖지 않는 변환 함수라 화면 흐름과 독립적이다.
* ========================================================================== */
import { readState, writeState } from "../A00_Common/b_page_state";
import type { RoutePanelValues } from "./B05_Profile_UI_Panel";
import type {
ModelBounds,
@@ -246,19 +247,15 @@ export const FACILITY_NAMES: Record<PipeFacility, string> = {
/* ── 측점 상단측(=측구 방향) 사용자 변경분 세션 보관 ─────────────────────
* 3D 램프 클릭으로 바꾼 값. 경로 확정 때 uphill_overrides로 백엔드에 병합한다.
* 화면 본체가 700줄에 닿아 읽기·쓰기만 여기로 뺐다(2026-09-04, 동작 불변). */
const uphillSessionKey = (projectId: string): string => `b05:uphill:${projectId}`;
/** 세션에 남은 상단측 변경분을 읽는다. 손상된 값은 무시하고 빈 것으로 시작한다. */
export function loadUphillOverrides(projectId: string): Map<string, "left" | "right"> {
const overrides = new Map<string, "left" | "right">();
try {
const raw = window.sessionStorage.getItem(uphillSessionKey(projectId));
if (!raw) return overrides;
Object.entries(JSON.parse(raw) as Record<string, "left" | "right">).forEach(
([chainage, side]) => {
if (side === "left" || side === "right") overrides.set(chainage, side);
},
);
const stored = readState<Record<string, "left" | "right">>("uphill", projectId);
if (!stored) return overrides;
Object.entries(stored).forEach(([chainage, side]) => {
if (side === "left" || side === "right") overrides.set(chainage, side);
});
} catch {
/* 손상된 세션 값은 무시 — 자동 판정값으로 재시작. */
}
@@ -271,10 +268,7 @@ export function saveUphillOverrides(
overrides: ReadonlyMap<string, "left" | "right">,
): void {
try {
window.sessionStorage.setItem(
uphillSessionKey(projectId),
JSON.stringify(Object.fromEntries(overrides)),
);
writeState("uphill", Object.fromEntries(overrides), projectId);
} catch {
/* 세션 저장 실패는 무시 — 값은 메모리에 유지된다. */
}
@@ -9,6 +9,7 @@
* 적혀, 어느 쪽으로 오가든 마지막 선택이 그대로 살아 있다.
* ========================================================================== */
import { readState, writeState } from "../A00_Common/b_page_state";
import type { StructurePick, StructurePickControls } from "./B05_Profile_UI_Viewer_Structure_Pick";
import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types";
import type { StructureInstance } from "./B05_Profile_Api_Structures";
@@ -19,21 +20,15 @@ export interface StructurePickHandoff {
key?: string;
}
const sessionKey = (projectId: string): string => `aislo:structure-pick:${projectId}`;
/* 넘김값도 ② 설계 초안이다 — 별도 임시 키를 두지 않고 등록표(`b_page_state`)를 쓴다
(2026-09-06 캐시·세션 일원화). */
/** 세션에 남긴다(선택 해제면 지운다). */
export function rememberStructurePick(projectId: string | null, pick: StructurePick | null): void {
if (!projectId) return;
try {
if (!pick) {
window.sessionStorage.removeItem(sessionKey(projectId));
return;
}
const handoff: StructurePickHandoff = { at: pick.chainageM, key: pick.key };
window.sessionStorage.setItem(sessionKey(projectId), JSON.stringify(handoff));
} catch {
/* 세션 저장소가 막힌 환경 — 화면 선택만 살고 넘김은 포기한다. */
}
if (!pick) return writeState("structure-pick", null, projectId);
const handoff: StructurePickHandoff = { at: pick.chainageM, key: pick.key };
writeState("structure-pick", handoff, projectId);
}
/** 화면에서 고른 것을 세션에 적는다 — B06 쪽 창구(측점만 고르면 부재키는 비운다). */
@@ -43,21 +38,15 @@ export function writeStructurePick(
key?: string,
): void {
if (!projectId) return;
try {
if (at === null) window.sessionStorage.removeItem(sessionKey(projectId));
else window.sessionStorage.setItem(sessionKey(projectId), JSON.stringify({ at, key }));
} catch {
/* 세션 저장소가 막힌 환경 — 화면 선택만 살고 넘김은 포기한다. */
}
writeState("structure-pick", at === null ? null : { at, key }, projectId);
}
/** 세션에 남은 선택을 읽는다(지우지 않는다). 없으면 null. */
export function readStructurePick(projectId: string | null): StructurePickHandoff | null {
if (!projectId) return null;
try {
const raw = window.sessionStorage.getItem(sessionKey(projectId));
if (!raw) return null;
const value = JSON.parse(raw) as Partial<StructurePickHandoff>;
const value = readState<Partial<StructurePickHandoff>>("structure-pick", projectId);
if (!value) return null;
if (typeof value.at !== "number" || !Number.isFinite(value.at)) return null;
return { at: value.at, key: typeof value.key === "string" ? value.key : undefined };
} catch {
+17 -3
View File
@@ -10,11 +10,14 @@
* 다음 그리기에서 그대로 본다 — 별도 동기화 코드가 필요 없다.
*
* ── 수명 규칙 ─────────────────────────────────────────────────────
* 키는 `projectId:routeId`. SPA 모듈 싱글턴이라 페이지를 오가도 살아 있고, 새로고침이면
* 사라져 영구저장소에서 다시 받는다(영구저장소가 항상 정본). 서버가 파일을 통째로 다시
* 키는 `projectId:routeId`. SPA 모듈 싱글턴이라 페이지를 오가도 살아 있다. 새로고침이면
* 메모리가 비므로 **세션에도 한 벌 얹어 둔다**(④ 계산 결과, 2026-09-06 캐시·세션 일원화)
* — 새로고침 뒤 첫 화면이 서버를 기다리지 않는다. 세션 용량을 넘으면 조용히 건너뛰고
* 예전처럼 영구저장소에서 다시 받는다(영구저장소가 항상 정본). 서버가 파일을 통째로 다시
* 쓰는 조작(재생성·계획선 편집 저장·확정)은 그 응답/재조회로 `replace`·`invalidate`한다.
* ========================================================================== */
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
import type { CrossSectionPatch, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import { fetchSectionDetail, saveSections } from "./B06_Section_Api_Fetch";
@@ -40,10 +43,17 @@ export async function loadSectionDetail(
if (cached) return cached;
const inFlight = pending.get(key);
if (inFlight) return inFlight;
// 새로고침으로 메모리가 빈 경우 — 세션에 얹어 둔 한 벌로 바로 선다.
const stored = readState<SectionDetailResponse>("section-detail", projectId, routeId);
if (stored) {
cache.set(key, stored);
return stored;
}
}
const request = fetchSectionDetail(projectId, routeId)
.then((detail) => {
cache.set(key, detail);
writeState("section-detail", detail, projectId, routeId);
return detail;
})
.finally(() => {
@@ -60,6 +70,7 @@ export function replaceSectionDetail(
detail: SectionDetailResponse,
): void {
cache.set(keyOf(projectId, routeId), detail);
writeState("section-detail", detail, projectId, routeId);
}
/**
@@ -69,11 +80,14 @@ export function replaceSectionDetail(
export function invalidateSectionDetail(projectId: string, routeId?: number): void {
if (routeId !== undefined) {
cache.delete(keyOf(projectId, routeId));
clearState("section-detail", projectId, routeId);
return;
}
const prefix = `${projectId}:`;
for (const key of [...cache.keys()]) {
if (key.startsWith(prefix)) cache.delete(key);
if (!key.startsWith(prefix)) continue;
cache.delete(key);
clearState("section-detail", projectId, Number(key.slice(prefix.length)));
}
}
+5 -10
View File
@@ -1,5 +1,6 @@
import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
import { leaveForDashboard } from "../A00_Common/b_missing_data_guard";
import { stateKey, type StateName } from "../A00_Common/b_page_state";
import { navigateTo } from "../A00_Common/router";
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
@@ -24,7 +25,6 @@ import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
import {
confirmCurrentSections,
createRockBoundaryStore,
rockBoundarySessionKey,
saveCurrentSections,
type SectionPersistContext,
} from "./B06_Section_UI_Page_Persist";
@@ -391,10 +391,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
// 암 경계선 오프셋(측점별) 세션 저장소는 저장 흐름 모듈이 맡는다(2026-09-02 분리).
const rockStore = createRockBoundaryStore({
sessionKey: () =>
projectId && currentRouteId !== null
? rockBoundarySessionKey(projectId, currentRouteId)
: null,
sessionKey: () => stateKey("rockb", projectId, currentRouteId),
detail: () => sectionDetail,
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
recompute: (chainageM) => recomputeIfRock(chainageM),
@@ -403,8 +400,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
const rockBoundaryControl = rockStore.control;
const stationControls = createStationControls({
sessionKey: (kind) =>
projectId && currentRouteId !== null ? `b06:${kind}:${projectId}:${currentRouteId}` : null,
// 키는 등록표(`b_page_state`)가 만든다 — 이름만 넘기면 통·범위·옛 키 이관이 따라온다.
sessionKey: (kind) => stateKey(kind as StateName, projectId, currentRouteId),
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
detail: () => sectionDetail,
crossHalfWidth,
@@ -505,9 +502,7 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
/** 표시 반폭 세션 키 — 페이지를 떠났다 와도 조절값이 유지되게 한다. */
const displaySessionKey = (): string | null =>
projectId && currentRouteId !== null
? `b06:cross-display:${projectId}:${currentRouteId}`
: null;
stateKey("cross-display", projectId, currentRouteId);
function persistDisplayHalfWidth(): void {
const key = displaySessionKey();
+16 -53
View File
@@ -17,6 +17,13 @@
* 카드에서 이뤄진다(여기서는 값만 보관).
* ========================================================================== */
import {
clearState,
readState,
readStateRaw,
writeState,
writeStateRaw,
} from "../A00_Common/b_page_state";
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements";
import { buildStandardDiagram } from "./B06_Section_UI_Standard_Diagram";
@@ -32,18 +39,6 @@ function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
const SESSION_PREFIX = "b06:std-cross:";
/** config 기본값 사본 — 편집값(`SESSION_PREFIX`)과 수명은 같되 용도가 다르다. */
const DEFAULTS_PREFIX = "b06:std-cross-default:";
function sessionKey(projectId: string): string {
return `${SESSION_PREFIX}${projectId}`;
}
function defaultsKey(projectId: string): string {
return `${DEFAULTS_PREFIX}${projectId}`;
}
/**
* 서버가 내려 준 표준횡단 **config 기본값**을 세션에 둔다(`sections/context` 응답).
*
@@ -52,47 +47,28 @@ function defaultsKey(projectId: string): string {
* 패널을 열지 않는 B05도 이 값으로 계산해야 두 화면 결과가 같다(2026-09-03 로컬 전환).
*/
export function rememberStandardDefaults(projectId: string, defaults: StandardCrossSection): void {
try {
window.sessionStorage.setItem(defaultsKey(projectId), JSON.stringify(defaults));
} catch {
/* 세션 저장 실패는 무시 — 계산은 편집값·저장분으로 이어 간다. */
}
writeState("std-cross-default", defaults, projectId);
}
/** 기억해 둔 config 기본값. 아직 컨텍스트를 못 받았으면 null. */
export function readStandardDefaults(projectId: string): StandardCrossSection | null {
try {
const raw = window.sessionStorage.getItem(defaultsKey(projectId));
return raw ? (JSON.parse(raw) as StandardCrossSection) : null;
} catch {
return null;
}
return readState<StandardCrossSection>("std-cross-default", projectId);
}
const ROCK_DEFAULT_PREFIX = "b06:rock-boundary-default:";
/**
* 암반 경계선 기본 오프셋(config `STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M`)을 기억한다.
* 저장분에 경계 오프셋이 없는 옛 암 측점을 서버와 **같은 기본값**으로 다시 계산하려면
* 브라우저에도 이 값이 있어야 한다 — 상수 복제 대신 컨텍스트 응답을 기억하는 방식이다.
*/
export function rememberRockBoundaryDefault(projectId: string, offsetM: number): void {
try {
window.sessionStorage.setItem(`${ROCK_DEFAULT_PREFIX}${projectId}`, String(offsetM));
} catch {
/* 세션 저장 실패는 무시. */
}
writeStateRaw("rock-boundary-default", String(offsetM), projectId);
}
/** 기억해 둔 암반 경계 기본 오프셋(m). 없으면 null. */
export function readRockBoundaryDefault(projectId: string): number | null {
try {
const raw = window.sessionStorage.getItem(`${ROCK_DEFAULT_PREFIX}${projectId}`);
const parsed = raw === null ? Number.NaN : Number(raw);
return Number.isFinite(parsed) ? parsed : null;
} catch {
return null;
}
const raw = readStateRaw("rock-boundary-default", projectId);
const parsed = raw === null ? Number.NaN : Number(raw);
return Number.isFinite(parsed) ? parsed : null;
}
/**
@@ -105,11 +81,7 @@ export function effectiveStandardCross(projectId: string): StandardCrossSection
/** 표준횡단 세션 편집값을 버린다 — B05 [초기화]가 부른다(정의처를 여기 하나로 둔다). */
export function clearStandardCrossSession(projectId: string): void {
try {
window.sessionStorage.removeItem(sessionKey(projectId));
} catch {
/* 세션 접근이 막혀도 초기화는 계속한다. */
}
clearState("std-cross", projectId);
}
/** config 기본값을 깊은 복사해 편집용 초기 상태로 만든다. */
@@ -124,20 +96,11 @@ function cloneDefaults(defaults: StandardCrossSection): StandardCrossSection {
* 패널이 없는 화면에서도 **같은 표준 단면값**을 서버로 보내야 두 화면 결과가 같다.
*/
export function readStandardCrossSession(projectId: string): StandardCrossSection | null {
try {
const raw = window.sessionStorage.getItem(sessionKey(projectId));
return raw ? (JSON.parse(raw) as StandardCrossSection) : null;
} catch {
return null;
}
return readState<StandardCrossSection>("std-cross", projectId);
}
function writeSession(projectId: string, value: StandardCrossSection): void {
try {
window.sessionStorage.setItem(sessionKey(projectId), JSON.stringify(value));
} catch {
/* 세션 저장 실패는 무시(용량 초과 등) — 값은 메모리에 유지된다. */
}
writeState("std-cross", value, projectId);
}
export interface StandardPanelController {