From c77e44ab0709e26f308108df95d5ead964a2e382 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 19:08:37 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B05=C2=B7B06=20=ED=95=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80):=20=EB=85=B8=EC=84=A0=20id=20=ED=95=9C=20?= =?UTF-8?q?=EC=B9=B8=20=E2=80=94=20=EC=B6=9C=EC=B2=98=20=EB=8B=A4=EC=84=AF?= =?UTF-8?q?=EC=9D=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=90=EB=A3=8C=20?= =?UTF-8?q?=EC=A4=84=20=ED=95=98=EB=82=98=EB=A1=9C=20(386-2b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - design_page_route 새로(칸 · 끄는 모듈 없음) · 채움 = design_page_data pageSectionContext(횡단 context · 세션 캐시) 하나 - 읽는 곳 = 종단 보기 [저장] 몫 · 횡단 보기 · 종단도 render · 평면 횡단 판(노선 번호 훑기 지움) · 3D · 측점 그림 - 페이지 떠남에 칸 비움 · 저장 정본 바뀜엔 그대로 - design_profile 700줄 앞 쪼갬(design_profile_context — 만들 때 넘기는 것 · 미리보기 대기) - 시험 test_384_2b · 평면 횡단 판 시험 = 페이지 노선 값만 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- A00_Common/design/design_page.ts | 2 +- A00_Common/design/design_page_data.ts | 19 +++- A00_Common/design/design_page_route.ts | 17 +++ A00_Common/design/design_profile.ts | 34 ++---- A00_Common/design/design_profile_context.ts | 32 ++++++ A00_Common/design/design_view_cross.ts | 13 ++- A00_Common/design/design_view_profile.ts | 9 +- A00_Common/design/design_view_profile_3d.ts | 6 +- .../design/design_view_profile_sections.ts | 3 +- B05_Profile/B05_Profile_UI_Page_Actions.ts | 3 +- B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 15 +-- .../tester/helper_273_2_preview_stations.cjs | 4 + resources/tester/helper_384_2_page_data.cjs | 22 ++++ .../helper_b05_routeedit_cross_width.cjs | 6 ++ resources/tester/test_384_2_page_data.py | 2 +- resources/tester/test_384_2b_route_id.py | 102 ++++++++++++++++++ .../tester/test_b05_routeedit_cross_width.py | 4 +- 17 files changed, 235 insertions(+), 58 deletions(-) create mode 100644 A00_Common/design/design_page_route.ts create mode 100644 A00_Common/design/design_profile_context.ts create mode 100644 resources/tester/test_384_2b_route_id.py diff --git a/A00_Common/design/design_page.ts b/A00_Common/design/design_page.ts index 2735e47b8..e3bb1ac5c 100644 --- a/A00_Common/design/design_page.ts +++ b/A00_Common/design/design_page.ts @@ -274,7 +274,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage { slots.clear(); shared.profile?.dispose(); stopBusy(); - forgetPageData(projectId); // 다시 열면 저장분 새로(386-2) + forgetPageData(projectId, true); // 다시 열면 저장분 · 노선 id 새로(386-2) delete (window as unknown as { __designPage?: unknown }).__designPage; }, }; diff --git a/A00_Common/design/design_page_data.ts b/A00_Common/design/design_page_data.ts index 6de98d569..752bfccf0 100644 --- a/A00_Common/design/design_page_data.ts +++ b/A00_Common/design/design_page_data.ts @@ -9,9 +9,14 @@ * 종류 목록은 `fetchStructureTypes` 가 탭 수명 동안 쥠 — 정의 판 확인만 페이지마다 한 번. * 돌려줄 때마다 사본 — 받는 쪽이 고쳐도 다른 쪽 저장분이 안 바뀜. * 「계산 중」 — 묶음 차례가 떠 있는 동안 좌측 아래 점(`onPageBusy`). + * 노선 id 한 칸(386-2b) — 횡단 context(서버 지금 노선 · 세션 캐시)에서 채움 · 두 보기 · 종단도 · [저장] · + * 평면 횡단 판이 여기서만 읽음 · 노선이 바뀌면 페이지를 통째 다시 세움(remountRoute) → 페이지 동안 고정. * ========================================================================== */ import { fetchDetailPipePoints } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch"; +import type { SectionContextResponse } from "../../B06_Section/B06_Section_Api_Fetch"; +import { setPageRouteId } from "./design_page_route"; +export { pageRouteId } from "./design_page_route"; import { dropStaleStructureTypes, fetchStructures, @@ -41,9 +46,19 @@ export const pagePipes = (projectId: string): ReturnType => once(`types:${projectId}`, () => dropStaleStructureTypes()); -/** 저장 정본이 바뀜 · 페이지를 떠남 — 다음에 부르면 한 번 다시 받음. */ -export function forgetPageData(projectId: string): void { +/** 저장 정본이 바뀜 · 페이지를 떠남(`leaving` = 노선 id 칸도) — 다음에 부르면 한 번 다시 받음. */ +export function forgetPageData(projectId: string, leaving = false): void { for (const key of [...memo.keys()]) if (key.endsWith(`:${projectId}`)) memo.delete(key); + if (leaving) setPageRouteId(projectId, null); +} + +/** 횡단 context(세션 캐시 · 새 요청 없음) — 노선 id 칸을 채움 · 두 보기가 이것으로만 받음. 서버 창구는 부를 때 + * 불러옴(묶음 · 초안 측점이 끄는 이 파일이 화면 모듈을 안 끌게). */ +export async function pageSectionContext(projectId: string): Promise { + const { fetchSectionContext } = await import("../../B06_Section/B06_Section_Api_Fetch"); + const context = await fetchSectionContext(projectId); + setPageRouteId(projectId, context.route_id); // 노선 id 한 칸(design_page_route) + return context; } const busy = new Map(); diff --git a/A00_Common/design/design_page_route.ts b/A00_Common/design/design_page_route.ts new file mode 100644 index 000000000..3c9c1f6c5 --- /dev/null +++ b/A00_Common/design/design_page_route.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * A00_Common/design/design_page_route.ts + * 한 페이지 노선 id 한 칸(386-2b) — 채우는 곳은 페이지 자료 줄 `pageSectionContext`(design_page_data) 하나 · + * 읽는 곳은 `pageRouteId` 만. 노선이 바뀌면 페이지를 통째 다시 세움(remountRoute) → 페이지 동안 고정. + * 끌어오는 모듈 없음(평면 횡단 판 · 묶음 시험이 화면 · 서버 창구를 안 끌게). + * ========================================================================== */ + +const routes = new Map(); + +/** 이 페이지의 노선 id(없으면 null). */ +export const pageRouteId = (projectId: string): number | null => routes.get(projectId) ?? null; + +/** 자료 줄만 부름 — context 노선(null = 비움). */ +export function setPageRouteId(projectId: string, routeId: number | null): void { + if (routeId === null) routes.delete(projectId); + else routes.set(projectId, routeId); +} diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index ee077f1fd..45cf43496 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -72,13 +72,14 @@ import { import { planCurveWarning, sampleAt } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Guard"; import { listedBase } from "./design_profile_base"; import * as pagePipeline from "./design_pipeline_page"; -import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types"; import type { ElevationWindowResult } from "@util/common_util_chart_ywindow"; import { frameOf } from "@util/common_util_view_window"; -import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots"; +import type { DesignSideBoard, DesignView } from "./design_profile_slots"; import { noticeDroppedEdits } from "./design_save"; import { createProfileOverlays } from "./design_profile_overlays"; import { bindProfileViewport, createSelectionReveal } from "./design_profile_viewport"; +import { CROSS_PREVIEW_DEBOUNCE_MS, type DesignProfileContext } from "./design_profile_context"; +import { pageRouteId } from "./design_page_route"; import "../../B06_Section/B06_Section_UI_Style.css"; // SVG 차트 색상(.b06-chart__*)의 정의처는 _Style_Cross.css다. 이걸 빼면 B05로 바로 진입했을 때 // 배경 rect가 브라우저 기본 fill(검정)로 그려진다 — B06을 먼저 방문해야 정상으로 보이던 원인. @@ -92,31 +93,7 @@ export type { DesignSideHost, DesignView, } from "./design_profile_slots"; - -/** 종단도를 만들 때 넘기는 것. */ -export interface DesignProfileContext { - projectId: string; - /** 처음 보기. */ - view: DesignView; - /** 오른쪽 판 — 없으면 판 없이 종단 열만. */ - side?: DesignSideFactory; - /** 도구줄 [초기화] 바로 왼쪽에 놓는 단추([새 창]) — 다시 그려도 그 자리. */ - toolLead?: HTMLElement; - /** 측점 선택 변경 — 같은 측점 재선택·빈 곳 클릭이면 null(해제)로 알린다. */ - onSelectStation: (stationId: string | null) => void; - /** [초기선 복원] 클릭 시 함께 실행(비정규 측점 등 다른 조작값도 초기화하려고 페이지가 넘긴다). */ - onResetAll?: () => void; - callbacks?: RouteProfilePanelCallbacks; -} - -/** 계획고 편집 후 횡단을 다시 계산하기까지 기다리는 시간(ms). - * ▲/▼ 길게 누르기(초당 10회)·끌기에서 매 프레임 다시 돌지 않게 마지막 값만 계산한다. - * 계산이 브라우저 안에서 끝나면서(2026-09-03) 서버 왕복이 사라져 250→60ms 로 줄였다 — - * 실측 전 측점(128) 재계산 7.5ms 라 손을 떼는 즉시 유토곡선이 따라온다. - * 60→16ms(2026-09-06) — ▲ 한 번에 라벨이 서기까지 중앙 130ms 를 쟀는데 그중 60ms 가 - * 이 대기였다. 한 프레임(16ms)이면 연속 입력은 여전히 마지막 값만 계산한다. - * 횡단 보기는 120ms — 계산마다 카드를 갈아 끼워 길게 누르면 멈춤이 쌓임(9da48d5e7 · 54-22). */ -const CROSS_PREVIEW_DEBOUNCE_MS = 16; +export type { DesignProfileContext } from "./design_profile_context"; export function createDesignProfile(ctx: DesignProfileContext) { const { projectId, onSelectStation, onResetAll, callbacks } = ctx; @@ -491,7 +468,8 @@ export function createDesignProfile(ctx: DesignProfileContext) { root.dataset.view = next; drainagePanel?.setView?.(next); }, - render(nextDetail: SectionDetailResponse, nextStationInterval?: number, nextRouteId?: number) { + render(nextDetail: SectionDetailResponse, nextStationInterval?: number, givenRouteId?: number) { + const nextRouteId = pageRouteId(projectId) ?? givenRouteId; // 노선 id 한 칸(386-2b) detail = nextDetail; stationInterval = nextStationInterval; const stored = readAlignment(nextDetail.longitudinal); diff --git a/A00_Common/design/design_profile_context.ts b/A00_Common/design/design_profile_context.ts new file mode 100644 index 000000000..bcb248dbe --- /dev/null +++ b/A00_Common/design/design_profile_context.ts @@ -0,0 +1,32 @@ +/* ============================================================================= + * A00_Common/design/design_profile_context.ts + * 종단도 한 벌(`design_profile`)을 만들 때 넘기는 것 · 횡단 미리보기 대기 — 700줄 쪼갬(386-2b). + * ========================================================================== */ + +import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types"; +import type { DesignSideFactory, DesignView } from "./design_profile_slots"; + +/** 종단도를 만들 때 넘기는 것. */ +export interface DesignProfileContext { + projectId: string; + /** 처음 보기. */ + view: DesignView; + /** 오른쪽 판 — 없으면 판 없이 종단 열만. */ + side?: DesignSideFactory; + /** 도구줄 [초기화] 바로 왼쪽에 놓는 단추([새 창]) — 다시 그려도 그 자리. */ + toolLead?: HTMLElement; + /** 측점 선택 변경 — 같은 측점 재선택·빈 곳 클릭이면 null(해제)로 알린다. */ + onSelectStation: (stationId: string | null) => void; + /** [초기선 복원] 클릭 시 함께 실행(비정규 측점 등 다른 조작값도 초기화하려고 페이지가 넘긴다). */ + onResetAll?: () => void; + callbacks?: RouteProfilePanelCallbacks; +} + +/** 계획고 편집 후 횡단을 다시 계산하기까지 기다리는 시간(ms). + * ▲/▼ 길게 누르기(초당 10회)·끌기에서 매 프레임 다시 돌지 않게 마지막 값만 계산한다. + * 계산이 브라우저 안에서 끝나면서(2026-09-03) 서버 왕복이 사라져 250→60ms 로 줄였다 — + * 실측 전 측점(128) 재계산 7.5ms 라 손을 떼는 즉시 유토곡선이 따라온다. + * 60→16ms(2026-09-06) — ▲ 한 번에 라벨이 서기까지 중앙 130ms 를 쟀는데 그중 60ms 가 + * 이 대기였다. 한 프레임(16ms)이면 연속 입력은 여전히 마지막 값만 계산한다. + * 횡단 보기는 120ms — 계산마다 카드를 갈아 끼워 길게 누르면 멈춤이 쌓임(9da48d5e7 · 54-22). */ +export const CROSS_PREVIEW_DEBOUNCE_MS = 16; diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index c1ae3da63..289199e3c 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -19,8 +19,8 @@ import { showToast } from "@ui/ui_template_elements"; import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations"; import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures"; import { pipeIdAt } from "./design_pipe_ids"; +import { pageRouteId, pageSectionContext } from "./design_page_data"; import { - fetchSectionContext, getSections, type SectionContextResponse, type SectionDetailResponse, @@ -579,7 +579,7 @@ export async function mountCrossView( return; } try { - context = await fetchSectionContext(projectId); + context = await pageSectionContext(projectId); // 노선 id 한 칸을 채움(386-2b) } catch { showToast(L("B06_Profile_Context_Failed"), "error"); } @@ -601,13 +601,12 @@ export async function mountCrossView( rememberRockBoundaryDefault(projectId, context.rock_boundary_default_offset_m); standardPanelSlot.append(standardPanel.root); - if (context.route_id === null) { + currentRouteId = pageRouteId(projectId); + if (currentRouteId === null) { // 자료가 통째로 없으면(새 자료가 올라와 옛 결과가 지워진 경우) 대시보드로 돌려보낸다. leaveForDashboard(); return; } - - currentRouteId = context.route_id; rockStore.load(); cutSlopeStore.load(); stationControls.load(); @@ -615,13 +614,13 @@ export async function mountCrossView( // 섰으면(종단 보기 [확정]) 한 벌 폼은 종단 보기 몫이라 싣지 않는다 — 보일 때 싣는다. if (host.visible()) void structuresPanel.load().then(restoreStructurePick); try { - const existing = await getSections(projectId, context.route_id); + const existing = await getSections(projectId, currentRouteId); if (!existing.longitudinal) { leaveForDashboard(); return; } // 공유 캐시 — 종단 보기가 이미 받아 뒀으면 같은 객체를 즉시 재사용한다. - sectionDetail = await loadSectionDetail(projectId, context.route_id); + sectionDetail = await loadSectionDetail(projectId, currentRouteId); structuresPanel.facility.recount(); // 단 수는 횡단 자료로 셈 — 흙막이 상자 · 목록 램프 다시(54-15) // 구조물 초안(저장 안 한 벽) · 그 면적은 상세 가게가 얹어 줌(387-4 · 화면은 안 얹음). // 단일 소스(DB data.options) 우선, options 스냅샷이 없는 과거 데이터는 샘플 최대 offset으로 추정 diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index 7334d380c..47787bced 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -21,6 +21,7 @@ import { leaveForDashboard } from "../b_missing_data_guard"; import { currentRoute, remountRoute } from "../router"; import { mountRouteEditor } from "../../B05_Profile/B05_Profile_UI_RouteEdit"; import { routeDraftBridge } from "./design_pipeline_page"; +import { pageRouteId, pageSectionContext } from "./design_page_data"; import { createSelectionSync, graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection"; import { readStructurePick, @@ -47,7 +48,6 @@ import { rememberRockBoundaryDefault, rememberStandardDefaults, } from "../../B06_Section/B06_Section_UI_Standard_Panel"; -import { fetchSectionContext } from "../../B06_Section/B06_Section_Api_Fetch"; import { invalidateSectionDetail } from "../../B06_Section/B06_Section_Section_Store"; import { onStaleStructureTypes, @@ -490,7 +490,7 @@ export async function mountProfileView( const cachedLatest = latestSource.read(); const [latestResponse, sectionContext, configuredRoadWidths] = await Promise.all([ cachedLatest ? Promise.resolve(cachedLatest) : loadLatest(true), - fetchSectionContext(activeProjectId), + pageSectionContext(activeProjectId), // 노선 id 한 칸을 채움(386-2b) fetchRoadWidths(activeProjectId), ]); if (host.disposed()) return; @@ -540,7 +540,8 @@ export async function mountProfileView( }); // ③ 종단면·횡단 자료 — 하단 패널을 3D보다 먼저 채운다. - if (latestResponse.route) await restoreSections(latestResponse.route.id); + const routeId = pageRouteId(activeProjectId); // 노선 id 한 칸(386-2b) + if (latestResponse.route && routeId !== null) await restoreSections(routeId); // 횡단 보기가 먼저 서서 배수유역도가 관 목록을 이미 알렸다면 그것을 먼저 싣는다 — 빈 목록으로 // 시작하면 아래 구조물 로딩이 관 대조로 관을 지우고 초안 옵션이 자동 배치로 되돌아간다(24-7a). if (lastPipes) applyPipes(lastPipes); @@ -559,7 +560,7 @@ export async function mountProfileView( return; } renderLatest(fresh); - if (fresh.route?.id) await restoreSections(fresh.route.id); + if (fresh.route && routeId !== null) await restoreSections(routeId); }); } } catch (error) { diff --git a/A00_Common/design/design_view_profile_3d.ts b/A00_Common/design/design_view_profile_3d.ts index 435f3b941..5b6058c80 100644 --- a/A00_Common/design/design_view_profile_3d.ts +++ b/A00_Common/design/design_view_profile_3d.ts @@ -16,6 +16,7 @@ import type { RouteViewer } from "../../B05_Profile/B05_Profile_UI_Viewer"; import type { DesignPageHost } from "./design_page"; import type { createProfileSections, ProfileViewState } from "./design_view_profile_sections"; import { createViewer3dModal } from "./design_view_profile_3d_modal"; +import { pageRouteId } from "./design_page_route"; export interface Profile3dDeps { projectId: string; @@ -62,13 +63,14 @@ export function createProfile3d(deps: Profile3dDeps) { async function refreshCorridorNow(): Promise { const detail = state.detail; const latest = state.latest; - if (!detail || !latest?.route?.id) return; + const routeId = pageRouteId(projectId); // 노선 id 한 칸(386-2b) + if (!detail || !latest?.route || routeId === null) return; const routePoints = latest.route_points ?? []; if (routePoints.length <= 1) return; await refreshCorridor( viewer, projectId, - latest.route.id, + routeId, detail, routePoints, deps.profilePanel().alignmentSamples() ?? undefined, diff --git a/A00_Common/design/design_view_profile_sections.ts b/A00_Common/design/design_view_profile_sections.ts index 47787fe6a..f83236d94 100644 --- a/A00_Common/design/design_view_profile_sections.ts +++ b/A00_Common/design/design_view_profile_sections.ts @@ -19,6 +19,7 @@ import { swapSectionStations, } from "../../B06_Section/B06_Section_Section_Store"; import { useDesignStore } from "./design_store"; +import { pageRouteId } from "./design_page_route"; import { migrateLegacyStations } from "../../B05_Profile/B05_Profile_Api_Structures"; import { loadCorridorIfFresh } from "../../B05_Profile/B05_Profile_UI_Corridor"; import { stationLinesOf } from "../../B05_Profile/B05_Profile_UI_Page_Stations"; @@ -97,7 +98,7 @@ export function createProfileSections(deps: ProfileSectionsDeps) { void loadCorridorIfFresh( viewer, projectId, - routeId ?? state.latest?.route?.id, + routeId ?? pageRouteId(projectId) ?? undefined, // 노선 id 한 칸(386-2b) detail, routePoints, profilePanel.alignmentSamples() ?? undefined, diff --git a/B05_Profile/B05_Profile_UI_Page_Actions.ts b/B05_Profile/B05_Profile_UI_Page_Actions.ts index add7a35ff..383de287c 100644 --- a/B05_Profile/B05_Profile_UI_Page_Actions.ts +++ b/B05_Profile/B05_Profile_UI_Page_Actions.ts @@ -10,6 +10,7 @@ import { confirmRoute, type RouteLatestResponse } from "./B05_Profile_Api_Fetch"; import type { DesignViewSave } from "../A00_Common/design/design_shared"; import { invalidateSectionDetail } from "../B06_Section/B06_Section_Section_Store"; +import { pageRouteId } from "../A00_Common/design/design_page_route"; import type { createRoutePanel } from "./B05_Profile_UI_Panel"; import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel"; import type { createStructuresBridge } from "./B05_Profile_UI_Page_Structures"; @@ -40,7 +41,7 @@ export interface PageActionContext { export function profileSaveParts(ctx: PageActionContext): DesignViewSave { const projectId = ctx.projectId; return { - routeId: () => (ctx.routeReady() ? (ctx.latest()?.route?.id ?? null) : null), + routeId: () => (ctx.routeReady() ? pageRouteId(projectId) : null), // 노선 id 한 칸(386-2b) steps: { // 구조물 — 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다. 실패는 자체 토스트로 알리고 // 세션에 남겨 두므로 저장 전체를 멈추지 않는다(2026-08-29 — CLAUDE.md 5장). diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 0defd6e76..d3593c60b 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -40,7 +40,8 @@ import { CROSS_PLOT_PAD, summarizeCross, } from "./B05_Profile_UI_RouteEdit_Cross_Draw"; -import { readByKey, routeStateKeys, stateKey } from "../A00_Common/b_page_state"; +import { readStateRaw } from "../A00_Common/b_page_state"; +import { pageRouteId } from "../A00_Common/design/design_page_route"; import { stationLabel as formatStation } from "../common_util/common_util_station"; import { sidePanelsOf } from "./B05_Profile_UI_RouteEdit_Chrome"; import type { FootprintRecord } from "./logic/B05_Profile_PlanFootprint"; @@ -107,17 +108,11 @@ interface CrossPane { /** * 우측 패널 「횡단 반폭(m)」에 **적용해 둔 값**(m). 적용한 적이 없으면 undefined — 서버가 기본값을 쓴다. - * - * B06 이 [적용]에서 노선 단위 세션 키에 적는다. 이 창은 노선 번호를 몰라 프로젝트 접두어로 - * 훑고, 노선이 여럿 남아 있으면 번호가 가장 큰(최신) 것을 쓴다. + * B06 이 [적용]에서 노선 단위 세션 키에 적음 · 노선 = 페이지 노선 id 한 칸(386-2b). */ function appliedHalfWidth(projectId: string): number | undefined { - const prefix = stateKey("cross-display", projectId, ""); - if (!prefix) return undefined; - const latest = routeStateKeys("cross-display", projectId).sort( - (a, b) => Number(b.slice(prefix.length)) - Number(a.slice(prefix.length)), - )[0]; - const value = Number(latest ? readByKey(latest) : NaN); + const routeId = pageRouteId(projectId); + const value = Number(routeId === null ? NaN : readStateRaw("cross-display", projectId, routeId)); return Number.isFinite(value) && value > 0 ? value : undefined; } diff --git a/resources/tester/helper_273_2_preview_stations.cjs b/resources/tester/helper_273_2_preview_stations.cjs index 88c4dd05d..958cb22a7 100644 --- a/resources/tester/helper_273_2_preview_stations.cjs +++ b/resources/tester/helper_273_2_preview_stations.cjs @@ -51,6 +51,10 @@ const drafts = loadTs("B06_Section/B06_Section_Section_Drafts.ts", { "../A00_Common/design/design_page_data": loadTs("A00_Common/design/design_page_data.ts", { "../../B04_PreProcess/B04_PreProcess_Api_Fetch": serverFetch, "../../B05_Profile/B05_Profile_Api_Structures": serverStructures, + "../../B06_Section/B06_Section_Api_Fetch": { + fetchSectionContext: async () => ({ route_id: 1 }), + }, + "./design_page_route": loadTs("A00_Common/design/design_page_route.ts"), // 노선 id 한 칸(386-2b) }), "../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => draftPipes }, "../B05_Profile/B05_Profile_Api_PlusStations": { readPlusDraft: () => null }, diff --git a/resources/tester/helper_384_2_page_data.cjs b/resources/tester/helper_384_2_page_data.cjs index 0f9ef6f89..1835191dc 100644 --- a/resources/tester/helper_384_2_page_data.cjs +++ b/resources/tester/helper_384_2_page_data.cjs @@ -7,7 +7,14 @@ const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "t const ROOT = path.join(__dirname, "..", ".."); const calls = { structures: 0, pipes: 0, version: 0 }; let failPipes = false; +const routeJs = ts.transpileModule( + fs.readFileSync(path.join(ROOT, "A00_Common", "design", "design_page_route.ts"), "utf8"), + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }, +).outputText; +const routeBox = { exports: {} }; +new Function("exports", "module", "require", routeJs)(routeBox.exports, routeBox, () => ({})); const STUBS = { + "./design_page_route": routeBox.exports, // 노선 id 한 칸(386-2b · 진짜) "../../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => { calls.pipes += 1; @@ -15,6 +22,10 @@ const STUBS = { return { pipe_points: [{ chainage_m: 10, facility: "pipe" }], basins: [] }; }, }, + // 횡단 context(세션 캐시) — 노선 id 한 칸(386-2b) + "../../B06_Section/B06_Section_Api_Fetch": { + fetchSectionContext: async (projectId) => ({ route_id: projectId === "p-none" ? null : 304 }), + }, "../../B05_Profile/B05_Profile_Api_Structures": { fetchStructures: async () => { calls.structures += 1; @@ -85,6 +96,17 @@ const tick = () => new Promise((resolve) => setTimeout(resolve, 0)); data.trackPageBusy(P, Promise.resolve()); await tick(); out.busy = seen; + // 노선 id 한 칸(386-2b) — context 가 채움 · 저장 정본 바뀜(비움)엔 그대로 · 페이지 떠남에 비움 + const route = { before: data.pageRouteId(P) }; + route.context = (await data.pageSectionContext(P)).route_id; + route.after = data.pageRouteId(P); + data.forgetPageData(P); + route.keptOnSave = data.pageRouteId(P); + data.forgetPageData(P, true); + route.leaving = data.pageRouteId(P); + await data.pageSectionContext("p-none"); + route.none = data.pageRouteId("p-none"); + out.route = route; process.stdout.write(JSON.stringify(out)); })().catch((error) => { console.error(error); diff --git a/resources/tester/helper_b05_routeedit_cross_width.cjs b/resources/tester/helper_b05_routeedit_cross_width.cjs index b3e7ce55c..c27f0dfd8 100644 --- a/resources/tester/helper_b05_routeedit_cross_width.cjs +++ b/resources/tester/helper_b05_routeedit_cross_width.cjs @@ -137,6 +137,8 @@ const { createCrossPreview } = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Cross.ts"), ); const pick = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts")); +// 페이지 노선 id 한 칸(386-2b) — 판이 읽는 적용값 = 이 노선 것만 +const pageRoute = loadTs(path.join(ROOT, "A00_Common", "design", "design_page_route.ts")); const record = (key, chainage_m, left, right, leftCut, rightCut) => ({ key, @@ -161,12 +163,16 @@ async function run() { out.before = cross.watched(); await cross.open(100); out.empty = { watched: cross.watched() }; + pageRoute.setPageRouteId("p-1", 3); session.setItem("aislo:draft:cross-display:p-1:3", "15"); await cross.refresh(); out.applied15 = { watched: cross.watched() }; session.setItem("aislo:draft:cross-display:p-1:12", "8.5"); session.setItem("aislo:draft:cross-display:p-2:99", "40"); await cross.refresh(); + out.otherRoute = { watched: cross.watched() }; // 다른 노선 · 다른 프로젝트 값은 안 봄 + pageRoute.setPageRouteId("p-1", 12); + await cross.refresh(); out.latestRoute = { watched: cross.watched() }; session.setItem("aislo:draft:cross-display:p-1:12", "abc"); await cross.refresh(); diff --git a/resources/tester/test_384_2_page_data.py b/resources/tester/test_384_2_page_data.py index 10465962a..b35dbbef0 100644 --- a/resources/tester/test_384_2_page_data.py +++ b/resources/tester/test_384_2_page_data.py @@ -87,7 +87,7 @@ def test_비우는_자리(): store = _read("B06_Section/B06_Section_Section_Store.ts") assert "forgetPageData(projectId)" in drafts # 저장 정본 바뀜(invalidate → forgetDraftStations) assert "forgetDraftStations(projectId)" in store - assert "forgetPageData(projectId)" in page # 페이지를 떠남 + assert "forgetPageData(projectId, true)" in page # 페이지를 떠남(노선 id 칸도 · 386-2b) assert "onPageBusy(projectId" in page diff --git a/resources/tester/test_384_2b_route_id.py b/resources/tester/test_384_2b_route_id.py new file mode 100644 index 000000000..70cbc9390 --- /dev/null +++ b/resources/tester/test_384_2b_route_id.py @@ -0,0 +1,102 @@ +"""PLAN 386-2b sub7 — 노선 id 한 칸(design_page_data). + +못박는 것 + - 출처 하나 = `pageSectionContext`(횡단 context · 세션 캐시)가 채운 칸 — 두 보기는 context 를 이것으로만 받음. + - 읽는 곳(옛 출처 다섯) = 종단 보기 [저장] 몫(PA) · 횡단 보기 · 종단도 render · 평면 횡단 판(RC) · 3D · 측점 그림 — + `pageRouteId` 만 · 묶음(PP)은 종단도 `pipelineSource().routeId`(= 칸)를 받음. + - 칸 = 저장 정본 바뀜(invalidate)엔 그대로 · 페이지 떠남에 비움 · context 노선 없음 = null. + +짝: `helper_384_2_page_data.cjs`(진짜 TS · 서버 흉내) — `test_384_2_page_data.py` 와 같은 헬퍼. +""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + +READERS = { + "B05_Profile/B05_Profile_UI_Page_Actions.ts": "pageRouteId(projectId)", + "A00_Common/design/design_view_cross.ts": "currentRouteId = pageRouteId(projectId);", + "A00_Common/design/design_profile.ts": "pageRouteId(projectId) ?? givenRouteId", + "B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts": "pageRouteId(projectId)", + "A00_Common/design/design_view_profile.ts": "pageRouteId(activeProjectId)", + "A00_Common/design/design_view_profile_3d.ts": "pageRouteId(projectId)", + "A00_Common/design/design_view_profile_sections.ts": "pageRouteId(projectId)", +} + + +def _read(rel: str) -> str: + return (ROOT / rel).read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def r() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HERE / "helper_384_2_page_data.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_칸_채움_비움(r): + route = r["route"] + assert route["before"] is None and route["context"] == 304 and route["after"] == 304 + assert route["keptOnSave"] == 304 # 저장 정본 바뀜(비움)엔 그대로 + assert route["leaving"] is None # 페이지 떠남 + assert route["none"] is None # context 노선 없음 + + +@pytest.mark.parametrize("rel", sorted(READERS)) +def test_읽는_곳은_칸만(rel): + src = _read(rel) + assert READERS[rel] in src + assert "context.route_id" not in src + used = [ + line + for line in src.splitlines() + if re.search(r"(latest(Response)?|ctx\.latest\(\))\??\.route\??\.id\b", line) + and "!==" not in line # 노선이 바뀌었나 견줌(다시 세움) = 출처 아님 + ] + assert not used + + +def test_출처는_자료_줄_하나(): + data = _read("A00_Common/design/design_page_data.ts") + assert "setPageRouteId(projectId, context.route_id)" in data + slot = _read("A00_Common/design/design_page_route.ts") + assert not re.search(r"^import ", slot, re.M) # 끄는 모듈 없음 + found = subprocess.run( # noqa: S603, S607 — 고정 실행 파일 + ["git", "grep", "-l", "setPageRouteId(", "--", "*.ts"], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + ) + users = set(found.stdout.split()) | { + "A00_Common/design/design_page_route.ts" + } # 새 파일(추적 전) + assert sorted(users) == [ + "A00_Common/design/design_page_data.ts", + "A00_Common/design/design_page_route.ts", + ] + for rel in ( + "A00_Common/design/design_view_cross.ts", + "A00_Common/design/design_view_profile.ts", + ): + src = _read(rel) + assert "fetchSectionContext(" not in src and "pageSectionContext(" in src + cross = _read("B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts") + assert "routeStateKeys" not in cross # 노선 번호를 몰라 훑던 길 지움 + page = _read("A00_Common/design/design_page.ts") + assert "forgetPageData(projectId, true)" in page diff --git a/resources/tester/test_b05_routeedit_cross_width.py b/resources/tester/test_b05_routeedit_cross_width.py index 8a99b6879..4f95bc83f 100644 --- a/resources/tester/test_b05_routeedit_cross_width.py +++ b/resources/tester/test_b05_routeedit_cross_width.py @@ -39,7 +39,9 @@ def test_적용값이_있으면_그_값_양쪽(results): assert results["applied15"]["watched"] == {"chainageM": 100, "leftM": 15, "rightM": 15} -def test_노선이_여럿이면_최신_노선_값_다른_프로젝트는_무시(results): +def test_페이지_노선_값만_다른_노선_프로젝트는_무시(results): + # 노선 = 페이지 노선 id 한 칸(386-2b · 옛 「가장 큰 노선 번호」 훑기 지움) + assert results["otherRoute"]["watched"]["leftM"] == 15 assert results["latestRoute"]["watched"]["leftM"] == 8.5