fix(463 B05 B06 한 페이지): 페이지 설정 한 칸 · 강제 받기 · 측점 만들기 뒤 상세 알림 · [저장] 상단측 차례 같은 본문

- (B) 종단도 기준(기울기 상한 · 최소고 강제 · 시작 측점) = 세션 초안 ?? 저장 노선 설정 — 두 보기가 그린 뒤 applyPageSettings 하나 · B06 이 받은 노선은 캐시에
- (C) 상세 강제 다시 받기 = 듣는 쪽 한 번(숨은 종단 보기 옛 객체 안 쥠) · [측점 만들기] 뒤 캐시 비우고 강제로 받음
- (D) [저장] 상단측 차례 = flushRouteParts 한 벌(노선 캐시 지표 조건 + 확정 지표면 id · 비정규 빈 목록) — 종단 보기를 열든 안 열든 같은 요청
- 시험 test_271_detail_notify · test_271_save_same · test_271_profile_settings · 옛 저장 시험 가짜 창구에 flushRouteParts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 23:51:49 +09:00
co-authored by Claude Opus 5.5
parent 1ca9f11a11
commit 60acb5ba79
23 changed files with 822 additions and 62 deletions
+2 -5
View File
@@ -37,10 +37,7 @@ import {
writePendingStructures,
} from "../../B05_Profile/B05_Profile_Api_Structures";
import { flushPlusStations, outsideNotice } from "../../B05_Profile/B05_Profile_Api_PlusStations";
import {
flushUphillOverrides,
saveProfileAlignment,
} from "../../B05_Profile/B05_Profile_Api_Fetch";
import { flushRouteParts, saveProfileAlignment } from "../../B05_Profile/B05_Profile_Api_Fetch";
import {
markAlignmentSaved,
pendingAlignment,
@@ -190,7 +187,7 @@ function defaultStep(
case "culvertOptions":
return () => flushCulvertOptions(projectId, stateKey("culvertopt", projectId, fromRouteId));
case "uphill":
return () => flushUphillOverrides(projectId);
return () => flushRouteParts(projectId); // 종단 보기 몫과 같은 본문(463)
}
}
@@ -24,7 +24,7 @@ import { migrateLegacyStations } from "../../B05_Profile/B05_Profile_Api_Structu
import { loadCorridorIfFresh } from "../../B05_Profile/B05_Profile_UI_Corridor";
import { stationLinesOf } from "../../B05_Profile/B05_Profile_UI_Page_Stations";
import { restorePoints, type RoadWidths } from "../../B05_Profile/B05_Profile_UI_Page_Helpers";
import { panelValuesOf } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
import { applyPageSettings, panelValuesOf } from "../../B05_Profile/B05_Profile_UI_Page_Latest";
/** 종단 보기 본체 · 조각이 함께 보는 값 한 벌. */
export interface ProfileViewState {
@@ -85,10 +85,8 @@ export function createProfileSections(deps: ProfileSectionsDeps) {
state.detail = detail;
profilePanel.render(detail, panel.values().stationInterval ?? undefined, routeId);
// 저장된 계획선의 정책은 만들 당시 기준이라 지금 설정(임도 종류·설계속도·지형)과
// 다를 수 있다 — 그릴 때마다 현재 기준으로 맞춘다(2026-08-19).
profilePanel.setGradeLimit(panel.gradeLimitPct());
profilePanel.setEnforceMinCover(panel.values().enforcePipeClearance);
profilePanel.setStationDisplay(panel.stationDisplayOffset());
// 다를 수 있다 — 그릴 때마다 페이지 설정 한 칸(세션 초안 ?? 저장 설정 · 횡단 보기와 같은 길 · 463)으로 맞춘다.
applyPageSettings(profilePanel, projectId, state.latest ?? undefined);
profilePanel.setIrregularStations(deps.bridge().irregularStations());
renderStationLines(detail);
// 예상형상 코리도 — 저장본이 지금 정본과 맞을 때만 올린다. 낡았으면 [3D 업데이트] 대기
+29 -4
View File
@@ -12,6 +12,7 @@
* ========================================================================== */
import { clearState, readState, writeState } from "../A00_Common/b_page_state";
import { fetchConfirmedSurface } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { API_ANALYSIS_TIMEOUT_MS, API_BASE_URL, API_TIMEOUT_MS } from "@config/config_frontend";
@@ -299,14 +300,38 @@ export async function confirmRoute(
* B06 에서 저장하면 이 값이 세션에만 남아 확정 뒤 옛 측구 방향이 그대로 쓰였다
* (2026-09-06 대응표 조사). 비어 있으면 요청을 내지 않는다. */
export async function flushUphillOverrides(projectId: string): Promise<void> {
const stored = readState<Record<string, "left" | "right">>("uphill", projectId);
const overrides = Object.entries(stored ?? {})
.filter(([, side]) => side === "left" || side === "right")
.map(([chainage, side]) => ({ chainage_m: Number(chainage), side }));
const overrides = uphillOverridesOf(projectId);
if (!overrides.length) return;
await saveRouteParts(projectId, { uphill_overrides: overrides });
}
/** 세션 상단측(측구 방향) 변경분 — 요청 본문 모양. */
function uphillOverridesOf(
projectId: string,
): NonNullable<RouteConfirmRequest["uphill_overrides"]> {
const stored = readState<Record<string, "left" | "right">>("uphill", projectId);
return Object.entries(stored ?? {})
.filter(([, side]) => side === "left" || side === "right")
.map(([chainage, side]) => ({ chainage_m: Number(chainage), side }));
}
/** [저장] 상단측 차례 본문 한 벌(463 · 요구 271-5) — 노선 캐시 지표 조건 + 확정 지표면 id(서버가 비정규 측점
* 횡단을 맞춤) · 비정규 목록 = 빈 목록(서버가 정본 관 · 구조물에서 셈) · 상단측 = 세션. 종단 보기를 열었든
* 안 열었든 같은 요청(기본 창구 · 종단 보기 몫 둘 다 이것). */
export async function flushRouteParts(projectId: string): Promise<void> {
const latest =
readRouteLatestCache(projectId) ?? (await fetchLatestRoute(projectId).catch(() => null));
const surface = await fetchConfirmedSurface(projectId).catch(() => null);
await saveRouteParts(projectId, {
filter_key: latest?.surface_params.source_filter,
method: latest?.surface_params.method,
smooth: latest?.surface_params.smooth,
surface_model_id: surface?.model_id ?? undefined,
irregular_stations: [],
uphill_overrides: uphillOverridesOf(projectId),
});
}
/** [저장] 차례의 확정(단계 전이 없음) — 하위 차례가 실패하면 던짐(443 · 저장 한 줄기가 그 차례에서 멈춤 ·
* 세션 그대로). 다 되면 보낸 상단측 세션을 비움 — 안 비우면 매 [저장]이 옛 값을 다시 보냄. */
export async function saveRouteParts(projectId: string, body: RouteConfirmRequest): Promise<void> {
+4 -26
View File
@@ -7,8 +7,7 @@
* 조작은 [저장]·[확정]에서만 정본으로 나간다). [초기화]는 한 벌(`design_reset`)이 한다.
* ========================================================================== */
import { saveRouteParts, type RouteLatestResponse } from "./B05_Profile_Api_Fetch";
import { loadUphillOverrides } from "./B05_Profile_UI_Page_Helpers";
import { flushRouteParts, type RouteLatestResponse } from "./B05_Profile_Api_Fetch";
import type { DesignViewSave } from "../A00_Common/design/design_shared";
import { pageRouteId } from "../A00_Common/design/design_page_route";
import type { createRoutePanel } from "./B05_Profile_UI_Panel";
@@ -46,31 +45,10 @@ export function profileSaveParts(ctx: PageActionContext): DesignViewSave {
// 구조물 — 저장 뒤 서버 목록(새 식별자)으로 화면을 맞춘다. 실패는 초안을 남기고 던져
// 저장 한 줄기가 이 차례에서 멈춘다(443).
structures: () => ctx.bridge().saveStructuresIfDirty(),
// 상단측 + 비정규 측점 — 데이터로는 확정 저장하되, 단계 완료 전이는 하지 않는다.
// 하위 실패 = 던짐 · 성공 = 상단측 세션 비움(443). 상단측은 세션에서 읽음 — 노선 바꾼 [저장]이
// 기본 창구로 세션을 비웠으면 메모리 값을 다시 보내지 않게.
// 상단측 + 비정규 측점 횡단 맞춤 — 기본 창구와 같은 본문(463 · 종단 보기를 안 열어도 같은 요청).
// 하위 실패 = 던짐 · 성공 = 상단측 세션 비움(443) · 메모리 값도 비움(다시 안 보냄).
uphill: async () => {
const latest = ctx.latest();
await saveRouteParts(projectId, {
filter_key: latest?.surface_params.source_filter,
method: latest?.surface_params.method,
smooth: latest?.surface_params.smooth,
surface_model_id: ctx.confirmedSurface()?.model_id ?? undefined,
irregular_stations: ctx
.bridge()
.irregularStations()
.map((station) => ({
chainage_m: station.chainage_m,
structure: station.structure,
})),
// 상단측(측구 방향) 사용자 변경분 — 종단 정본에 병합되어 B06이 그대로 소비한다.
uphill_overrides: [...loadUphillOverrides(projectId).entries()].map(
([chainage, side]) => ({
chainage_m: Number(chainage),
side,
}),
),
});
await flushRouteParts(projectId);
ctx.uphillOverrides.clear();
},
},
+23
View File
@@ -12,6 +12,8 @@ import {
} from "./B05_Profile_Api_Fetch";
import { panelValuesOfDraft, readPageSettings } from "./B05_Profile_Api_PageSettings";
import type { RoutePanelValues } from "./B05_Profile_UI_Panel";
import { SPEED_CHOICES, gradeLimitOf } from "./B05_Profile_UI_Panel_Fields";
import type { DesignProfile } from "../A00_Common/design/design_profile";
/**
* 확정 이력이 있으면 매 진입마다 DB(latest) 조회 대신 브라우저 세션 캐시를 우선 사용해
@@ -86,3 +88,24 @@ export function panelValuesOf(next: RouteLatestResponse): Partial<RoutePanelValu
...(draft ? panelValuesOfDraft(draft) : {}),
};
}
/** 페이지 설정 한 칸(463 · 요구 271-5) — 종단도 기울기 상한 · 최소고 강제 · 시작 측점 = 세션 초안(448) ?? 저장 노선
* 설정(빈 칸 = 좌측 패널 기본). 어느 보기로 서도 종단도를 그린 뒤 얹음 · B05 패널은 세션에 쓰고 고친 즉시 얹음. */
export function applyPageSettings(
profile: Pick<DesignProfile, "setGradeLimit" | "setEnforceMinCover" | "setStationDisplay">,
projectId: string,
latest: RouteLatestResponse | null = readRouteLatestCache(projectId),
): void {
const draft = readPageSettings(projectId);
const values = latest ? panelValuesOf(latest) : draft ? panelValuesOfDraft(draft) : null;
if (!values) return; // 모름 — 종단도 그대로(서버 경고 기준)
const allowed = SPEED_CHOICES[values.gradeClass ?? "trunk"] ?? [20];
const speed = allowed.includes(values.designSpeed ?? 20)
? (values.designSpeed ?? 20)
: allowed[0];
profile.setGradeLimit(
gradeLimitOf(speed, values.terrainType ?? "special", values.maxGradePct ?? null),
);
profile.setEnforceMinCover(values.enforcePipeClearance === true);
profile.setStationDisplay(values.stationDisplay ?? { station: 0, cumulative: 0 });
}
@@ -63,6 +63,13 @@ export const PROFILE_CRITERIA: Record<
20: { grade: { normal: 9, special: 14 }, radius: 100 },
};
/** 적용 종단기울기 상한(%) — 넣은 값(> 0)이면 그 값 · 비웠으면 설계속도 · 지형 법정 기준(좌측 패널 · 페이지 설정 한 칸 463). */
export const gradeLimitOf = (
speed: RoutePanelValues["designSpeed"],
terrain: RoutePanelValues["terrainType"],
typed: number | null,
): number => (typed != null && typed > 0 ? typed : PROFILE_CRITERIA[speed].grade[terrain]);
/** 임도 종류별 표시 이름 — 프로젝트 등록(B02)과 같은 말을 쓴다(2026-08-19 확정). */
export const ROAD_TYPE_LABELS: Record<string, string> = {
trunk: "간선임도",
+2 -3
View File
@@ -12,6 +12,7 @@ import { createSelectField } from "@ui/ui_template_elements";
import {
L,
PROFILE_CRITERIA,
gradeLimitOf,
ROAD_TYPE_LABELS,
SPEED_CHOICES,
button,
@@ -187,10 +188,8 @@ export function createSettingsPart(callbacks: SettingsPartCallbacks) {
}
function gradeLimitPct(): number {
const criteria = PROFILE_CRITERIA[currentSpeed()];
const terrain = terrainType.value as RoutePanelValues["terrainType"];
const typed = parseOptional(maxGradePct);
return typed != null && typed > 0 ? typed : criteria.grade[terrain];
return gradeLimitOf(currentSpeed(), terrain, parseOptional(maxGradePct));
}
/** 임도 종류에 맞춰 설계속도 선택지를 갈아 끼운다 — 작업임도는 20만 쓸 수 있다. */
+5 -1
View File
@@ -137,7 +137,11 @@ export async function loadSectionDetail(
cache.set(key, detail);
writeState("section-detail", detail, projectId, routeId);
keepCanon(projectId, routeId, detail);
return withDraftWalls(detail, projectId, routeId);
const ready = await withDraftWalls(detail, projectId, routeId);
// 강제로 받음(넓힘 재생성 · 측점 만들기) = 갈아끼움 — 옛 객체를 쥔 보기(숨은 종단 보기 등)가 새것으로(463)
if (options?.force && cache.get(key) === detail)
listeners.get(key)?.forEach((fn) => fn(ready, new Map()));
return ready;
})
.finally(() => {
pending.delete(key);
@@ -19,6 +19,8 @@ import { createButton, showToast } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import { useDesignStore } from "../A00_Common/design/design_store";
import { savingDesign } from "../A00_Common/design/design_save";
import { pageRouteId } from "../A00_Common/design/design_page_route";
import { invalidateSectionDetail, loadSectionDetail } from "./B06_Section_Section_Store";
interface MissingStation {
chainage_m: number;
@@ -88,6 +90,12 @@ export async function mountMissingStationNotice(
const result = await call(projectId, "POST");
showToast(`측점 ${result.created ?? 0}개를 만들었습니다.`, "success");
box.remove();
// 서버가 측점을 세움 = 옛 상세 — 캐시 비우고 강제로 받아 듣는 쪽(종단 보기)까지 새것(463)
const routeId = pageRouteId(projectId);
if (routeId !== null) {
invalidateSectionDetail(projectId, routeId);
await loadSectionDetail(projectId, routeId, { force: true });
}
await onCreated();
} catch (error) {
const detail = error instanceof Error ? ` ${error.message}` : "";
@@ -15,7 +15,6 @@ import {
sharedProfile,
type DesignPageShared,
} from "../A00_Common/design/design_shared";
import { readState } from "../A00_Common/b_page_state";
import {
coversChainage,
NO_SELECTION,
@@ -26,7 +25,12 @@ import {
import { paintMapPick } from "../A00_Common/design/design_pick_profile";
import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape";
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
import { fetchLatestRoute, type RouteLatestResponse } from "../B05_Profile/B05_Profile_Api_Fetch";
import {
fetchLatestRoute,
readRouteLatestCache,
writeRouteLatestCache,
} from "../B05_Profile/B05_Profile_Api_Fetch";
import { applyPageSettings } from "../B05_Profile/B05_Profile_UI_Page_Latest";
import type {
CrossSection,
EarthworkConversion,
@@ -79,24 +83,20 @@ export interface SectionProfile {
/**
* 배수유역도에 노선 평면선을 싣는다 — 종단 보기가 두고 간 최신 노선 캐시를 먼저 쓰고,
* 없으면 한 번 읽는다(읽기만 · 계산 없음).
* 없으면 한 번 읽어 캐시에 둔다(페이지 설정 한 칸이 읽음 · 463).
*/
function loadRoute(projectId: string, profile: DesignProfile): void {
// 최소 흙덮기 강제 여부 — 종단 보기를 거치지 않고 바로 들어왔으면 저장된 노선 설정값.
const enforceKnown = readState<boolean>("profile-enforce-min-cover", projectId) !== null;
const apply = (latest: RouteLatestResponse): void => {
profile.setRoutePolyline(latest.route_points ?? []);
if (!enforceKnown) {
profile.setEnforceMinCover(latest.route_params?.enforce_pipe_clearance === true);
}
};
const cached = readState<RouteLatestResponse>("latest", projectId);
const cached = readRouteLatestCache(projectId);
if (cached?.route_points?.length) {
apply(cached);
profile.setRoutePolyline(cached.route_points);
return;
}
void fetchLatestRoute(projectId)
.then(apply)
.then((latest) => {
writeRouteLatestCache(projectId, latest);
profile.setRoutePolyline(latest.route_points ?? []);
applyPageSettings(profile, projectId, latest); // 그린 뒤 왔으면 지금 얹음
})
.catch(() => {
/* 노선을 못 읽으면 판이 「노선 필요」 안내를 그대로 보인다. */
});
@@ -242,6 +242,7 @@ export function createSectionProfile(
input.stationInterval,
Number.isFinite(routeId) ? routeId : undefined,
);
applyPageSettings(profile, id); // 페이지 설정 한 칸 — B05 를 안 열었어도 같은 기준(463)
if (input.conversion) {
profile.setEarthworkContext({
conversion: input.conversion,
@@ -0,0 +1,218 @@
/* 463 B05 · B06 한 페이지 상세 알림(요구 271-5 · 분석 4장 (C)) — 진짜 상세 가게(`B06_Section_Section_Store`) ·
* 진짜 [측점 만들기] 줄(`B06_Section_UI_Missing_Stations`) · 진짜 페이지 노선 칸. 서버 · 그림 · DOM 은 가짜.
* R1 — B05 가 쥔 상세(구독) · B06 넓힘 재생성 = 강제 다시 받기 → 듣는 쪽 한 번 · 새 객체
* R2 — [측점 만들기] 뒤 = 캐시 비우고 강제로 받음 → 듣는 쪽 한 번 · 다시 그림(onCreated)이 새 상세를 읽음
* node helper_271_detail_notify.cjs (짝 = `test_271_detail_notify.py`)
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..");
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
const CATALOG = require(path.join(__dirname, "helper_322_catalog_module.cjs"));
const P = "P463";
function memoryStorage() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
key: (i) => [...map.keys()][i] ?? null,
get length() {
return map.size;
},
};
}
globalThis.window = {
sessionStorage: memoryStorage(),
localStorage: memoryStorage(),
setTimeout,
clearTimeout,
};
/** 가짜 DOM — [측점 만들기] 줄이 쓰는 만큼(요소 · 단추 누름). */
function fakeElement() {
const handlers = {};
return {
className: "",
textContent: "",
disabled: false,
isConnected: true,
children: [],
append(...nodes) {
this.children.push(...nodes);
},
prepend(node) {
this.children.unshift(node);
},
remove() {},
querySelector: () => null,
addEventListener: (name, fn) => (handlers[name] = fn),
fire: (name) => handlers[name](),
};
}
globalThis.document = { createElement: () => fakeElement() };
const buttons = [];
const anyModule = (extra = {}) =>
new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) });
/** 가짜 서버 — 노선 3 상세 · 받을 때마다 새 객체 · 측점 수는 `server.stations`. */
const server = { stations: [0, 20, 40], fetches: 0, created: 0 };
const detailOf = () => ({
longitudinal: { stations: [], design_profiles: [] },
cross_sections: server.stations.map((chainage_m) => ({
station_id: `s${chainage_m}`,
chainage_m,
samples: [],
design: {},
})),
});
globalThis.fetch = async (url, init) => {
const method = init?.method ?? "GET";
if (method === "POST") {
server.stations = [0, 20, 33.5, 40]; // 서버가 관 자리 측점을 세움
server.created += 1;
return { ok: true, status: 200, json: async () => ({ created: 1 }) };
}
const missing = server.created ? [] : [{ chainage_m: 33.5, label: "배수관" }];
return { ok: true, status: 200, json: async () => ({ missing, can_create: true }) };
};
const STUBS = {
"design_pipeline_page.ts": {
reconcilePagePipeline: async () => 0,
pipelineOwnsCross: () => false,
pagePipeline: () => null,
pipelineCross: () => undefined,
pipelineDetail: () => null,
},
"design_pipeline_cross.ts": { forceCross: () => undefined },
"B06_Section_Section_Drafts.ts": {
forgetDraftStations: () => undefined,
supersedeDraftStations: () => undefined,
withDraftShapes: async () => undefined,
withDraftStations: async () => undefined,
},
"B05_Profile_Api_PlusStations.ts": {
loadSavedPlusStations: async () => undefined,
plusStationsOf: () => [],
},
"B05_Profile_Api_Pipes_Draft.ts": { readPendingPipes: () => null },
"design_page_data.ts": { pagePipes: async () => ({ pipe_points: [] }) },
"B06_Section_Api_Fetch.ts": anyModule({
fetchSectionDetail: async () => {
server.fetches += 1;
return detailOf();
},
}),
"B06_Section_Api_Culvert_Options.ts": anyModule({ STRUCTURE_ADJUST_KEYS: [] }),
"B06_Section_Swap_Bus.ts": anyModule(),
"B06_Section_Station_Keys.ts": anyModule({ KEYED_STATES: [] }),
"B06_Section_UI_Page_Persist.ts": anyModule(),
"B06_Section_UI_Standard_Panel.ts": anyModule({ readRockBoundaryDefault: () => null }),
"B06_Section_Cross_Design_Session.ts": anyModule(),
"common_util_cross_design.ts": anyModule(),
"B06_Section_Structure_Layouts.ts": anyModule({ computeStoredLayouts: () => null }),
"B06_Section_UI_Cross_Culvert_Wire.ts": anyModule(),
"B06_Section_UI_Cross_Ford_Geom.ts": anyModule(),
"B06_Section_UI_Cross_Box_Geom.ts": anyModule(),
"B06_Section_UI_Cross_View.ts": anyModule(),
"B06_Section_UI_Section_Common.ts": anyModule({ designElevationAt: () => undefined }),
"B06_Section_Display_Width.ts": anyModule({ displayHalfWidth: () => 10 }),
"B06_Section_UI_Style_Cross_Controls.css": {},
"B06_Section_UI_Cross_Culvert_Basin.ts": anyModule(),
"B06_Section_UI_Cross_Culvert_Const.ts": anyModule(),
"B06_Section_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"B05_Profile_UI_IrregularStations.ts": anyModule({ PIPE_DISPLAY_NAME: {} }),
"config_frontend.ts": anyModule({ PIPE_TYPES: [], API_BASE_URL: "/api" }),
"ui_template_elements.ts": anyModule({
createButton: () => {
const button = fakeElement();
buttons.push(button);
return button;
},
showToast: () => undefined,
}),
"ui_template_hover_info.ts": anyModule(),
"design_store.ts": { useDesignStore: () => ({ subscribe: () => () => undefined }) },
"design_save.ts": { savingDesign: () => false },
};
const cache = new Map();
const resolve = (base) =>
[base, `${base}.ts`, path.join(base, "index.ts")].find(
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
) ?? `${base}.ts`;
function loadTs(full) {
const stub = STUBS[path.basename(full)];
if (stub) return stub;
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
esModuleInterop: true,
},
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const near = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
for (const [prefix, dir] of Object.entries(alias))
if (request.startsWith(prefix))
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
return loadTs(resolve(path.join(path.dirname(full), request)));
};
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
}
const store = loadTs(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
const missing = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Missing_Stations.ts"));
const route = loadTs(path.join(ROOT, "A00_Common", "design", "design_page_route.ts"));
const tick = () => new Promise((next) => setTimeout(next, 0));
(async () => {
const out = {};
route.setPageRouteId(P, 3);
// R1 — B05 이 쥔 상세 · B06 넓힘 재생성 = 강제 다시 받기
const held = await store.loadSectionDetail(P, 3);
const heard = [];
const stop = store.subscribeSectionDetail(P, 3, (next) => heard.push(next));
const fresh = await store.loadSectionDetail(P, 3, { force: true });
out.r1 = {
newObject: fresh !== held,
listenerCalls: heard.length,
listenerGotFresh: heard[0] === fresh,
cacheIsFresh: store.peekSectionDetail(P, 3) === fresh,
};
// R2 — [측점 만들기] 뒤 = 캐시 비우고 강제로 받음 · 듣는 쪽 한 번 · 다시 그림이 새 상세
heard.length = 0;
const before = server.fetches;
const redrawn = [];
const host = fakeElement();
await missing.mountMissingStationNotice(host, P, async () => {
const seen = await store.loadSectionDetail(P, 3); // 다시 그림 자리(횡단 보기 refreshDetailForStructures)
redrawn.push(seen.cross_sections.map((section) => section.chainage_m));
});
const button = buttons.at(-1);
await button.fire("click");
await tick();
out.r2 = {
posted: server.created,
fetchesAfter: server.fetches - before,
listenerCalls: heard.length,
listenerStations: heard[0]?.cross_sections.map((section) => section.chainage_m) ?? null,
redrawn,
};
stop();
console.log(JSON.stringify(out));
})().catch((error) => {
console.error(error);
process.exit(1);
});
@@ -0,0 +1,118 @@
/* 463 B05 · B06 한 페이지 페이지 설정 한 칸(요구 271-5 · 분석 4장 (B)) — 진짜 `applyPageSettings` · 페이지 설정 초안 ·
* 기준표 · 세션 칸. 종단도는 가짜(받은 것 기록). 노선 캐시 · 초안 갈래마다 종단도가 받는 기준을 JSON 한 줄로.
* node helper_271_profile_settings.cjs (짝 = `test_271_profile_settings.py`)
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..");
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
function memoryStorage() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
key: (i) => [...map.keys()][i] ?? null,
get length() {
return map.size;
},
};
}
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
const REAL = new Set(
[
"A00_Common/b_page_state.ts",
"B05_Profile/B05_Profile_Api_Fetch.ts",
"B05_Profile/B05_Profile_Api_PageSettings.ts",
"B05_Profile/B05_Profile_UI_Page_Latest.ts",
"B05_Profile/B05_Profile_UI_Panel_Fields.ts",
].map((rel) => path.join(ROOT, rel)),
);
const anything = () =>
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
const cache = new Map();
function load(file) {
if (cache.has(file)) return cache.get(file);
const box = { exports: {} };
cache.set(file, box.exports);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const req = (request) => {
if (request === "@ui/ui_template_locale") return { ui_locales: {}, currentLanguageIndex: 0 };
if (!request.startsWith(".")) return anything();
const target = path.resolve(path.dirname(file), request);
const full = fs.existsSync(`${target}.ts`) ? `${target}.ts` : target;
return REAL.has(full) ? load(full) : anything();
};
new Function("exports", "module", "require", js)(box.exports, box, req);
return box.exports;
}
const latestApi = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Page_Latest.ts"));
const settings = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"));
const state = load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const P = "P463";
/** 가짜 종단도 — 받은 기준을 적음. */
function apply(latest) {
const got = {};
latestApi.applyPageSettings(
{
setGradeLimit: (pct) => (got.grade = pct),
setEnforceMinCover: (on) => (got.enforce = on),
setStationDisplay: (next) => (got.display = next),
},
P,
latest,
);
return got;
}
const latestOf = (params) => ({ project_id: P, surface_params: {}, route_params: params });
const out = {};
window.sessionStorage = memoryStorage();
out.nothing = apply(undefined); // 노선 캐시 · 초안 없음 = 종단도 그대로
state.writeState(
"latest",
latestOf({
options: { grade_class: "trunk", design_speed_kph: 30, terrain_type: "normal" },
max_grade_pct: null,
enforce_pipe_clearance: true,
station_display: { station: 3, cumulative: 60 },
}),
P,
);
out.saved = apply(undefined); // 캐시 읽음
settings.writePageSettings(P, {
design_speed_kph: 30,
terrain_type: "normal",
max_grade_pct: 12,
min_vertical_radius_m: null,
min_tangent_length_m: null,
enforce_pipe_clearance: false,
station_display: { station: 5, cumulative: 100 },
});
out.draft = apply(undefined); // 초안이 이김
out.work = apply(
latestOf({ options: { grade_class: "work", design_speed_kph: 30, terrain_type: "normal" } }),
); // 받은 노선 · 초안 그대로 이김(작업임도 30 → 20 · 넣은 12 그대로)
state.clearState("pagesettings", P);
out.workSaved = apply(
latestOf({ options: { grade_class: "work", design_speed_kph: 30, terrain_type: "normal" } }),
);
out.defaults = apply(latestOf({})); // 빈 설정 = 좌측 패널 기본(20 · 특수 · 강제 해제 · 0)
state.clearState("latest", P);
settings.writePageSettings(P, {
design_speed_kph: 40,
terrain_type: "special",
max_grade_pct: null,
min_vertical_radius_m: null,
min_tangent_length_m: null,
enforce_pipe_clearance: true,
station_display: { station: 0, cumulative: 0 },
});
out.draftOnly = apply(undefined);
console.log(JSON.stringify(out));
+142
View File
@@ -0,0 +1,142 @@
/* 463 B05 · B06 한 페이지 [저장] 같은 본문(요구 271-5 · 분석 4장 (D)) — 진짜 저장 한 줄기(`design_save`) ·
* 종단 보기 몫(`B05_Profile_UI_Page_Actions.profileSaveParts`) · B05 · B04 서버 창구 · 세션 칸. fetch 는 가짜(요청 줄 기록) ·
* 그 밖 차례 창구 · 화면은 가짜. 종단 보기를 안 연 [저장](기본 창구) · 연 뒤 [저장]의 요청 줄을 견줌.
* node helper_271_save_same.cjs (짝 = `test_271_save_same.py`)
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..");
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
function memoryStorage() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
key: (i) => [...map.keys()][i] ?? null,
get length() {
return map.size;
},
};
}
globalThis.window = {
sessionStorage: memoryStorage(),
localStorage: memoryStorage(),
setTimeout,
clearTimeout,
};
/** 진짜로 읽는 파일 — 그 밖은 가짜(부르면 undefined). */
const REAL = new Set(
[
"A00_Common/design/design_save.ts",
"A00_Common/design/design_page_route.ts",
"A00_Common/b_page_state.ts",
"B05_Profile/B05_Profile_Api_Fetch.ts",
"B05_Profile/B05_Profile_UI_Page_Actions.ts",
"B04_PreProcess/B04_PreProcess_Api_Fetch.ts",
].map((rel) => path.join(ROOT, rel)),
);
const CONFIG = { API_BASE_URL: "/api", API_TIMEOUT_MS: 1000, API_ANALYSIS_TIMEOUT_MS: 1000 };
const anything = () =>
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
const cache = new Map();
function load(file) {
if (cache.has(file)) return cache.get(file);
const box = { exports: {} };
cache.set(file, box.exports);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const req = (request) => {
if (request === "@config/config_frontend") return CONFIG;
if (!request.startsWith(".")) return anything();
const target = path.resolve(path.dirname(file), request);
const full = fs.existsSync(`${target}.ts`) ? `${target}.ts` : target;
return REAL.has(full) ? load(full) : anything();
};
new Function("exports", "module", "require", js)(box.exports, box, req);
return box.exports;
}
const save = load(path.join(ROOT, "A00_Common", "design", "design_save.ts"));
const actions = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Page_Actions.ts"));
const state = load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const route = load(path.join(ROOT, "A00_Common", "design", "design_page_route.ts"));
const P = "P463";
const LATEST = {
project_id: P,
route: { id: 3 },
route_points: [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
],
surface_params: { source_filter: "csf", method: "tin", smooth: true, contour_interval_m: 1 },
route_params: {},
};
let lines = [];
globalThis.fetch = async (url, init) => {
const method = init?.method ?? "GET";
lines.push(`${method} ${url}${init?.body ? ` ${init.body}` : ""}`);
let reply = {};
if (url.includes("/surface/confirmed")) reply = { model_id: 7 };
else if (url.includes("/route/latest")) reply = LATEST;
else if (url.includes("/route/confirm")) reply = { failed_parts: [] };
return { ok: true, status: 200, json: async () => reply };
};
/** 세션을 같은 출발점으로 — 노선 캐시(있으면) · 상단측 한 곳. */
function seed(withLatest) {
window.sessionStorage = memoryStorage();
if (withLatest) state.writeState("latest", LATEST, P);
state.writeState("uphill", { "40.000": "right" }, P);
}
async function run(steps) {
lines = [];
const failed = [];
const ok = await save.saveDesign({
projectId: P,
routeId: () => 3,
steps,
cross: async () => undefined,
failed: (error) => failed.push(String(error?.message ?? error)),
});
return { ok, failed, lines, uphillAfter: state.readState("uphill", P) };
}
(async () => {
route.setPageRouteId(P, 3);
const out = {};
// ① 종단 보기를 안 연 [저장] = 기본 창구
seed(true);
out.closed = await run(undefined);
// ② 종단 보기를 연 뒤 [저장] = 종단 보기 몫(상단측 차례) — 구조물 몫은 PUT 없음(가짜)
seed(true);
const memory = new Map([["40.000", "right"]]);
const parts = actions.profileSaveParts({
projectId: P,
latest: () => LATEST,
confirmedSurface: () => ({ model_id: 7 }),
routeReady: () => true,
bridge: () => ({ saveStructuresIfDirty: async () => undefined }),
uphillOverrides: memory,
});
out.open = await run({ uphill: parts.steps.uphill });
out.openMemoryAfter = memory.size;
// ③ 새로고침해 B06 로 바로 — 노선 캐시 없음 = 서버에서 한 번 읽고 같은 본문
seed(false);
out.noCache = await run(undefined);
console.log(JSON.stringify(out));
})().catch((error) => {
console.error(error);
process.exit(1);
});
@@ -72,6 +72,9 @@ function loadTs(file) {
STUBS["@util/common_util_station_key"] = loadTs(
path.join(ROOT, "common_util", "common_util_station_key.ts"),
);
// 상단측 차례 본문(463 · 지표 조건 · 확정 지표면 GET · 확정 POST) — 이 시험은 횡단 뒤처리만 봄
const B05_FETCH = path.join(ROOT, "B05_Profile", "B05_Profile_Api_Fetch.ts");
STUBS[B05_FETCH] = { ...loadTs(B05_FETCH), flushRouteParts: async () => undefined };
const { saveDesign, savingDesign } = loadTs(
path.join(ROOT, "A00_Common", "design", "design_save.ts"),
);
@@ -77,6 +77,7 @@ STUBS["B04_PreProcess_Api_Fetch"] = {
STUBS["B05_Profile_Api_Structures"] = { flushPendingStructures: async () => undefined };
STUBS["B05_Profile_Api_Fetch"] = {
flushUphillOverrides: async () => undefined,
flushRouteParts: async () => undefined, // 상단측 차례(463)
saveProfileAlignment: async () => ({}),
};
STUBS["B05_Profile_UI_Profile_Edit"] = {
@@ -76,6 +76,7 @@ STUBS["B05_Profile_Api_Structures"] = {
STUBS["B05_Profile_Api_PlusStations"] = { flushPlusStations: async () => undefined };
STUBS["B05_Profile_Api_Fetch"] = {
flushUphillOverrides: async () => undefined,
flushRouteParts: async () => undefined, // 상단측 차례(463)
saveProfileAlignment: async (_projectId, routeId, draft) => {
calls.push(["ALIGN", routeId, draft]);
return {};
@@ -74,6 +74,7 @@ const STUBS = {
},
"../../B05_Profile/B05_Profile_Api_Fetch": {
flushUphillOverrides: () => record("uphill")(),
flushRouteParts: () => record("uphill")(), // 상단측 차례 = 종단 보기 몫과 같은 본문(463)
saveProfileAlignment: async (_p, routeId, edits) =>
calls.push(`alignment:${routeId}:${JSON.stringify(edits.station_offsets)}`),
},
@@ -30,7 +30,11 @@ def test_save_path_and_merge_kept():
# 저장 길(세션 → 저장 몸통)과 B06 이 부르는 종단 병합 함수는 그대로
page = _read("A00_Common", "design", "design_view_profile.ts")
assert "persistUphillOverrides" in page
assert "uphill_overrides" in _read("B05_Profile", "B05_Profile_UI_Page_Actions.ts")
# 상단측 본문 = 종단 보기 몫 · 기본 창구 한 함수(463)
assert "flushRouteParts(projectId)" in _read("B05_Profile", "B05_Profile_UI_Page_Actions.ts")
assert "uphill_overrides: uphillOverridesOf(projectId)," in _read(
"B05_Profile", "B05_Profile_Api_Fetch.ts"
)
assert "def _merge_uphill_overrides_into_longitudinal" in _read(
"B05_Profile", "B05_Profile_Router_Confirm.py"
)
@@ -0,0 +1,55 @@
"""463 B05 · B06 한 페이지 — 상세 갈아끼움 알림(요구 271-5 · 분석 4장 (C)).
① R1 — 강제 다시 받기(넓힘 재생성) = 듣는 쪽(B05 종단 보기) 한 번 · 새 상세
· 옛 코드 = 0 번(B05 옛 객체 쥠)
② R2 — [측점 만들기] 뒤 = 캐시 비우고 강제로 받음 · 듣는 쪽 한 번 · 다시 그림이 새 측점
· 옛 코드 = 옛 캐시
TS 셈은 `helper_271_detail_notify.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_271_detail_notify.py -q
"""
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_271_detail_notify.cjs")
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def test_R1_강제_다시_받기는_듣는_쪽_한_번(got):
assert got["r1"] == {
"newObject": True,
"listenerCalls": 1,
"listenerGotFresh": True,
"cacheIsFresh": True,
}
def test_R2_측점_만들기_뒤_새_상세(got):
r2 = got["r2"]
assert r2["posted"] == 1
assert r2["fetchesAfter"] == 1 # 강제로 한 번 · 다시 그림은 그 캐시
assert r2["listenerCalls"] == 1
assert r2["listenerStations"] == [0, 20, 33.5, 40]
assert r2["redrawn"] == [[0, 20, 33.5, 40]]
@@ -0,0 +1,91 @@
"""463 B05 · B06 한 페이지 — 페이지 설정 한 칸(요구 271-5 · 분석 4장 (B)).
① 종단도 기준(기울기 상한 · 최소고 강제 · 시작 측점) = 세션 초안(448) ?? 저장 노선 설정
· 빈 칸 = 좌측 패널 기본(설계속도 20 · 특수지형 · 강제 해제 · 0) · 작업임도는 20 만
· 둘 다 없으면 종단도 그대로
② 이음 — 횡단 보기 · 종단 보기 둘 다 종단도를 그린 뒤 같은 함수
· 옛 코드 = B05 좌측 패널만 얹어 B06 새로고침에서 라벨 오프셋 · 기울기 빨강 · 강제가
B05 열기 전까지 다름 · B06 이 받은 노선은 캐시에 둠
③ 좌측 패널 상한 = 같은 기준 함수
TS 셈은 `helper_271_profile_settings.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_271_profile_settings.py -q
"""
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_271_profile_settings.cjs")
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
def src(*parts: str) -> str:
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def test_초안이_저장_설정을_이김(got):
assert got["nothing"] == {}
assert got["saved"] == {
"grade": 8,
"enforce": True,
"display": {"station": 3, "cumulative": 60},
}
assert got["draft"] == {
"grade": 12,
"enforce": False,
"display": {"station": 5, "cumulative": 100},
}
assert got["work"] == got["draft"]
assert got["draftOnly"] == {
"grade": 10,
"enforce": True,
"display": {"station": 0, "cumulative": 0},
}
def test_빈_칸은_좌측_패널_기본(got):
zero = {"station": 0, "cumulative": 0}
assert got["workSaved"] == {"grade": 9, "enforce": False, "display": zero} # 작업임도 30 → 20
assert got["defaults"] == {"grade": 14, "enforce": False, "display": zero} # 20 · 특수
def test_두_보기가_같은_함수로_얹음():
cross = src("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
render = cross[cross.index(" profile.render(") :]
assert render.index("applyPageSettings(profile, id);") < render.index("lane.paint();")
loader = cross[cross.index("function loadRoute(") :]
loader = loader[: loader.index("\n}\n")]
assert "writeRouteLatestCache(projectId, latest);" in loader
assert "applyPageSettings(profile, projectId, latest);" in loader
assert "enforceKnown" not in cross
sections = src("A00_Common", "design", "design_view_profile_sections.ts")
body = sections[sections.index(" function renderSections(") :]
body = body[: body.index("\n }\n")]
assert "applyPageSettings(profilePanel, projectId, state.latest ?? undefined);" in body
assert "panel.gradeLimitPct()" not in body
def test_좌측_패널_상한도_같은_기준():
part = src("B05_Profile", "B05_Profile_UI_Panel_Settings.ts")
assert "return gradeLimitOf(currentSpeed(), terrain, parseOptional(maxGradePct));" in part
+84
View File
@@ -0,0 +1,84 @@
"""463 B05 · B06 한 페이지 — [저장] 같은 본문(요구 271-5 · 분석 4장 (D)).
① 종단 보기를 안 연 [저장](기본 창구) · 연 뒤 [저장](종단 보기 몫) = 요청 줄 같음
— 옛 코드 = 기본 창구가 상단측만 보내 서버 확정 요청에 지표 조건 · 확정 지표면 id 가 빠짐
(비정규 측점 횡단 맞춤 안 돎)
② 노선 캐시 없음(새로고침해 B06 로 바로) = 서버에서 한 번 읽고 같은 본문
③ 서버 — 그 본문이면 확정이 비정규 측점 횡단을 맞춤(`can_regenerate`)
TS 셈은 `helper_271_save_same.cjs`.
./venv/Scripts/python.exe -m pytest resources/tester/test_271_save_same.py -q
"""
import json
import subprocess
from pathlib import Path
import pytest
from B05_Profile.B05_Profile_Schema import RouteConfirmRequest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_271_save_same.cjs")
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run( # noqa: S603 — 고정 실행 파일
["node", str(HELPER)],
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
def _confirm_body(lines: list[str]) -> dict:
posts = [line for line in lines if line.startswith("POST ") and "/route/confirm" in line]
assert len(posts) == 1, lines
return json.loads(posts[0].split(" ", 2)[2])
def test_종단_보기를_열든_안_열든_같은_요청(got):
assert got["closed"]["ok"] and got["open"]["ok"]
assert got["closed"]["failed"] == [] and got["open"]["failed"] == []
assert got["closed"]["lines"] == got["open"]["lines"]
body = _confirm_body(got["closed"]["lines"])
assert body == {
"filter_key": "csf",
"method": "tin",
"smooth": True,
"surface_model_id": 7,
"irregular_stations": [],
"uphill_overrides": [{"chainage_m": 40, "side": "right"}],
}
# 상단측 세션 · 종단 보기 메모리 둘 다 비움(다음 [저장]이 옛 값을 다시 안 보냄)
assert got["closed"]["uphillAfter"] is None and got["open"]["uphillAfter"] is None
assert got["openMemoryAfter"] == 0
def test_노선_캐시_없어도_같은_본문(got):
lines = got["noCache"]["lines"]
assert lines[0] == "GET /api/projects/P463/route/latest"
assert _confirm_body(lines) == _confirm_body(got["closed"]["lines"])
def test_서버는_그_본문이면_비정규_측점_횡단을_맞춤(got):
body = _confirm_body(got["closed"]["lines"])
assert RouteConfirmRequest(**body).can_regenerate()
assert not RouteConfirmRequest(uphill_overrides=body["uphill_overrides"]).can_regenerate()
def test_종단_보기_몫은_기본_창구_함수를_부름():
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text("utf-8")
assert "await flushRouteParts(projectId);" in actions
assert "saveRouteParts(" not in actions
assert "return () => flushRouteParts(projectId);" in save
+4 -2
View File
@@ -72,8 +72,10 @@ def test_확정_하위_실패면_던짐(out):
def test_종단_보기_상단측_차례도_같은_창구():
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text("utf-8")
assert "await saveRouteParts(projectId, {" in actions
assert "loadUphillOverrides(projectId)" in actions # 세션에서 읽음(메모리 옛 값 안 보냄)
# 기본 창구와 같은 함수(463) — 상단측은 세션에서 읽음(메모리 옛 값 안 보냄)
assert "await flushRouteParts(projectId);" in actions
fetch = (ROOT / "B05_Profile" / "B05_Profile_Api_Fetch.ts").read_text("utf-8")
assert "uphill_overrides: uphillOverridesOf(projectId)," in fetch
assert "confirmRoute(" not in actions
+1 -1
View File
@@ -70,7 +70,7 @@ def test_저장이_창구를_다섯_다_거친다():
save = (ROOT / "A00_Common" / "design" / "design_save.ts").read_text(encoding="utf-8")
for token in (
"flushCulvertOptions(",
"flushUphillOverrides(",
"flushRouteParts(", # 상단측 차례 = 종단 보기 몫과 같은 본문(463)
"flushPendingPipes(",
"flushPendingStructures(",
):