Merge remote-tracking branch 'origin/CODEX' into CODEX
This commit is contained in:
@@ -427,6 +427,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
|
||||
function draw(): void {
|
||||
renderProfile({
|
||||
onStaleDesigns: crossPreview.scheduleIfStale, // 그린 뒤 생긴 낡음도 다시 셈(317-1)
|
||||
body,
|
||||
callbacks,
|
||||
massHaul,
|
||||
@@ -535,7 +536,6 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
// 저장된 횡단 설계가 옛 계획고로 굳어 있으면 진입 즉시 한 번 맞춘다 —
|
||||
// 지금까지는 B06에 들어가야만 고쳐져, B05의 횡단 기준 유토곡선과 3D
|
||||
// 예상형상이 옛 설계선을 그대로 썼다(2026-08-23 사용자 보고 · 실측 확인).
|
||||
//
|
||||
// 비교 대상은 **편집이 반영된 계획선**이다. 저장분끼리 견주면, 세션에 미저장 편집이
|
||||
// 남은 채로 새로고침했을 때 「어긋남 없음」으로 나와 재계산이 예약되지 않고, 그리기
|
||||
// 쪽은 편집분 기준으로 어긋났다고 보아 유토곡선이 영영 빈 화면이 됐다
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
writeCachedBytes,
|
||||
} from "../A00_Common/b_asset_cache";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { refreshStaleDesigns } from "../B06_Section/B06_Section_Cross_Refresh";
|
||||
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
|
||||
import { buildCorridor, type CorridorBuildResult } from "./B05_Profile_UI_Corridor_Build";
|
||||
import {
|
||||
@@ -247,7 +248,7 @@ function withSections(
|
||||
|
||||
/** Page 훅 — 현재 종횡단 정본 그대로 코리도를 확보해 뷰어에 반영(실패 시 제거).
|
||||
* 끝나는 시점을 [3D 업데이트] 버튼이 기다려야 해 Promise를 돌려준다(2026-09-01). */
|
||||
export function refreshCorridor(
|
||||
export async function refreshCorridor(
|
||||
viewer: { setCorridor: (build: CorridorBuildResult | null) => void },
|
||||
projectId: string,
|
||||
routeId: number | undefined,
|
||||
@@ -256,7 +257,9 @@ export function refreshCorridor(
|
||||
designSamples?: ProfileSamples,
|
||||
rebuild = true,
|
||||
): Promise<void> {
|
||||
if (!routeId) return Promise.resolve();
|
||||
if (!routeId) return;
|
||||
// [3D 업데이트] — 낡은 횡단 설계(계획고 · 구조물 자리 값)를 먼저 다시 셈한 뒤 그 설계로 지음(317-1)
|
||||
if (rebuild) await refreshStaleDesigns({ projectId, routeId, detail }).catch(() => []);
|
||||
return ensureCorridor(projectId, routeId, detail, routePoints, designSamples, rebuild)
|
||||
.then((build) => viewer.setCorridor(withSections(build, detail)))
|
||||
.catch(() => viewer.setCorridor(null));
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface CrossPreviewContext {
|
||||
export interface CrossPreview {
|
||||
/** 편집이 있을 때마다 부른다 — 마지막 값만 서버로 나간다. */
|
||||
schedule: () => void;
|
||||
/** 그리기가 잰 낡음 지문 — 새 지문일 때만 `schedule`(같은 낡음 되풀이 · 서버 폴백 반복 막음 · 317-1). */
|
||||
scheduleIfStale: (key: string) => void;
|
||||
/** 패널을 걷을 때 대기 중인 요청 타이머를 끈다. */
|
||||
dispose: () => void;
|
||||
}
|
||||
@@ -34,8 +36,13 @@ export interface CrossPreview {
|
||||
export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
|
||||
let timer = 0;
|
||||
let seq = 0;
|
||||
let lastStale = "";
|
||||
|
||||
return {
|
||||
const preview: CrossPreview = {
|
||||
scheduleIfStale(key) {
|
||||
if (key && key !== lastStale) preview.schedule();
|
||||
lastStale = key;
|
||||
},
|
||||
schedule() {
|
||||
if (!ctx.detail() || ctx.routeId() === null) return;
|
||||
window.clearTimeout(timer);
|
||||
@@ -68,4 +75,5 @@ export function createCrossPreview(ctx: CrossPreviewContext): CrossPreview {
|
||||
window.clearTimeout(timer);
|
||||
},
|
||||
};
|
||||
return preview;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
longitudinalMinimumWidth,
|
||||
} from "../B06_Section/B06_Section_UI_Longitudinal";
|
||||
import {
|
||||
hasStaleDesigns,
|
||||
LONG_PAD,
|
||||
staleKey,
|
||||
windowElevationRange,
|
||||
} from "../B06_Section/B06_Section_UI_Section_Common";
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
@@ -59,6 +59,8 @@ import type { RouteProfilePanelCallbacks } from "./B05_Profile_UI_Profile_Panel"
|
||||
/** 본체가 들고 있는 상태를 읽고, 그리는 도중 바뀌는 것만 되돌려 주는 통로. */
|
||||
export interface ProfileRenderContext {
|
||||
body: HTMLElement;
|
||||
/** 그릴 때 잰 낡은 횡단 지문("" = 모두 맞음) — 패널이 새 지문일 때만 다시 셈(317-1). */
|
||||
onStaleDesigns?: (key: string) => void;
|
||||
callbacks: RouteProfilePanelCallbacks | undefined;
|
||||
massHaul: { draw: (params: RouteMassHaulDrawParams) => void };
|
||||
tableOverlay: {
|
||||
@@ -223,6 +225,12 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
const designProfiles = alignment
|
||||
? [toDesignProfile(alignment, longitudinal.design_profiles?.[0])]
|
||||
: (longitudinal.design_profiles ?? []);
|
||||
// 횡단이 지금 계획선 · 얹힌 구조물과 어긋나면(낡음) 패널이 다시 셈을 예약 — 같은 낡음은 한 번만(317-1)
|
||||
const stale = staleKey({
|
||||
longitudinal: { design_profiles: designProfiles },
|
||||
cross_sections: detail.cross_sections,
|
||||
});
|
||||
ctx.onStaleDesigns?.(stale);
|
||||
// 그래프에는 비정규 측점을 일반 측점처럼(세로선+라벨) 섞어 넣는다.
|
||||
const graphData = normalizedLongitudinal(longitudinal);
|
||||
// 측점 한 벌(272) — 서버의 구조물 측점은 사이드바 목록(관을 옮기면 따라옴)이 갈음하고, 같은 m
|
||||
@@ -416,10 +424,7 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
// 비교 대상은 **편집이 반영된 계획선**(`designProfiles`)이다. 저장분
|
||||
// (`detail.longitudinal.design_profiles`)과 견주면 편집 중에는 영원히 어긋난 것으로
|
||||
// 나와 곡선이 계속 빈 화면이 된다(2026-09-03 사용자 보고).
|
||||
pendingRecalc: hasStaleDesigns({
|
||||
longitudinal: { design_profiles: designProfiles },
|
||||
cross_sections: detail.cross_sections,
|
||||
}),
|
||||
pendingRecalc: stale !== "",
|
||||
axis: {
|
||||
maxChainageM: maxChainageOf(longitudinal),
|
||||
// 종단 그래프의 `chainageMapper`와 정확히 같은 매핑이 되도록 반 칸 들여쓰기를
|
||||
|
||||
@@ -48,6 +48,8 @@ import {
|
||||
} from "../B05_Profile/B05_Profile_UI_Profile_Alignment";
|
||||
import { readAlignment, toDesignProfile } from "../B05_Profile/B05_Profile_UI_Profile_Data";
|
||||
import { readState } from "../A00_Common/b_page_state";
|
||||
import { readAlignmentDraft } from "../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { hasStaleDesigns } from "./B06_Section_UI_Section_Common";
|
||||
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
|
||||
|
||||
/** 계획선 편집 델타 — B05 `AlignmentEdits`와 저장분 `profile_alignment.edits`가 같은 모양이다. */
|
||||
@@ -151,6 +153,32 @@ function preserveUserFields(
|
||||
* 돌려주는 값은 실제로 바뀐 측점의 누가거리 목록 — 호출한 쪽이 그 카드만 다시 그리면 된다.
|
||||
* 계획선 ▲ 한 번에 값이 바뀌는 측점은 몇 개뿐 — 전부 돌려주면 카드를 다 갈아 끼워 멈춤(54-22).
|
||||
*/
|
||||
/**
|
||||
* 낡은 설계만 다시 셈 — B05 [3D 업데이트]가 예상형상을 짓기 전에 부름(317-1).
|
||||
* 편집 = 세션 계획선 초안 → 없으면 저장분 · 초안이 있으면 늘 맞춤(B06 `reconcileStaleDesigns` 와 같은 규칙).
|
||||
*/
|
||||
export async function refreshStaleDesigns(
|
||||
input: Omit<CrossRefreshInput, "edits" | "shouldApply">,
|
||||
): Promise<number[]> {
|
||||
const draft = readAlignmentDraft(input.routeId);
|
||||
if (!draft && !hasStaleDesigns(input.detail)) return [];
|
||||
const saved = (
|
||||
input.detail.longitudinal.profile_alignment as
|
||||
| {
|
||||
edits?: {
|
||||
station_offsets?: Record<string, number>;
|
||||
curve_radii?: Record<string, number>;
|
||||
};
|
||||
}
|
||||
| undefined
|
||||
)?.edits;
|
||||
const edits = draft ?? {
|
||||
station_offsets: saved?.station_offsets ?? {},
|
||||
curve_radii: saved?.curve_radii ?? {},
|
||||
};
|
||||
return refreshCrossDesigns({ ...input, edits });
|
||||
}
|
||||
|
||||
export async function refreshCrossDesigns(input: CrossRefreshInput): Promise<number[]> {
|
||||
const sections = input.detail.cross_sections;
|
||||
const before = new Map(sections.map((section) => [section, JSON.stringify(section.design)]));
|
||||
|
||||
@@ -160,10 +160,33 @@ export async function withDraftStations(
|
||||
if (inputs) {
|
||||
const [pipes, structures, types] = inputs;
|
||||
const planted = plantedChainages(pipes, structures, types);
|
||||
// 구조물 측점에서 종류를 바꿈 = 「구조물 없는 상태」부터 새 종류로(요구 213) — 저장분 횡단은 빼 두고
|
||||
// 새 자리처럼 미리 뜬 횡단을 씀 · 저장분 종류로 되돌리면 빼 둔 횡단이 돌아옴
|
||||
const [oldPipes, oldStructures] = await savedOf(projectId).catch(
|
||||
(): [Pipes, StructureInstance[]] => [[], []],
|
||||
);
|
||||
const byType = new Map(types.map((type) => [type.type_id, type]));
|
||||
const kindsAt = (ps: Pipes, ss: StructureInstance[], at: number): string =>
|
||||
[
|
||||
...ps
|
||||
.filter((pipe) => pipeStationMarks(pipe).some((m) => near(m.chainage_m, at)))
|
||||
.map((pipe) => `p:${pipe.facility ?? "pipe"}`),
|
||||
...ss
|
||||
.filter((s) =>
|
||||
structureStationMarks(s, byType.get(s.type_id)).some((m) => near(m.chainage_m, at)),
|
||||
)
|
||||
.map((s) => `s:${s.type_id}`),
|
||||
]
|
||||
.sort()
|
||||
.join(",");
|
||||
const replaced = (section: CrossSection): boolean => {
|
||||
const was = kindsAt(oldPipes, oldStructures, section.chainage_m);
|
||||
return was !== "" && was !== kindsAt(pipes, structures, section.chainage_m);
|
||||
};
|
||||
const isStale = (section: CrossSection): boolean =>
|
||||
section.kind === "irregular" &&
|
||||
!section.grid &&
|
||||
planted.every((at) => !near(at, section.chainage_m));
|
||||
(planted.every((at) => !near(at, section.chainage_m)) || replaced(section));
|
||||
stale = all.filter(isStale);
|
||||
live = all.filter((section) => !isStale(section));
|
||||
const missing = planted.filter(
|
||||
|
||||
@@ -99,6 +99,36 @@ export function hasStaleDesigns(
|
||||
return detail.cross_sections.some((section) => isStaleSection(section, profiles, toleranceM));
|
||||
}
|
||||
|
||||
/**
|
||||
* 낡은 측점의 지문 — 같은 낡음에 다시 셈을 되풀이하지 않게 부르는 쪽이 견줌(317-1).
|
||||
* 다시 셈이 못 고친 측점(서버 폴백 등)은 지문이 그대로라 한 번만 묻는다 · 모두 맞으면 "".
|
||||
*/
|
||||
export function staleKey(
|
||||
detail: {
|
||||
longitudinal: { design_profiles?: DesignProfile[] };
|
||||
cross_sections: StaleCandidate[];
|
||||
},
|
||||
toleranceM = 1e-3,
|
||||
): string {
|
||||
const profiles = detail.longitudinal.design_profiles;
|
||||
return detail.cross_sections
|
||||
.filter((section) => isStaleSection(section, profiles, toleranceM))
|
||||
.map((s) =>
|
||||
[
|
||||
s.chainage_m,
|
||||
s.design?.design_elevation_m,
|
||||
s.design?.tilted,
|
||||
s.design?.paved,
|
||||
s.design?.surface_drop_m,
|
||||
Boolean(s.ford_pavement),
|
||||
Boolean(s.underdrain),
|
||||
s.pavement?.length ?? 0,
|
||||
s.ford?.overflow_depth_m ?? 0,
|
||||
].join(":"),
|
||||
)
|
||||
.join("|");
|
||||
}
|
||||
|
||||
interface StaleCandidate {
|
||||
chainage_m: number;
|
||||
// 측점에 얹힌 구조물 제원 — 저장분 설계값(포장 · 기울기 · 노면 하강)과 맞대 본다(314)
|
||||
|
||||
@@ -241,3 +241,4 @@
|
||||
210. (원문 · 10-13) 1. 물넘이포장과 맹암거 생성 한후 측구가 생성됨. 두항목다 측구없는 형상. 저장하면 정상 저장되는걸로 보임. 2. 물넘이포장과 맹암거 생성후 다른 구조물로 변경할 경우 캐시 비우기 안됨(예상). 나는 박스암거로 변경했는데 물넘이포장이 남아있었음. 확인 후 해당부분만 개선하지말고 동일한 문제가 있는지 확인하고 다른 정상인 구조물들이 어던게 있는지 확인. 3. 물넘이포장의 월류높이가 초기에 자동계산안됨. 세월교 확인할 것. 4. 이부분은 너가 작업하고 전역 깃 싱크하자.
|
||||
211. (원문 · 10-13) 추가 검토 사항 1. 물넘이 포장 횡단도 형상 개선 - 추가 도수로/ 돌붙임의 경우 길이가 없는 경우에는 성토는 원지반까지 이으면 됨. 하지만 길이값이 들어간 경우에는 길이값만큼 성토부가 짧더라도 원지반까지 이어서라고 지정 길이 확보, 절토인경우에는 현재가 맞음. - 물넘이 포장의 기준측점을 이동하는 경우 반영안되는걸로 보임. - 월류높이에 따라 접선 생성이 되는데 절토부까지는 문제없이 이어지나 성토부에는 성토선을 점선으로 연장해서 이어야함. 세월교 참고. 2. 맹암거 - 기준측점 이동 안될것으로 예상. 3. 횡단도의 복합경사 기본값을 꺼짐으로 반영
|
||||
212. (원문 · 10-13) 추가 검토사항 - 3D영역의 기본보기를 원근으로 변경
|
||||
213. (원문 · 10-13) 1. 3D 형상 업데이트 버튼에 변경에 대한 표현은 나오지만 버튼누를경우 형상 업데이트는 안됨. 2. 구조물측점이 정해진 곳에서 다른 구조물로 변경하는 경우에 다시 구조물이 없는 상태에서 시작해서 다시 선택된 구조물로 변경하면 되는데 이게 힘든거야? 이건 너가 직접 정밀분석해서 직접 수정해봐
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* PLAN 317 단위검증 헬퍼 — 구조물 측점에서 종류를 바꾸면 그 측점 횡단을 새로 뜬 것으로(저장분은 빼 둠).
|
||||
* `withDraftStations` 를 Node 로 돌림 · 서버 부르는 모듈은 흉내 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_317_kind_change_station.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedStructures: [] };
|
||||
const STUBS = {
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: state.savedPipes }),
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => ({ structures: state.savedStructures }),
|
||||
fetchStructureTypes: async () => [],
|
||||
readPendingStructures: () => state.draftStructures,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
|
||||
"./B06_Section_Api_Fetch": {
|
||||
previewStructureSections: async (_p, chainages) =>
|
||||
chainages.map((at) => ({ chainage_m: at, kind: "irregular", design: { fresh: true } })),
|
||||
},
|
||||
};
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (STUBS[m]) return STUBS[m];
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
const { withDraftStations, forgetDraftStations } = load(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_Section_Drafts.ts"),
|
||||
);
|
||||
|
||||
const detail = {
|
||||
cross_sections: [
|
||||
{ chainage_m: 0, kind: "regular", design: { old: true } },
|
||||
{ chainage_m: 50, kind: "irregular", design: { old: true } },
|
||||
],
|
||||
};
|
||||
const at50 = () => detail.cross_sections.find((s) => Math.abs(s.chainage_m - 50) < 0.01);
|
||||
const snap = () => ({
|
||||
count: detail.cross_sections.length,
|
||||
fresh: Boolean(at50()?.design?.fresh),
|
||||
old: Boolean(at50()?.design?.old),
|
||||
preview: Boolean(at50()?.preview),
|
||||
});
|
||||
|
||||
(async () => {
|
||||
state.savedPipes = [{ chainage_m: 50, facility: "ford_pavement" }];
|
||||
const out = {};
|
||||
state.draftPipes = [{ chainage_m: 50, facility: "ford_pavement", options: { ford_width_m: 3 } }];
|
||||
await withDraftStations(detail, "p1", 1);
|
||||
out.same_kind = snap();
|
||||
state.draftPipes = [{ chainage_m: 50, facility: "box_culvert" }];
|
||||
await withDraftStations(detail, "p1", 1);
|
||||
out.changed_kind = snap();
|
||||
state.draftPipes = [{ chainage_m: 50, facility: "ford_pavement" }];
|
||||
await withDraftStations(detail, "p1", 1);
|
||||
out.back_to_saved = snap();
|
||||
forgetDraftStations("p1");
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error && error.stack ? error.stack : error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/* PLAN 317-1 단위검증 헬퍼 — 낡음 지문(`staleKey`) · 같은 지문이면 다시 셈 안 함(`scheduleIfStale`).
|
||||
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 다시 셈 창구는 흉내(부른 횟수만 셈).
|
||||
* 짝 = `test_317_3d_update.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
let refreshCalls = 0;
|
||||
const STUBS = {
|
||||
"../B06_Section/B06_Section_Cross_Refresh": {
|
||||
refreshCrossDesigns: async () => {
|
||||
refreshCalls += 1;
|
||||
return [];
|
||||
},
|
||||
},
|
||||
};
|
||||
globalThis.window = { setTimeout: (fn) => (fn(), 1), clearTimeout: () => {} };
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (STUBS[m]) return STUBS[m];
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
const { staleKey } = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Section_Common.ts"));
|
||||
const { createCrossPreview } = load(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Preview.ts"),
|
||||
);
|
||||
|
||||
const detailOf = (sections) => ({
|
||||
longitudinal: { design_profiles: [] },
|
||||
cross_sections: sections,
|
||||
});
|
||||
const fordStale = {
|
||||
chainage_m: 50,
|
||||
ford_pavement: {},
|
||||
design: { design_elevation_m: 1, paved: true },
|
||||
};
|
||||
const fordFresh = { ...fordStale, design: { ...fordStale.design, tilted: true } };
|
||||
|
||||
(async () => {
|
||||
const out = {
|
||||
key_stale: staleKey(detailOf([fordStale])),
|
||||
key_fresh: staleKey(detailOf([fordFresh])),
|
||||
};
|
||||
const preview = createCrossPreview({
|
||||
projectId: "p1",
|
||||
detail: () => detailOf([fordStale]),
|
||||
routeId: () => 1,
|
||||
edits: () => ({ station_offsets: {}, curve_radii: {} }),
|
||||
debounceMs: () => 0,
|
||||
onApplied: () => {},
|
||||
});
|
||||
const calls = [];
|
||||
for (const key of ["a", "a", "", "a", "b"]) {
|
||||
preview.scheduleIfStale(key);
|
||||
await Promise.resolve();
|
||||
calls.push(refreshCalls);
|
||||
}
|
||||
out.calls = calls;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
process.stderr.write(String(error && error.stack ? error.stack : error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
"""PLAN 317-1 — B05 [3D 업데이트]가 낡은 횡단을 먼저 다시 셈(요구 213).
|
||||
|
||||
- 낡음 지문(`staleKey`) — 낡은 측점이 있으면 지문 · 모두 맞으면 ""
|
||||
- 그릴 때 잰 지문이 새것일 때만 다시 셈 예약(같은 낡음 되풀이 · 서버 폴백 반복 막음)
|
||||
- 그래프가 그릴 때 지문을 패널에 넘김 · 단추는 예상형상을 짓기 전에 낡은 설계부터 다시 셈
|
||||
|
||||
실제 TS 를 `helper_317_stale_preview.cjs` 가 Node 로 돌린다(다시 셈 창구는 흉내).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def result() -> dict:
|
||||
out = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_317_stale_preview.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_stale_key_marks_only_stale_sections(result: dict) -> None:
|
||||
assert result["key_stale"].startswith("50:")
|
||||
assert result["key_fresh"] == ""
|
||||
|
||||
|
||||
def test_same_stale_key_schedules_once(result: dict) -> None:
|
||||
# a → 부름 · a(같음) → 안 부름 · "" → 안 부름(되돌림) · a(다시 낡음) → 부름 · b → 부름
|
||||
assert result["calls"] == [1, 1, 1, 2, 3]
|
||||
|
||||
|
||||
def test_draw_and_button_wiring() -> None:
|
||||
panel = _read("A00_Common", "design", "design_profile.ts")
|
||||
assert "onStaleDesigns: crossPreview.scheduleIfStale" in panel
|
||||
render = _read("B05_Profile", "B05_Profile_UI_Profile_Render.ts")
|
||||
assert "ctx.onStaleDesigns?.(stale);" in render
|
||||
assert 'pendingRecalc: stale !== ""' in render
|
||||
corridor = _read("B05_Profile", "B05_Profile_UI_Corridor.ts")
|
||||
body = corridor.split("export async function refreshCorridor", 1)[1].split("\n}\n", 1)[0]
|
||||
assert "await refreshStaleDesigns({ projectId, routeId, detail })" in body
|
||||
assert body.index("refreshStaleDesigns") < body.index("ensureCorridor")
|
||||
@@ -0,0 +1,49 @@
|
||||
"""PLAN 317 — 구조물 측점에서 종류를 바꾸면 횡단을 「구조물 없는 상태」부터 새로(요구 213).
|
||||
|
||||
- 저장분과 같은 종류 = 저장분 횡단 그대로
|
||||
- 다른 종류로 바꿈 = 저장분 횡단은 빼 두고 새 자리처럼 미리 뜬 횡단(옛 구조물 흔적 없음)
|
||||
- 저장분 종류로 되돌림 = 빼 둔 저장분 횡단이 돌아옴
|
||||
|
||||
실제 TS(`withDraftStations`)를 `helper_317_kind_change_station.cjs` 가 Node 로 돌린다(서버 부르는 곳 흉내).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def result() -> dict:
|
||||
out = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_317_kind_change_station.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_same_kind_keeps_saved_section(result: dict) -> None:
|
||||
assert result["same_kind"] == {"count": 2, "fresh": False, "old": True, "preview": False}
|
||||
|
||||
|
||||
def test_changed_kind_starts_from_fresh_section(result: dict) -> None:
|
||||
assert result["changed_kind"] == {"count": 2, "fresh": True, "old": False, "preview": True}
|
||||
|
||||
|
||||
def test_back_to_saved_kind_restores_saved_section(result: dict) -> None:
|
||||
assert result["back_to_saved"] == {"count": 2, "fresh": False, "old": True, "preview": False}
|
||||
Reference in New Issue
Block a user