refactor(B05/B06): 유토곡선 일원화 — 보는 노선·횡단 재계산 창구·낡음 판정 통합
같은 데이터를 두 화면에 보여 주는 기능인데 세 곳이 갈려 값이 달랐다(실측: 절토(자연) B06 3,704.6㎥ ↔ B05 4,526.8㎥). ① 보는 노선 — B05는 최신 경로, B06은 최신 **확정** 경로를 열어 노선을 다시 탐색한 프로젝트에서 서로 다른 노선을 봤다(route 126 DRAFT ↔ 125 CONFIRMED, 같은 20m 측점 성토 4.82㎡ ↔ 85.9㎡). `get_workflow_route_context()`(최신 경로) 신설해 B06 화면 context가 그것을 쓴다. 납품 도면(B07)이 쓰는 확정 전용 창구는 그대로 둔다. ② 횡단 재계산 — 호출이 두 벌이라 인자가 갈렸다(B06만 표준 단면값·암 경계 오프셋 전달, 보존하는 사용자 부속값도 2개 ↔ 7개). `B06_Section_Cross_Refresh.refreshCrossDesigns()` 한 창구로 모으고 세션 편집값은 저장소에서 직접 읽어 패널 없는 B05도 같은 값을 보낸다. ③ 낡음 판정 — 「옛 암 2단계 필드 누락」 조건이 B06 페이지에만 있어 B05는 재계산을 건너뛰었다. 공용 `staleDesignChainages()` 안으로 옮겨 두 화면이 같은 시점에 같은 조치를 한다. 검증 — 공용 브라우저 실측: 두 화면 모두 route 126, 요약줄 문자열 완전 일치 (`절토(자연) 4,526.8㎥ · 성토 14,881.8㎥ · 토취 10,213.3㎥ · 최종 누가토량 −10,213.3㎥`). pytest 370 passed·17 skipped(일원화 검사 4건 신설), typecheck·prettier·ruff 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import type { AlignmentEdits } from "./B05_Profile_UI_Profile_Alignment";
|
||||
import { previewCrossDesigns } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { refreshCrossDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
|
||||
export interface CrossPreviewContext {
|
||||
@@ -43,29 +43,20 @@ export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
|
||||
const routeId = ctx.routeId();
|
||||
if (!detail || routeId === null) return;
|
||||
const current = (seq += 1);
|
||||
// full_designs — 설계선 좌표까지 통째로 받아야 3D 코리도가 편집 즉시 정확한
|
||||
// 형상으로 재빌드된다(2026-08-23 사용자 지시). 암 경계는 백엔드가 세션값이 없으면
|
||||
// DB 저장 echo를 폴백으로 쓰므로 그대로 유지된다.
|
||||
void previewCrossDesigns(ctx.projectId, routeId, ctx.edits(), undefined, {
|
||||
fullDesigns: true,
|
||||
// 재계산은 B06과 **같은 창구**를 쓴다 — 표준 단면값·암 경계 오프셋이 빠지면 서버가
|
||||
// 다른 설계를 그려 같은 데이터가 두 화면에서 다른 값이 된다(2026-09-03 일원화).
|
||||
const isCurrent = (): boolean =>
|
||||
current === seq && ctx.detail() === detail && ctx.routeId() === routeId;
|
||||
void refreshCrossDesigns({
|
||||
projectId: ctx.projectId,
|
||||
routeId,
|
||||
detail,
|
||||
edits: ctx.edits(),
|
||||
// 늦게 온 옛 응답이 새 설계를 덮지 않게 반영 직전에 한 번 더 확인한다.
|
||||
shouldApply: isCurrent,
|
||||
})
|
||||
.then((next) => {
|
||||
const live = ctx.detail();
|
||||
if (current !== seq || !live || ctx.routeId() !== routeId) return;
|
||||
const designByChainage = new Map(
|
||||
next.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
|
||||
);
|
||||
for (const section of live.cross_sections) {
|
||||
const full = designByChainage.get(section.chainage_m.toFixed(3));
|
||||
if (!full || !section.design) continue;
|
||||
// 전체 교체(설계선 포함) — B06 reconcile과 같은 패턴으로 사용자 부속값은 보존.
|
||||
section.design = {
|
||||
...(full as NonNullable<typeof section.design>),
|
||||
inlet_structure: section.design.inlet_structure,
|
||||
basin_adjust: section.design.basin_adjust,
|
||||
};
|
||||
}
|
||||
ctx.onApplied();
|
||||
.then((updated) => {
|
||||
if (updated.length) ctx.onApplied();
|
||||
})
|
||||
.catch(() => {
|
||||
/* 프리뷰 실패는 무시 — 화면의 계획선은 그대로 두고 다음 편집에서 다시 시도한다. */
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/* =============================================================================
|
||||
* B06_Section_Cross_Refresh.ts
|
||||
* 「현재 계획선에 맞춘 횡단 재계산」 **단일 창구** — B05·B06이 같은 입력으로 같은 결과를 본다.
|
||||
*
|
||||
* ── 왜 필요한가 (2026-09-03 사용자 지시) ─────────────────────────────
|
||||
* 유토곡선은 두 화면이 같은 데이터를 보여 주는 기능인데, 재계산 호출이 두 벌이라 값이
|
||||
* 갈렸다. 실측 — 같은 프로젝트·같은 시점에 B06 `절토(자연) 3,704.6㎥` ↔ B05 `4,526.8㎥`.
|
||||
* 원인은 인자였다:
|
||||
* · B06: `previewCrossDesigns(..., standardPanel.getValues(), { fullDesigns, rockBoundaryOffsets })`
|
||||
* · B05: `previewCrossDesigns(..., undefined, { fullDesigns })`
|
||||
* 표준 단면값과 암 경계 오프셋이 빠지면 서버가 다른 설계를 그려 단면적이 달라진다.
|
||||
* 보존하는 사용자 부속값 목록도 서로 달라(B05는 2개, B06은 7개) B05를 거치면 기슭막이
|
||||
* 조정 같은 값이 사라졌다.
|
||||
*
|
||||
* 그래서 **입력 수집·서버 호출·제자리 반영**을 여기 한 곳으로 모은다. 세션 편집값은
|
||||
* 패널이 아니라 세션 저장소에서 직접 읽으므로, 패널이 없는 B05도 B06과 같은 값을 보낸다.
|
||||
* ========================================================================== */
|
||||
|
||||
import { previewCrossDesigns } from "./B06_Section_Api_Fetch";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import { readRockBoundarySession } from "./B06_Section_UI_Page_Persist";
|
||||
import { readStandardCrossSession } from "./B06_Section_UI_Standard_Panel";
|
||||
|
||||
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
|
||||
export interface CrossRefreshEdits {
|
||||
station_offsets: Record<string, number>;
|
||||
curve_radii: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface CrossRefreshInput {
|
||||
projectId: string;
|
||||
routeId: number;
|
||||
/** 제자리 갱신 대상 — 공유 캐시가 들고 있는 그 객체여야 두 화면이 같이 따라온다. */
|
||||
detail: SectionDetailResponse;
|
||||
edits: CrossRefreshEdits;
|
||||
/**
|
||||
* 응답이 도착한 시점에 **아직 이 결과를 써도 되는지** 묻는다(false면 반영하지 않는다).
|
||||
* 끌기 중에는 요청이 겹치므로, 늦게 온 옛 응답이 새 설계를 덮지 않게 하는 문지기다.
|
||||
*/
|
||||
shouldApply?: () => boolean;
|
||||
}
|
||||
|
||||
/** 서버 설계로 갈아 끼워도 **살려 두는 사용자 부속값** — 화면 조작으로만 생기는 값이다. */
|
||||
function preserveUserFields(
|
||||
next: NonNullable<CrossSection["design"]>,
|
||||
previous: CrossSection["design"],
|
||||
): NonNullable<CrossSection["design"]> {
|
||||
if (!previous) return next;
|
||||
return {
|
||||
...next,
|
||||
inlet_structure: previous.inlet_structure,
|
||||
basin_adjust: previous.basin_adjust,
|
||||
revet_adjust: previous.revet_adjust,
|
||||
extra_wall_counts: previous.extra_wall_counts,
|
||||
extra_spans: previous.extra_spans,
|
||||
revet_link_detached: previous.revet_link_detached,
|
||||
revet_follow_grade: previous.revet_follow_grade,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 전 측점 횡단을 현재 계획선으로 다시 계산해 `detail.cross_sections[].design`을 제자리 교체한다.
|
||||
* 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
|
||||
*/
|
||||
export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<number[]> {
|
||||
const { projectId, routeId, detail, edits, shouldApply } = input;
|
||||
// full_designs — 설계선 좌표까지 받아야 3D 코리도·횡단도가 편집 즉시 같은 형상이 된다.
|
||||
const response = await previewCrossDesigns(
|
||||
projectId,
|
||||
routeId,
|
||||
edits,
|
||||
readStandardCrossSession(projectId) ?? undefined,
|
||||
{
|
||||
fullDesigns: true,
|
||||
rockBoundaryOffsets: readRockBoundarySession(projectId, routeId),
|
||||
},
|
||||
);
|
||||
if (shouldApply && !shouldApply()) return [];
|
||||
const designByChainage = new Map(
|
||||
response.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
|
||||
);
|
||||
const updated: number[] = [];
|
||||
for (const section of detail.cross_sections) {
|
||||
const next = designByChainage.get(section.chainage_m.toFixed(3));
|
||||
if (!next) continue;
|
||||
section.design = preserveUserFields(
|
||||
next as NonNullable<CrossSection["design"]>,
|
||||
section.design,
|
||||
);
|
||||
updated.push(section.chainage_m);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
@@ -25,17 +25,18 @@ def _validate_stage_path(relative_path: str) -> str:
|
||||
return normalized.as_posix()
|
||||
|
||||
|
||||
async def get_confirmed_route_context(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
async def _route_context(
|
||||
connection: aiomysql.Connection, project_id: UUID, confirmed_only: bool
|
||||
) -> dict[str, Any] | None:
|
||||
"""프로젝트의 최신 확정 경로와 연결된 지표면 좌표계를 조회한다.
|
||||
"""경로 하나와 연결된 지표면 좌표계를 조회한다(정렬은 최신 우선).
|
||||
|
||||
surface_models.crs_epsg가 NULL이면(분석에 사용한 입력 파일에 좌표계가
|
||||
없던 경우) 같은 프로젝트 input_files의 감지된 좌표계로 폴백한다.
|
||||
"""
|
||||
status_filter = "AND r.status = 'CONFIRMED'" if confirmed_only else ""
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT r.id AS route_id,
|
||||
COALESCE(
|
||||
sm.crs_epsg,
|
||||
@@ -47,7 +48,7 @@ async def get_confirmed_route_context(
|
||||
) AS crs_epsg
|
||||
FROM routes r
|
||||
LEFT JOIN surface_models sm ON sm.id = r.surface_model_id
|
||||
WHERE r.project_id = %s AND r.status = 'CONFIRMED'
|
||||
WHERE r.project_id = %s {status_filter}
|
||||
ORDER BY r.computed_at DESC, r.id DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
@@ -62,6 +63,26 @@ async def get_confirmed_route_context(
|
||||
}
|
||||
|
||||
|
||||
async def get_confirmed_route_context(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> dict[str, Any] | None:
|
||||
"""최신 **확정** 경로 — 납품 도면(B07)처럼 확정본만 봐야 하는 곳이 쓴다."""
|
||||
return await _route_context(connection, project_id, confirmed_only=True)
|
||||
|
||||
|
||||
async def get_workflow_route_context(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> dict[str, Any] | None:
|
||||
"""워크플로 화면이 보는 경로 = **최신 경로**(확정 여부 무관).
|
||||
|
||||
B05는 `get_latest_route()`로 최신 경로를 열고, B06은 확정 경로만 열어서 노선을 다시
|
||||
탐색한 프로젝트에서 두 화면이 **다른 노선**을 봤다(2026-09-03 실측: B05 route 126
|
||||
DRAFT / B06 route 125 CONFIRMED — 같은 20m 측점의 성토가 4.82㎡ ↔ 85.9㎡). 같은
|
||||
데이터를 두 창으로 보여 주는 구조이므로 경로 선택 규칙을 최신 경로 하나로 맞춘다.
|
||||
"""
|
||||
return await _route_context(connection, project_id, confirmed_only=False)
|
||||
|
||||
|
||||
async def get_latest_section_options(
|
||||
connection: aiomysql.Connection, project_id: UUID
|
||||
) -> dict[str, Any] | None:
|
||||
|
||||
@@ -26,7 +26,7 @@ from B06_Section.B06_Section_Repository import (
|
||||
count_cross_sections,
|
||||
create_longitudinal_section,
|
||||
delete_sections_for_route,
|
||||
get_confirmed_route_context,
|
||||
get_workflow_route_context,
|
||||
get_cross_section_designs,
|
||||
get_latest_section_options,
|
||||
get_longitudinal_section,
|
||||
@@ -104,7 +104,9 @@ async def get_section_context(project_id: UUID) -> SectionContextResponse | JSON
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
async with pool.acquire() as connection:
|
||||
route_context = await get_confirmed_route_context(connection, project_id)
|
||||
# 화면이 보는 경로 = 최신 경로(확정 여부 무관) — B05와 같은 규칙이어야
|
||||
# 두 화면이 같은 노선의 같은 값을 본다(2026-09-03 일원화).
|
||||
route_context = await get_workflow_route_context(connection, project_id)
|
||||
surface_params = await get_surface_confirmation_params(connection, str(project_id))
|
||||
# 임도 종류(projects.road_type) — B05가 계획선 법정 기준을 정하는 데 쓴다.
|
||||
async with connection.cursor() as cursor:
|
||||
|
||||
@@ -15,15 +15,16 @@ import {
|
||||
computeCrossDesign,
|
||||
fetchSectionContext,
|
||||
getSections,
|
||||
previewCrossDesigns,
|
||||
type SectionContextResponse,
|
||||
type SectionDetailResponse,
|
||||
type StandardCrossSection,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import { createStationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
import { refreshCrossDesigns } from "./B06_Section_Cross_Refresh";
|
||||
import {
|
||||
confirmCurrentSections,
|
||||
createRockBoundaryStore,
|
||||
rockBoundarySessionKey,
|
||||
saveCurrentSections,
|
||||
type SectionPersistContext,
|
||||
} from "./B06_Section_UI_Page_Persist";
|
||||
@@ -284,16 +285,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
*/
|
||||
async function reconcileStaleDesigns(): Promise<void> {
|
||||
if (!sectionDetail || !projectId || currentRouteId === null) return;
|
||||
// 계획고 어긋남은 B05와 **같은 규칙**으로 판정한다(공용 staleDesignChainages).
|
||||
// 옛 암 2단계 필드 누락은 B06 전용 조건이라 여기서 더한다.
|
||||
const staleByPlan = new Set(staleDesignChainages(sectionDetail));
|
||||
const stale = sectionDetail.cross_sections.filter((section) => {
|
||||
const design = section.design;
|
||||
if (!design) return false;
|
||||
if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true;
|
||||
return staleByPlan.has(section.chainage_m);
|
||||
});
|
||||
if (!stale.length) return;
|
||||
// 낡음 판정은 B05와 **같은 규칙** 하나뿐이다(공용 staleDesignChainages — 계획고 어긋남 +
|
||||
// 옛 암 2단계 필드 누락). 조건이 갈리면 같은 데이터가 두 화면에서 다른 값이 된다.
|
||||
if (!staleDesignChainages(sectionDetail).length) return;
|
||||
// 진입 정합은 화면을 잠그지 않는다 — 카드가 도착하는 대로 조용히 갱신된다
|
||||
// (CLAUDE.md 5장).
|
||||
try {
|
||||
@@ -309,33 +303,15 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
station_offsets: alignment?.edits?.station_offsets ?? {},
|
||||
curve_radii: alignment?.edits?.curve_radii ?? {},
|
||||
};
|
||||
const response = await previewCrossDesigns(
|
||||
// 재계산은 B05와 **같은 창구**를 쓴다 — 인자가 갈리면 같은 데이터가 두 화면에서
|
||||
// 다른 값이 된다(2026-09-03 사용자 지시로 일원화).
|
||||
const updated = await refreshCrossDesigns({
|
||||
projectId,
|
||||
currentRouteId,
|
||||
routeId: currentRouteId,
|
||||
detail: sectionDetail,
|
||||
edits,
|
||||
standardPanel?.getValues(),
|
||||
{ fullDesigns: true, rockBoundaryOffsets: Object.fromEntries(rockOffsets) },
|
||||
);
|
||||
const designByChainage = new Map(
|
||||
response.designs.map((entry) => [entry.chainage_m.toFixed(3), entry.design]),
|
||||
);
|
||||
for (const section of sectionDetail.cross_sections) {
|
||||
const next = designByChainage.get(section.chainage_m.toFixed(3));
|
||||
if (next) {
|
||||
// full_designs 응답은 설계 전체(설계선 좌표 포함)라 통째로 교체한다.
|
||||
section.design = {
|
||||
...(next as NonNullable<typeof section.design>),
|
||||
inlet_structure: section.design?.inlet_structure,
|
||||
basin_adjust: section.design?.basin_adjust,
|
||||
revet_adjust: section.design?.revet_adjust,
|
||||
extra_wall_counts: section.design?.extra_wall_counts,
|
||||
extra_spans: section.design?.extra_spans,
|
||||
revet_link_detached: section.design?.revet_link_detached,
|
||||
revet_follow_grade: section.design?.revet_follow_grade,
|
||||
};
|
||||
sectionView.refreshCard(section.chainage_m);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const chainageM of updated) sectionView.refreshCard(chainageM);
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||
@@ -399,7 +375,9 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
// 암 경계선 오프셋(측점별) 세션 저장소는 저장 흐름 모듈이 맡는다(2026-09-02 분리).
|
||||
const rockStore = createRockBoundaryStore({
|
||||
sessionKey: () =>
|
||||
projectId && currentRouteId !== null ? `b06:rockb:${projectId}:${currentRouteId}` : null,
|
||||
projectId && currentRouteId !== null
|
||||
? rockBoundarySessionKey(projectId, currentRouteId)
|
||||
: null,
|
||||
detail: () => sectionDetail,
|
||||
refreshCard: (chainageM) => sectionView.refreshCard(chainageM),
|
||||
recompute: (chainageM) => recomputeIfRock(chainageM),
|
||||
|
||||
@@ -25,6 +25,28 @@ import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
|
||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||
import { L } from "./B06_Section_UI_Page_Common";
|
||||
|
||||
/**
|
||||
* 암 경계선 오프셋 세션 키 — 저장소와 재계산 창구가 **같은 자리**를 보게 정의처를 하나로 둔다.
|
||||
* (`B06_Section_Cross_Refresh` 가 패널 없는 B05에서도 같은 값을 읽어 서버로 보낸다.)
|
||||
*/
|
||||
export function rockBoundarySessionKey(projectId: string, routeId: number): string {
|
||||
return `b06:rockb:${projectId}:${routeId}`;
|
||||
}
|
||||
|
||||
/** 세션에 쌓인 암 경계선 오프셋(측점키 → m). 없거나 손상되면 빈 객체. */
|
||||
export function readRockBoundarySession(
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
): Record<string, number> {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(rockBoundarySessionKey(projectId, routeId));
|
||||
const parsed = raw ? (JSON.parse(raw) as unknown) : null;
|
||||
return parsed && typeof parsed === "object" ? (parsed as Record<string, number>) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/** 암 경계선 오프셋 저장소 — 값(Map)과 조정창 제어기를 함께 낸다. */
|
||||
export interface RockBoundaryStore {
|
||||
/** 측점키(누가거리 2자리) → 오프셋(m). `buildCrossPatches` 가 그대로 읽는다. */
|
||||
|
||||
@@ -81,16 +81,27 @@ export function validElevation(
|
||||
export function staleDesignChainages(
|
||||
detail: {
|
||||
longitudinal: { design_profiles?: DesignProfile[] };
|
||||
cross_sections: Array<{ chainage_m: number; design?: { design_elevation_m: number } | null }>;
|
||||
cross_sections: Array<{
|
||||
chainage_m: number;
|
||||
design?: {
|
||||
design_elevation_m: number;
|
||||
geometry_preset?: string;
|
||||
two_stage_slope?: boolean;
|
||||
} | null;
|
||||
}>;
|
||||
},
|
||||
toleranceM = 1e-3,
|
||||
): number[] {
|
||||
const profiles = detail.longitudinal.design_profiles;
|
||||
if (!profiles?.length) return [];
|
||||
return detail.cross_sections
|
||||
.filter((section) => {
|
||||
const design = section.design;
|
||||
if (!design) return false;
|
||||
// 옛 암 측점: 2단계 절토 경사 필드가 없으면 절토 면적이 최신 엔진과 다르다.
|
||||
// 이 조건이 B06 페이지에만 있어 B05는 재계산을 건너뛰었고, 같은 데이터인데 두 화면의
|
||||
// 절토량이 갈렸다(2026-09-03 실측 228.52㎡ ↔ 270.12㎡). 판정을 여기 한 곳으로 모은다.
|
||||
if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true;
|
||||
if (!profiles?.length) return false;
|
||||
const planned = designElevationAt(profiles, section.chainage_m);
|
||||
return planned !== undefined && Math.abs(planned - design.design_elevation_m) > toleranceM;
|
||||
})
|
||||
|
||||
@@ -52,8 +52,13 @@ function cloneDefaults(defaults: StandardCrossSection): StandardCrossSection {
|
||||
return JSON.parse(JSON.stringify(defaults)) as StandardCrossSection;
|
||||
}
|
||||
|
||||
/** 세션에 저장된 편집값을 읽는다. 없거나 손상 시 null. */
|
||||
function readSession(projectId: string): StandardCrossSection | null {
|
||||
/**
|
||||
* 세션에 저장된 편집값을 읽는다. 없거나 손상 시 null.
|
||||
*
|
||||
* 패널 밖에서도 필요하다 — 횡단 재계산 단일 창구(`B06_Section_Cross_Refresh`)가 B05처럼
|
||||
* 패널이 없는 화면에서도 **같은 표준 단면값**을 서버로 보내야 두 화면 결과가 같다.
|
||||
*/
|
||||
export function readStandardCrossSession(projectId: string): StandardCrossSection | null {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(sessionKey(projectId));
|
||||
return raw ? (JSON.parse(raw) as StandardCrossSection) : null;
|
||||
@@ -170,7 +175,7 @@ export function createStandardPanel(
|
||||
onApplyAll?: () => void | Promise<void>,
|
||||
): StandardPanelController {
|
||||
// 세션값 우선, 없으면 config 기본값. defaults는 복원 기준으로 보존한다.
|
||||
const sessionValue = readSession(projectId);
|
||||
const sessionValue = readStandardCrossSession(projectId);
|
||||
const hadSession = sessionValue !== null;
|
||||
const state: StandardCrossSection = sessionValue ?? cloneDefaults(defaults);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user