refactor(B05,B06): 화면 상태 보관소를 한 곳으로 — 등록표 신설 + 취향값 이관
2026-09-06 사용자 지시(캐시·세션 일원화 1단계). 세션 접근이 19개 파일 64곳에
흩어져 있고 키 이름이 네 갈래라 같은 설계값인데 두 화면이 서로의 값을 못 보는
자리가 있었다.
- `A00_Common/b_page_state.ts` 신설 — 값마다 통(① 취향 · ② 초안 · ④ 계산 결과)과
범위(전역·프로젝트·노선)를 한 줄로 적는 등록표. 키 형식은
`aislo:{통}:{이름}:{프로젝트}[:{노선}]` 하나이고 **페이지 이름을 키에 넣지 않는다**.
- 옛 키는 처음 읽을 때 새 키로 한 번 옮기고 지운다(사용자가 쓰던 배치 유지).
- 초안 비우기(`clearDrafts`)·결과 버리기(`clearResults`)를 그 한 곳에 둠.
- 이번 커밋은 ① 취향값 11개를 이관 — 패널 접힘·높이, 유토곡선 펼침·높이·범례,
테이블 펼침·높이, 배수유역 접힘·폭, B06 패널 접힘·높이.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/b_page_state.ts
|
||||
* 화면 상태 보관소 **한 곳** — B05·B06 이 한 페이지처럼 움직이기 위한 뼈대
|
||||
* (2026-09-06 사용자 지시).
|
||||
*
|
||||
* 왜 필요한가 — 세션 접근이 19개 파일 64곳에 흩어져 있었고 키 이름이 네 갈래
|
||||
* (`b05:`, `b06:`, `b05-route-`, `aislo:`)로 제각각이었다. 같은 설계값인데 키가
|
||||
* 페이지 이름으로 갈려 두 화면이 서로의 값을 못 보는 자리도 있었다. 값마다 **어느 통에
|
||||
* 속하는지**를 아래 등록표에 한 줄로 적어 두고, 저장·복원·비우기가 전부 그 표만 보고
|
||||
* 움직이게 한다. 나중에 캐시↔즉시를 바꿀 때도 표 한 줄만 고치면 된다.
|
||||
*
|
||||
* 통은 넷이다.
|
||||
* ① pref 화면 취향 — 패널 열림·높이, 보기 토글. **계정**에 붙는다(다른 PC에서도 같은
|
||||
* 배치). 세션은 그 값의 사본이라 서버가 없어도 화면은 돈다.
|
||||
* ② draft 설계 초안 — 사용자가 만진 설계값. [저장]·[확정] 때만 서버로 간다.
|
||||
* ③ (즉시) 누르는 순간 다시 계산을 부르는 명령. 여기 쌓지 않는다 — 표에도 없다.
|
||||
* ④ result 계산 결과 — 서버·브라우저가 만들어 낸 값. 저장 대상이 아니고, 입력이
|
||||
* 바뀌면 버리고 다시 만든다. 페이지를 오갈 때 다시 부르지 않으려고 둔다.
|
||||
*
|
||||
* 키 형식은 `aislo:{통}:{이름}:{프로젝트}[:{노선}]` 하나다. **페이지 이름을 키에 넣지
|
||||
* 않는다** — ②·④는 두 화면이 같은 키를 쓴다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 값이 속한 통. `pref`·`draft`·`result` 셋만 저장소를 쓴다(즉시 반영은 표에 없다). */
|
||||
export type StateBucket = "pref" | "draft" | "result";
|
||||
|
||||
/** 값을 가르는 범위 — 키에 무엇을 덧붙일지 정한다. */
|
||||
export type StateScope = "global" | "project" | "route";
|
||||
|
||||
export interface StateEntry {
|
||||
bucket: StateBucket;
|
||||
scope: StateScope;
|
||||
/** 형식이 바뀌면 올린다 — 옛 값을 읽지 않고 기본값으로 시작한다. */
|
||||
version?: number;
|
||||
/** 옛 키(있으면 한 번 읽어 옮기고 지운다). `project`·`route` 범위는 함수로 받는다. */
|
||||
legacy?: (projectId?: string, routeId?: number | string) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록표 — **새 값은 여기 줄부터 넣고 코드를 쓴다.**
|
||||
*
|
||||
* `scope: "global"` 은 프로젝트를 가리지 않는 화면 취향이다(패널 높이 등). 설계값은
|
||||
* 반드시 `project` 또는 `route` 범위를 쓴다 — 프로젝트를 옮겼는데 앞 프로젝트의 조작이
|
||||
* 남으면 안 된다.
|
||||
*/
|
||||
export const STATE_REGISTRY = {
|
||||
/* ── ① 화면 취향 ─────────────────────────────────────────────────────── */
|
||||
"profile-collapsed": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b05-route-profile-collapsed",
|
||||
},
|
||||
"profile-height": { bucket: "pref", scope: "global", legacy: () => "b05-route-profile-height" },
|
||||
"drainage-collapsed": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b05-route-drainage-collapsed",
|
||||
},
|
||||
"drainage-width": { bucket: "pref", scope: "global", legacy: () => "b05-route-drainage-width" },
|
||||
"masshaul-open": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b05-route-profile-masshaul-open",
|
||||
},
|
||||
"masshaul-height": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b05-route-profile-masshaul-height",
|
||||
},
|
||||
/** 유토곡선 범례 — B05·B06 이 같은 값을 본다(예전에도 키를 공유했다). */
|
||||
"masshaul-visible": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
version: 5,
|
||||
legacy: () => "b06:masshaul-visible-v4",
|
||||
},
|
||||
"table-open": { bucket: "pref", scope: "global", legacy: () => "b05:profile:table:open" },
|
||||
"table-height": { bucket: "pref", scope: "global", legacy: () => "b05:profile:table:height" },
|
||||
"section-panel-collapsed": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b06:profile-panel-collapsed",
|
||||
},
|
||||
"section-panel-height": {
|
||||
bucket: "pref",
|
||||
scope: "global",
|
||||
legacy: () => "b06:profile-panel-height",
|
||||
},
|
||||
|
||||
/* ── ② 설계 초안 ─────────────────────────────────────────────────────── */
|
||||
/** 아직 정본에 안 넣은 구조물 목록. B05 에서 만들고 B06 [저장]이 내보낸다. */
|
||||
structures: { bucket: "draft", scope: "project", legacy: (p) => `b05:structures:${p}` },
|
||||
/** 3D 램프로 바꾼 측점 상단측(측구 방향). */
|
||||
uphill: { bucket: "draft", scope: "project", legacy: (p) => `b05:uphill:${p}` },
|
||||
/** 관 위치(되돌리기 스냅샷이 함께 본다). */
|
||||
pipes: { bucket: "draft", scope: "project", legacy: () => "b05:pipes" },
|
||||
/** B05 에서 고른 구조물을 B06 이 이어받는 자리 — 예전 `aislo:structure-pick:*`. */
|
||||
"structure-pick": {
|
||||
bucket: "draft",
|
||||
scope: "project",
|
||||
legacy: (p) => `aislo:structure-pick:${p}`,
|
||||
},
|
||||
/** 배수관·암거 조정창에서 예약한 옵션 값. */
|
||||
culvertopt: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertopt:${p}:${r}` },
|
||||
/** 배수관 이동(측점 옮김) 예약. */
|
||||
culvertmove: { bucket: "draft", scope: "route", legacy: (p, r) => `b06:culvertmove:${p}:${r}` },
|
||||
/** 암 경계선 오프셋(측점별). */
|
||||
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}`,
|
||||
},
|
||||
|
||||
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
|
||||
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
|
||||
latest: { bucket: "result", scope: "project", legacy: (p) => `b05:latest:${p}` },
|
||||
/** 횡단 상세 응답 — 예전에는 메모리에만 있어 페이지를 떠나면 사라졌다. */
|
||||
"section-detail": { bucket: "result", scope: "route" },
|
||||
} as const satisfies Record<string, StateEntry>;
|
||||
|
||||
export type StateName = keyof typeof STATE_REGISTRY;
|
||||
|
||||
function entryOf(name: StateName): StateEntry {
|
||||
return STATE_REGISTRY[name] as StateEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장소 키. 범위가 요구하는 값이 없으면 `null` — 부를 쪽은 그때 저장을 건너뛴다
|
||||
* (프로젝트를 아직 모를 때 전역 키에 적으면 다음 프로젝트가 그 값을 물려받는다).
|
||||
*/
|
||||
export function stateKey(
|
||||
name: StateName,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): string | null {
|
||||
const entry = entryOf(name);
|
||||
const suffix = entry.version && entry.version > 1 ? `-v${entry.version}` : "";
|
||||
const head = `aislo:${entry.bucket}:${name}${suffix}`;
|
||||
if (entry.scope === "global") return head;
|
||||
if (!projectId) return null;
|
||||
if (entry.scope === "project") return `${head}:${projectId}`;
|
||||
if (routeId === null || routeId === undefined) return null;
|
||||
return `${head}:${projectId}:${routeId}`;
|
||||
}
|
||||
|
||||
/** 세션 접근은 저장소가 막힌 브라우저(사생활 보호)에서 던진다 — 전부 조용히 넘긴다. */
|
||||
function readRaw(key: string): string | null {
|
||||
try {
|
||||
return window.sessionStorage.getItem(key);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeRaw(key: string, value: string | null): void {
|
||||
try {
|
||||
if (value === null) window.sessionStorage.removeItem(key);
|
||||
else window.sessionStorage.setItem(key, value);
|
||||
} catch {
|
||||
/* 무시 — 값은 화면 메모리에 남는다. */
|
||||
}
|
||||
}
|
||||
|
||||
/** 옛 키에 남은 값을 새 키로 한 번 옮긴다(옮기고 나면 옛 키는 지운다). */
|
||||
function migrate(
|
||||
name: StateName,
|
||||
key: string,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): void {
|
||||
const legacy = entryOf(name).legacy?.(projectId ?? undefined, routeId ?? undefined);
|
||||
if (!legacy || legacy === key) return;
|
||||
const old = readRaw(legacy);
|
||||
if (old !== null && readRaw(key) === null) writeRaw(key, old);
|
||||
if (old !== null) writeRaw(legacy, null);
|
||||
}
|
||||
|
||||
/** 문자열 그대로 읽는다(숫자·참거짓처럼 JSON 이 아닌 값). */
|
||||
export function readStateRaw(
|
||||
name: StateName,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): string | null {
|
||||
const key = stateKey(name, projectId, routeId);
|
||||
if (!key) return null;
|
||||
migrate(name, key, projectId, routeId);
|
||||
return readRaw(key);
|
||||
}
|
||||
|
||||
export function writeStateRaw(
|
||||
name: StateName,
|
||||
value: string | null,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): void {
|
||||
const key = stateKey(name, projectId, routeId);
|
||||
if (key) writeRaw(key, value);
|
||||
}
|
||||
|
||||
/** JSON 값을 읽는다. 없거나 손상됐으면 `null` — 부를 쪽이 기본값으로 시작한다. */
|
||||
export function readState<T>(
|
||||
name: StateName,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): T | null {
|
||||
const raw = readStateRaw(name, projectId, routeId);
|
||||
if (raw === null) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeState(
|
||||
name: StateName,
|
||||
value: unknown,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): void {
|
||||
writeStateRaw(name, value === null ? null : JSON.stringify(value), projectId, routeId);
|
||||
}
|
||||
|
||||
export function clearState(
|
||||
name: StateName,
|
||||
projectId?: string | null,
|
||||
routeId?: number | string | null,
|
||||
): void {
|
||||
writeStateRaw(name, null, projectId, routeId);
|
||||
}
|
||||
|
||||
/** 등록표에서 한 통에 속한 이름만 고른다. */
|
||||
export function namesInBucket(bucket: StateBucket): StateName[] {
|
||||
return (Object.keys(STATE_REGISTRY) as StateName[]).filter(
|
||||
(name) => entryOf(name).bucket === bucket,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 초안을 통째로 비운다 — **[저장]·[확정]·[초기화]·노선 변경 뒤 여기 한 곳**만 부른다
|
||||
* (예전에는 파일마다 따로 지웠다). 노선을 모르면 노선 범위 초안은 남는다.
|
||||
*/
|
||||
export function clearDrafts(projectId: string | null, routeId?: number | string | null): void {
|
||||
namesInBucket("draft").forEach((name) => clearState(name, projectId, routeId));
|
||||
}
|
||||
|
||||
/** 계산 결과를 통째로 버린다 — 입력(설계값·노선)이 바뀌어 다시 만들어야 할 때. */
|
||||
export function clearResults(projectId: string | null, routeId?: number | string | null): void {
|
||||
namesInBucket("result").forEach((name) => clearState(name, projectId, routeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 지금 이 브라우저에 쌓인 초안이 있는가 — [저장] 버튼의 미저장 표시에 쓴다.
|
||||
* 값이 빈 객체(`{}`)면 없는 것으로 본다.
|
||||
*/
|
||||
export function hasDrafts(projectId: string | null, routeId?: number | string | null): boolean {
|
||||
return namesInBucket("draft").some((name) => {
|
||||
const raw = readStateRaw(name, projectId, routeId);
|
||||
return raw !== null && raw !== "{}" && raw !== "[]" && raw !== "";
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import {
|
||||
computeDetailBasins,
|
||||
fetchDetailPipePoints,
|
||||
@@ -39,7 +40,6 @@ import {
|
||||
fitViewToRoute,
|
||||
observeViewportSize,
|
||||
bindPipeContextMenu,
|
||||
COLLAPSED_KEY,
|
||||
MAX_PANEL_WIDTH_RATIO,
|
||||
MIN_PANEL_WIDTH,
|
||||
renderBasinRows,
|
||||
@@ -526,7 +526,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
function setCollapsed(collapsed: boolean): void {
|
||||
root.classList.toggle("is-collapsed", collapsed);
|
||||
panelHandle.setOpen(!collapsed);
|
||||
sessionStorage.setItem(COLLAPSED_KEY, String(collapsed));
|
||||
writeStateRaw("drainage-collapsed", String(collapsed));
|
||||
if (!collapsed) scheduleDraw();
|
||||
}
|
||||
panelHandle.root.addEventListener("click", () =>
|
||||
@@ -534,7 +534,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
);
|
||||
// 상세 배수유역 정보는 페이지에 들어오면 바로 보여야 한다 — 저장값이 없으면 펼침이 기본이다
|
||||
// (같은 페이지의 하단 종단 패널과 같은 규칙).
|
||||
setCollapsed(sessionStorage.getItem(COLLAPSED_KEY) === "true");
|
||||
setCollapsed(readStateRaw("drainage-collapsed") === "true");
|
||||
|
||||
return {
|
||||
root,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 분리한 것으로, 여기 있는 것들은 패널의 내부 상태를 알지 못한다 — 전부 인자로 받는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { stateKey } from "../A00_Common/b_page_state";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { themeColor } from "@ui/ui_template_palette";
|
||||
import { DRAINAGE_SHEET_LAYERS, fetchCachedSheetLayer } from "../A00_Common/b_asset_cache";
|
||||
@@ -52,9 +53,10 @@ export const hotspotToggleColor = (): string => themeColor("--map-flow-ramp-4",
|
||||
/** 위성사진은 선이 아니라 배경이라 맞출 선 색이 없다 — 중립 회색을 띠 색으로 쓴다. */
|
||||
export const satelliteToggleColor = (): string => themeColor("--map-satellite-toggle", "#64748b");
|
||||
|
||||
export const COLLAPSED_KEY = "b05-route-drainage-collapsed";
|
||||
/** 접힘·폭은 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||
export const COLLAPSED_KEY = stateKey("drainage-collapsed") ?? "";
|
||||
/** 드래그로 조절한 패널 폭(px) 보관 키 — 브라우저 세션 동안만 유지한다. */
|
||||
export const WIDTH_KEY = "b05-route-drainage-width";
|
||||
export const WIDTH_KEY = stateKey("drainage-width") ?? "";
|
||||
/** 지도가 담기는 최소 폭(px). CSS의 min-width와 같은 값. */
|
||||
export const MIN_PANEL_WIDTH = 320;
|
||||
/** 상한은 하단 패널 폭의 70%까지(사용자 지시) — 종단면도가 최소한 30%는 남아야 한다. */
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* 스크롤러와 scrollLeft를 양방향 동기화해 측점 세로선이 어긋나지 않게 한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import type {
|
||||
EarthworkConversion,
|
||||
HaulEquipmentLimit,
|
||||
@@ -56,9 +57,9 @@ import "./B05_Profile_UI_Style_MassHaul.css";
|
||||
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
||||
|
||||
/** 2차 패널 펼침 여부 — 세션 동안만 유지(패널 높이·접힘과 같은 수명). */
|
||||
const OPEN_KEY = "b05-route-profile-masshaul-open";
|
||||
/* 펼침 상태·높이·범례는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||
/** 오버레이 높이(px) 세션 키 — 메인 하단 패널 높이와 같은 수명. */
|
||||
const OVERLAY_HEIGHT_KEY = "b05-route-profile-masshaul-height";
|
||||
const OVERLAY_HEIGHT_KEY = stateKey("masshaul-height") ?? "";
|
||||
/** 오버레이 높이 하한/기본값(px). 곡선 + 요약 막대가 읽히는 최소 크기다. */
|
||||
const OVERLAY_MIN_HEIGHT = 160;
|
||||
/** 유토곡선 오버레이 최소 높이 — 메인 패널 하한 계산(Profile_Panel)이 함께 쓴다. */
|
||||
@@ -73,7 +74,6 @@ const HEIGHT_VAR = "--b05-masshaul-height";
|
||||
* 표시 상태가 갈리면 "같은 곡선인데 왜 다르게 보이나"가 된다(2026-08-03 사용자 확정).
|
||||
* 정의처는 B06 `_UI_Section_View`와 이 파일 두 곳뿐이며 값이 반드시 같아야 한다.
|
||||
*/
|
||||
const VISIBLE_KEY = "b06:masshaul-visible-v5";
|
||||
/** 처음 열 때는 **곡선만** 보인다 — 토량 분배(평형선·운반 블록)는 범례에서 켠다
|
||||
* (2026-09-06 사용자 지시). 판을 v4 → v5 로 올려 이미 켜 둔 브라우저도 새 기본값으로
|
||||
* 시작하게 한다. */
|
||||
@@ -164,7 +164,7 @@ export interface RouteMassHaulPanel {
|
||||
|
||||
function readVisible(): Set<string> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(VISIBLE_KEY);
|
||||
const raw = readStateRaw("masshaul-visible");
|
||||
if (!raw) return new Set(DEFAULT_VISIBLE);
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
// 기준 키는 라디오라 항상 하나로 눌러 맞춘다(B06 읽기와 같은 규칙).
|
||||
@@ -224,7 +224,7 @@ export function createRouteMassHaulPanel(
|
||||
// 높이를 복원하며 생성 중에 onResize → syncHandlePosition을 부르는데, `open`이 그 아래
|
||||
// 있으면 TDZ ReferenceError로 B05 페이지 전체가 죽는다(2026-08-04 사용자 보고 —
|
||||
// 저장된 높이가 있는 브라우저에서만 재현되는 이유).
|
||||
let open = sessionStorage.getItem(OPEN_KEY) === "true";
|
||||
let open = readStateRaw("masshaul-open") === "true";
|
||||
let context: RouteMassHaulContext | null = null;
|
||||
// 높이 조절 — 메인 하단 패널과 같은 공용 리사이저(위 경계, 위로 끌면 커짐, 세션 보존).
|
||||
let resizeRedrawPending = false;
|
||||
@@ -277,7 +277,7 @@ export function createRouteMassHaulPanel(
|
||||
|
||||
function applyOpen(next: boolean): void {
|
||||
open = next;
|
||||
sessionStorage.setItem(OPEN_KEY, String(next));
|
||||
writeStateRaw("masshaul-open", String(next));
|
||||
handleControl.setOpen(next);
|
||||
// 툴팁은 setOpen이 일반 문구로 덮으므로 매번 유토곡선용으로 다시 밝힌다.
|
||||
handleControl.root.title = next ? "유토곡선 접기" : "유토곡선 펼치기";
|
||||
@@ -295,7 +295,7 @@ export function createRouteMassHaulPanel(
|
||||
function toggleSeries(key: string): void {
|
||||
// 곡선 기준(횡단/종단)은 라디오 — 하나를 고르면 그 기준 그래프만 전체 영역에 보인다.
|
||||
const visible = applyLegendToggle(readVisible(), key);
|
||||
sessionStorage.setItem(VISIBLE_KEY, JSON.stringify([...visible]));
|
||||
writeStateRaw("masshaul-visible", JSON.stringify([...visible]));
|
||||
onChanged();
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ export function createRouteMassHaulPanel(
|
||||
commitHeight(px) {
|
||||
const next = Math.round(px);
|
||||
overlay.style.setProperty(HEIGHT_VAR, `${next}px`);
|
||||
sessionStorage.setItem(OVERLAY_HEIGHT_KEY, String(next));
|
||||
writeStateRaw("masshaul-height", String(next));
|
||||
syncHandlePosition();
|
||||
},
|
||||
syncHandle: () => syncHandlePosition(),
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* `saveProfileAlignment()`로 편집 델타만 보낸다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
@@ -67,9 +68,8 @@ import "../B06_Section/B06_Section_UI_Style.css";
|
||||
import "../B06_Section/B06_Section_UI_Style_Cross.css";
|
||||
import "../B06_Section/B06_Section_UI_Style_Cross_Areas.css";
|
||||
|
||||
const COLLAPSED_KEY = "b05-route-profile-collapsed";
|
||||
/** 드래그로 조절한 하단 패널 높이(px) 보관 키 — 브라우저 세션 동안만 유지한다. */
|
||||
const HEIGHT_KEY = "b05-route-profile-height";
|
||||
/** 접힘·높이는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||
const HEIGHT_KEY = stateKey("profile-height") ?? "";
|
||||
/* 테이블 높이는 오버레이 서브패널(--b05-table-height 리사이저)이 관리한다 —
|
||||
예전 4:6 고정 분할(TABLE_HEIGHT_KEY·CHART_HEIGHT_RATIO)은 폐지(2026-08-05). */
|
||||
const MIN_CHART_HEIGHT = 100;
|
||||
@@ -599,14 +599,14 @@ export function createRouteProfilePanel(
|
||||
function setCollapsed(collapsed: boolean): void {
|
||||
root.classList.toggle("is-collapsed", collapsed);
|
||||
panelHandle.setOpen(!collapsed);
|
||||
sessionStorage.setItem(COLLAPSED_KEY, String(collapsed));
|
||||
writeStateRaw("profile-collapsed", String(collapsed));
|
||||
if (!collapsed) requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
panelHandle.root.addEventListener("click", () =>
|
||||
setCollapsed(!root.classList.contains("is-collapsed")),
|
||||
);
|
||||
setCollapsed(sessionStorage.getItem(COLLAPSED_KEY) === "true");
|
||||
setCollapsed(readStateRaw("profile-collapsed") === "true");
|
||||
|
||||
return {
|
||||
root,
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
* 가로 오프셋). 테이블 내용물은 Profile_Panel의 draw()가 만들어 setTable로 넣는다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
|
||||
|
||||
const OPEN_KEY = "b05:profile:table:open";
|
||||
const HEIGHT_KEY = "b05:profile:table:height";
|
||||
/* 펼침·높이는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
|
||||
const HEIGHT_KEY = stateKey("table-height") ?? "";
|
||||
const OVERLAY_MIN_HEIGHT = 140;
|
||||
/** 테이블 오버레이 최소 높이 — 메인 패널 하한 계산(Profile_Panel)이 함께 쓴다. */
|
||||
export const TABLE_OVERLAY_MIN_HEIGHT = OVERLAY_MIN_HEIGHT;
|
||||
@@ -61,7 +62,7 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
|
||||
|
||||
// 상태 선언은 리사이저 생성보다 먼저 — 세션 높이 복원이 생성 중 onResize를 부른다
|
||||
// (유토곡선 TDZ 크래시와 같은 함정, 2026-08-04 확인).
|
||||
let open = sessionStorage.getItem(OPEN_KEY) !== "false"; // 기본 펼침(기존 테이블 상시 표시 유지)
|
||||
let open = readStateRaw("table-open") !== "false"; // 기본 펼침(기존 테이블 상시 표시 유지)
|
||||
let bottomOffset = 0;
|
||||
let resizeRedrawPending = false;
|
||||
const resizer = createPanelResizer({
|
||||
@@ -99,7 +100,7 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
|
||||
|
||||
function applyOpen(next: boolean): void {
|
||||
open = next;
|
||||
sessionStorage.setItem(OPEN_KEY, String(next));
|
||||
writeStateRaw("table-open", String(next));
|
||||
handleControl.setOpen(next);
|
||||
handleControl.root.title = next ? "테이블 접기" : "테이블 펼치기";
|
||||
handle.classList.toggle("is-open", next);
|
||||
@@ -136,7 +137,7 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
|
||||
commitHeight: (px) => {
|
||||
const next = Math.round(px);
|
||||
overlay.style.setProperty(HEIGHT_VAR, `${next}px`);
|
||||
sessionStorage.setItem(HEIGHT_KEY, String(next));
|
||||
writeStateRaw("table-height", String(next));
|
||||
syncHandlePosition();
|
||||
},
|
||||
contentHeight: () => Math.max(0, overlay.offsetHeight - 6),
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* 외부(Page)에는 `createSectionView`와 `CrossDesignChange` 타입만 노출한다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
// 가로 스크롤 고정 Y축 — B05와 같은 오버레이를 쓴다(정의처: B05 MassHaul 모듈 + 그 CSS).
|
||||
@@ -59,12 +60,10 @@ import {
|
||||
import {
|
||||
BASE_PANEL_HEIGHT,
|
||||
chartHeights,
|
||||
MASS_HAUL_VISIBLE_KEY,
|
||||
MAX_PANEL_HEIGHT_RATIO,
|
||||
MIN_LONG_HEIGHT,
|
||||
MIN_PANEL_HEIGHT,
|
||||
PANEL_CHROME_PX,
|
||||
PANEL_COLLAPSED_KEY,
|
||||
PANEL_HEIGHT_KEY,
|
||||
readVisibleSeries,
|
||||
} from "./B06_Section_UI_Section_View_Panel";
|
||||
@@ -186,11 +185,11 @@ export function createSectionView(
|
||||
panelToggle.root.classList.add("b06-section__panel-toggle");
|
||||
panel.append(panelHeader, panelBody, panelToggle.root);
|
||||
// 접힘 상태는 세션에만 남긴다(리사이저와 같은 규칙).
|
||||
if (sessionStorage.getItem(PANEL_COLLAPSED_KEY) === "true") panel.classList.add("is-collapsed");
|
||||
if (readStateRaw("section-panel-collapsed") === "true") panel.classList.add("is-collapsed");
|
||||
panelToggle.setOpen(!panel.classList.contains("is-collapsed"));
|
||||
panelToggle.root.addEventListener("click", () => {
|
||||
const collapsed = panel.classList.toggle("is-collapsed");
|
||||
sessionStorage.setItem(PANEL_COLLAPSED_KEY, String(collapsed));
|
||||
writeStateRaw("section-panel-collapsed", String(collapsed));
|
||||
panelToggle.setOpen(!collapsed);
|
||||
// 펼칠 때는 레이아웃이 잡힌 **다음 프레임**에 그린다 — 그래프 몫을 실측으로 잡기 때문에
|
||||
// 같은 프레임에 그리면 접혀 있던 0 높이를 읽는다(B05 하단 패널과 같은 처리).
|
||||
@@ -458,7 +457,7 @@ export function createSectionView(
|
||||
const next = applyLegendToggle(visibleSeries, key);
|
||||
visibleSeries.clear();
|
||||
next.forEach((entry) => visibleSeries.add(entry));
|
||||
sessionStorage.setItem(MASS_HAUL_VISIBLE_KEY, JSON.stringify([...visibleSeries]));
|
||||
writeStateRaw("masshaul-visible", JSON.stringify([...visibleSeries]));
|
||||
drawPanel();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MASS_HAUL_DEFAULT_VISIBLE, normalizeVisibleBasis } from "@util/common_util_mass_haul";
|
||||
import { readStateRaw, stateKey } from "../A00_Common/b_page_state";
|
||||
import { MASS_HAUL_HEIGHT, MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
||||
import { LONG_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
|
||||
@@ -7,9 +8,9 @@ export const BASE_PANEL_HEIGHT = LONG_HEIGHT + MASS_HAUL_HEIGHT + PANEL_CHROME_P
|
||||
export const MIN_LONG_HEIGHT = 110;
|
||||
export const MIN_PANEL_HEIGHT = MIN_LONG_HEIGHT + MASS_HAUL_MIN_HEIGHT + PANEL_CHROME_PX;
|
||||
export const MAX_PANEL_HEIGHT_RATIO = 0.8;
|
||||
export const PANEL_HEIGHT_KEY = "b06:profile-panel-height";
|
||||
export const PANEL_COLLAPSED_KEY = "b06:profile-panel-collapsed";
|
||||
export const MASS_HAUL_VISIBLE_KEY = "b06:masshaul-visible-v5";
|
||||
/* 패널 접힘·높이·유토곡선 범례는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다.
|
||||
범례 키는 B05 와 **같은 값**이어야 한다(두 화면이 같은 그림의 두 창). */
|
||||
export const PANEL_HEIGHT_KEY = stateKey("section-panel-height") ?? "";
|
||||
|
||||
/** 처음 열 때는 **곡선만** — 토량 분배는 범례에서 켠다(2026-09-06 사용자 지시).
|
||||
* B05 `_UI_Profile_MassHaul` 과 같은 값이어야 한다(두 화면 공용 키). */
|
||||
@@ -17,7 +18,7 @@ const DEFAULT_VISIBLE_KEYS = [...MASS_HAUL_DEFAULT_VISIBLE];
|
||||
|
||||
export function readVisibleSeries(): Set<string> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(MASS_HAUL_VISIBLE_KEY);
|
||||
const raw = readStateRaw("masshaul-visible");
|
||||
if (!raw) return new Set(DEFAULT_VISIBLE_KEYS);
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return Array.isArray(parsed)
|
||||
|
||||
Reference in New Issue
Block a user