Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-10-17 19:19:41 +09:00
17 changed files with 235 additions and 58 deletions
+1 -1
View File
@@ -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;
},
};
+17 -2
View File
@@ -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<typeof fetchDetailPipeP
export const pageTypesChecked = (projectId: string): Promise<boolean> =>
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<SectionContextResponse> {
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<string, number>();
+17
View File
@@ -0,0 +1,17 @@
/* =============================================================================
* A00_Common/design/design_page_route.ts
* 한 페이지 노선 id 한 칸(386-2b) — 채우는 곳은 페이지 자료 줄 `pageSectionContext`(design_page_data) 하나 ·
* 읽는 곳은 `pageRouteId` 만. 노선이 바뀌면 페이지를 통째 다시 세움(remountRoute) → 페이지 동안 고정.
* 끌어오는 모듈 없음(평면 횡단 판 · 묶음 시험이 화면 · 서버 창구를 안 끌게).
* ========================================================================== */
const routes = new Map<string, number>();
/** 이 페이지의 노선 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);
}
+6 -28
View File
@@ -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);
@@ -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;
+6 -7
View File
@@ -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으로 추정
+5 -4
View File
@@ -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) {
+4 -2
View File
@@ -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<void> {
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,
@@ -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,
+2 -1
View File
@@ -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장).
+5 -10
View File
@@ -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;
}
@@ -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 },
@@ -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);
@@ -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();
+1 -1
View File
@@ -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
+102
View File
@@ -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
@@ -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