Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_facilities.ts
|
||||
* 계산 묶음 관 · 구조물 먹이기(PLAN 415) — 가게 관 · 구조물 초안이 바뀌면 묶음에 넣음(비면 저장분).
|
||||
* 계산 묶음 관 · 구조물 · 추가 측점 먹이기(PLAN 415 · 429 ⑨) — 가게 초안이 바뀌면 묶음에 넣음(비면 저장분).
|
||||
* · [저장] 도중 = 안 넣음(H2) — 초안이 비는 사이 열 때 읽은 옛 저장분으로 돌지 않게 · 끝나면 `resync`
|
||||
* (저장 목록 다시 읽기 · 입력 맞춤) 한 번.
|
||||
* · 되돌리기 · 다시하기 = 세션만 되쓰고 가게 알림이 없음 → 묶음 입력과 견줘 다르면 넣음(415-2 · 노선 초안은
|
||||
@@ -12,10 +12,11 @@ import { onPageHistory } from "../../B05_Profile/B05_Profile_UI_Profile_History"
|
||||
import type { useDesignStore } from "./design_store";
|
||||
import type { ChangeSource } from "./logic/design_pipeline_types";
|
||||
|
||||
type Key = "pipes" | "structures";
|
||||
type Key = "pipes" | "structures" | "plusStations";
|
||||
const FACILITIES: ReadonlyArray<{ source: ChangeSource; key: Key }> = [
|
||||
{ source: "pipe", key: "pipes" },
|
||||
{ source: "structure", key: "structures" },
|
||||
{ source: "plus", key: "plusStations" },
|
||||
];
|
||||
|
||||
export interface FacilityFeed {
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
readAlignmentDraft,
|
||||
} from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
|
||||
import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
|
||||
import { plusStationsOf } from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import { pageReplaying } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
||||
import {
|
||||
analysisTerrain,
|
||||
@@ -533,6 +534,7 @@ function createPagePipeline(
|
||||
cross: crossNow()?.input ?? null,
|
||||
pipes: store.get("pipes") ?? savedPipes,
|
||||
structures: store.get("structures") ?? savedStructures,
|
||||
plusStations: plusStationsOf(projectId),
|
||||
};
|
||||
};
|
||||
const start = (): void => {
|
||||
@@ -625,7 +627,12 @@ function createPagePipeline(
|
||||
store,
|
||||
ready: () => started && !echo,
|
||||
gone: () => disposed,
|
||||
saved: (key) => (key === "pipes" ? savedPipes : savedStructures),
|
||||
saved: (key) =>
|
||||
key === "pipes"
|
||||
? savedPipes
|
||||
: key === "structures"
|
||||
? savedStructures
|
||||
: plusStationsOf(projectId),
|
||||
input: (key) => pipe.input(key),
|
||||
routed: () => pipe.input("route") !== null || store.get("routeDraft") !== null,
|
||||
feed: (source, value) => void feed(source, value),
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* 새 노선 번호를 준다. 그 뒤 차례는 **새 노선**으로: 관 · 구조물 초안(좌표 정본) · 계획고 초안(옛 노선 칸 →
|
||||
* 새 노선) · 횡단 초안은 측점 열쇠로 넘겨(`carryStationDrafts`) 저장 · 결과 한 줄 · 페이지 다시 세움.
|
||||
* 노선 저장이 실패하면 서버가 되살리고 여기서 멈춤(다른 초안도 화면 · 세션에 그대로 · 오류 한 줄).
|
||||
* 어느 차례든 실패하면 그 차례에서 멈추고 차례 이름째 알림(429 ⑨ · 뒤 차례는 안 보냄).
|
||||
* 노선을 저장했으면 횡단은 고침이 없어도 한 번 나감 — 뒤에 나간 계획고 초안을 서버가 횡단에 다시 셈(429 ④).
|
||||
*
|
||||
* 차례는 여기 한 곳에만 둔다. 보기마다 다른 것은 **화면이 쥔 값으로 한 번 더 맞추는 일**
|
||||
* (B05 배수유역도 목록 · 구조물 목록 새로 받기 · 종단 패널 다시 그리기)과 횡단 저장 본문뿐이라,
|
||||
@@ -86,12 +88,22 @@ export function savingDesign(projectId: string | null): boolean {
|
||||
return projectId !== null && saving.has(projectId);
|
||||
}
|
||||
|
||||
/** 실패해도 저장을 멈추지 않는 창구 — 알리고 넘어간다(초안은 세션에 남아 다시 나갈 수 있다). */
|
||||
function reportOnly(label: string): (error: unknown) => void {
|
||||
return (error) => {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`${label} 저장에 실패했습니다.${detail}`, "error");
|
||||
};
|
||||
/** 차례 이름 — 실패하면 그 차례에서 멈추고 이름째 알림(429 ⑨ · 뒤 차례가 옛 정본 위에 얹히지 않게 ·
|
||||
* 초안은 세션에 남아 다음 [저장]에서 다시 나감). */
|
||||
const STEP_LABEL: Record<DesignSaveStep, string> = {
|
||||
route: "노선",
|
||||
pipes: "배수관",
|
||||
structures: "구조물",
|
||||
plusStations: "추가 측점",
|
||||
alignment: "계획선",
|
||||
culvertOptions: "암거 옵션",
|
||||
uphill: "상단측",
|
||||
cross: "횡단",
|
||||
};
|
||||
|
||||
function stepFailed(step: DesignSaveStep, error: unknown): Error {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
return new Error(`${STEP_LABEL[step]} 차례에서 멈췄습니다(뒤 차례 안 보냄).${detail}`);
|
||||
}
|
||||
|
||||
/** 서버가 버린 계획고 편집 자리(측점 · 관 · 구조물이 없는 자리 · 213-2)를 알린다. */
|
||||
@@ -128,15 +140,7 @@ async function flushAlignmentDraft(
|
||||
if (moved) markAlignmentSaved(DRAFT_ROUTE_ID);
|
||||
}
|
||||
|
||||
/** 실패해도 다음 차례로 넘어가는 창구와 그 이름(보기가 준 일에도 같이 씌운다). */
|
||||
const TOLERANT: Partial<Record<DesignSaveStep, string>> = {
|
||||
pipes: "배수관",
|
||||
structures: "구조물",
|
||||
plusStations: "추가 측점",
|
||||
alignment: "계획선",
|
||||
};
|
||||
|
||||
/** 차례마다 기본 창구. 암거 옵션 · 상단측은 조용히 넘어간다(다음 [저장]에서 다시 나감). */
|
||||
/** 차례마다 기본 창구. */
|
||||
function defaultStep(
|
||||
step: Exclude<DesignSaveStep, "cross" | "route">,
|
||||
projectId: string,
|
||||
@@ -153,12 +157,9 @@ function defaultStep(
|
||||
case "alignment":
|
||||
return () => flushAlignmentDraft(projectId, routeId, fromRouteId);
|
||||
case "culvertOptions":
|
||||
return () =>
|
||||
flushCulvertOptions(projectId, stateKey("culvertopt", projectId, fromRouteId)).catch(
|
||||
() => undefined,
|
||||
);
|
||||
return () => flushCulvertOptions(projectId, stateKey("culvertopt", projectId, fromRouteId));
|
||||
case "uphill":
|
||||
return () => flushUphillOverrides(projectId).catch(() => undefined);
|
||||
return () => flushUphillOverrides(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,35 +202,38 @@ export async function saveDesign(
|
||||
try {
|
||||
await ctx.prepare?.();
|
||||
for (const step of DESIGN_SAVE_ORDER) {
|
||||
if (step === "route") {
|
||||
const body = await pendingRouteSave(ctx.projectId); // 안 바뀌었으면 null — 안 보냄
|
||||
if (!body) continue;
|
||||
const before = await savedPipesIfDraft(ctx.projectId);
|
||||
routeSaved = await saveRouteDraft(ctx.projectId, body); // 실패 = throw(서버가 되살림)
|
||||
if (routeSaved.route_id != null) routeId = routeSaved.route_id;
|
||||
if (before) await rebasePendingPipes(ctx.projectId, before);
|
||||
continue;
|
||||
}
|
||||
if (step === "cross") {
|
||||
if (!routeSaved) await ctx.cross(confirm);
|
||||
else {
|
||||
const outside = await carryAfterRouteSave(ctx.projectId, fromRouteId, routeId);
|
||||
showToast(routeSaveSummary(routeSaved, outside), "info");
|
||||
try {
|
||||
if (step === "route") {
|
||||
const body = await pendingRouteSave(ctx.projectId); // 안 바뀌었으면 null — 안 보냄
|
||||
if (!body) continue;
|
||||
const before = await savedPipesIfDraft(ctx.projectId);
|
||||
routeSaved = await saveRouteDraft(ctx.projectId, body); // 실패 = throw(서버가 되살림)
|
||||
if (routeSaved.route_id != null) routeId = routeSaved.route_id;
|
||||
if (before) await rebasePendingPipes(ctx.projectId, before);
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
if (step === "cross") {
|
||||
if (!routeSaved) await ctx.cross(confirm);
|
||||
else {
|
||||
const outside = await carryAfterRouteSave(ctx.projectId, fromRouteId, routeId);
|
||||
showToast(routeSaveSummary(routeSaved, outside), "info");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 노선이 바뀌면 화면 몫 일(옛 노선 판 목록 · 계획고 판)은 건너뛰고 기본 창구만 새 노선으로.
|
||||
const own = routeSaved ? undefined : ctx.steps?.[step];
|
||||
const base = defaultStep(step, ctx.projectId, routeId, fromRouteId);
|
||||
const run =
|
||||
step === "pipes"
|
||||
? async () => {
|
||||
await base();
|
||||
await own?.();
|
||||
}
|
||||
: (own ?? base);
|
||||
await run();
|
||||
} catch (error) {
|
||||
throw stepFailed(step, error); // 그 차례에서 멈춤 · 이름째 알림(429 ⑨)
|
||||
}
|
||||
// 노선이 바뀌면 화면 몫 일(옛 노선 판 목록 · 계획고 판)은 건너뛰고 기본 창구만 새 노선으로.
|
||||
const own = routeSaved ? undefined : ctx.steps?.[step];
|
||||
const base = defaultStep(step, ctx.projectId, routeId, fromRouteId);
|
||||
const run =
|
||||
step === "pipes"
|
||||
? async () => {
|
||||
await base();
|
||||
await own?.();
|
||||
}
|
||||
: (own ?? base);
|
||||
const label = TOLERANT[step];
|
||||
await (label ? run().catch(reportOnly(label)) : run());
|
||||
}
|
||||
clearState("structure-type-cache", ctx.projectId); // 종류별 캐시는 저장 뒤 비움(54-32)
|
||||
saving.delete(ctx.projectId); // 다 나감 — 뒤처리는 새 상세로 그림
|
||||
|
||||
@@ -50,7 +50,7 @@ type Dep = StageName | `input.${InputKey}`;
|
||||
export const STAGE_DEPS: Record<StageName, Dep[]> = {
|
||||
route: ["input.route"],
|
||||
follow: ["route"],
|
||||
stations: ["route", "input.pipes", "input.structures"],
|
||||
stations: ["route", "input.pipes", "input.structures", "input.plusStations"],
|
||||
ground: ["stations"],
|
||||
planline: ["ground"],
|
||||
profile: ["planline", "input.alignment"],
|
||||
@@ -152,6 +152,7 @@ export function createDesignPipeline(options: { stages: PipelineStages }): Desig
|
||||
cross: null,
|
||||
pipes: null,
|
||||
structures: null,
|
||||
plusStations: null,
|
||||
};
|
||||
const inputVer: Record<InputKey, number> = {
|
||||
route: 0,
|
||||
@@ -159,6 +160,7 @@ export function createDesignPipeline(options: { stages: PipelineStages }): Desig
|
||||
cross: 0,
|
||||
pipes: 0,
|
||||
structures: 0,
|
||||
plusStations: 0,
|
||||
};
|
||||
const out = emptyOuts();
|
||||
const outVer = {} as Record<StageName, number>;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* 계산 묶음 본체 = `design_pipeline.ts`.
|
||||
*
|
||||
* 흐름 — 한 방향:
|
||||
* 입력 route(노선 초안) · alignment(계획고) · cross(횡단 입력) · pipes(관) · structures(구조물)
|
||||
* 입력 route(노선 초안) · alignment(계획고) · cross(횡단 입력) · pipes(관) · structures(구조물) ·
|
||||
* plusStations(추가 측점 · 429 ⑨)
|
||||
* ↓ update(출처, 값, 범위) — 조작마다 차례 번호(generation) +1 · 옛 계산 · 옛 요청 버림
|
||||
* 브라우저 즉시(차례대로 · 단계 사이마다 번호 확인 · 단계마다 결과 바로 내보냄)
|
||||
* route → follow → stations → ground → planline → profile → cross → footprint → masshaul
|
||||
@@ -24,7 +25,7 @@
|
||||
* | 단계 | 주인 | deps | 출력 |
|
||||
* | route | sub8 | input.route | RouteLine |
|
||||
* | follow | sub1 | route | FollowPatch(입력 고침) |
|
||||
* | stations | sub7 | route · input.pipes · input.structures | PipelineStation[](지반 null) |
|
||||
* | stations | sub7 | route · input.pipes · input.structures · input.plusStations | PipelineStation[](지반 null) |
|
||||
* | ground | sub8 | stations | GroundOutput |
|
||||
* | planline | sub1 | ground | AlignmentBase(첫 계획선) |
|
||||
* | profile | sub1 | planline · input.alignment | ProfileAlignment |
|
||||
@@ -89,12 +90,14 @@ export interface PipelineInputs {
|
||||
pipes: unknown;
|
||||
/** 구조물 목록(`StructureInstance[]`) — 위와 같음. */
|
||||
structures: unknown;
|
||||
/** 추가 측점(정수 m · 초안 ?? 저장분) — 바뀌면 stations 부터(429 ⑨) · 셈은 `plusStations()` 가 같은 값을 읽음. */
|
||||
plusStations?: ReadonlyArray<number> | null;
|
||||
}
|
||||
|
||||
export type InputKey = keyof PipelineInputs;
|
||||
|
||||
/** 조작 출처 — 노선 · 계획고 · 횡단 · 관 · 구조물. */
|
||||
export type ChangeSource = "route" | "grade" | "cross" | "pipe" | "structure";
|
||||
/** 조작 출처 — 노선 · 계획고 · 횡단 · 관 · 구조물 · 추가 측점. */
|
||||
export type ChangeSource = "route" | "grade" | "cross" | "pipe" | "structure" | "plus";
|
||||
|
||||
/** 출처 → 입력 칸. */
|
||||
export const SOURCE_INPUT: Record<ChangeSource, InputKey> = {
|
||||
@@ -103,6 +106,7 @@ export const SOURCE_INPUT: Record<ChangeSource, InputKey> = {
|
||||
cross: "cross",
|
||||
pipe: "pipes",
|
||||
structure: "structures",
|
||||
plus: "plusStations",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -118,11 +118,14 @@ async def sync_uphill_overrides_into_designs(
|
||||
from B06_Section.B06_Section_Repository import get_cross_section_designs
|
||||
from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs
|
||||
from B06_Section.B06_Section_Router_Design import (
|
||||
USER_TOUCHED_KEYS,
|
||||
ford_drop_at,
|
||||
ford_surface_drops,
|
||||
pavement_suggestions,
|
||||
read_cross_design_inputs,
|
||||
resolve_longitudinal_path,
|
||||
stored_berm,
|
||||
stored_cut_slope,
|
||||
)
|
||||
|
||||
if not overrides:
|
||||
@@ -172,23 +175,26 @@ async def sync_uphill_overrides_into_designs(
|
||||
ground_type=str(design.get("ground_type", "soil")),
|
||||
section_mode=next_mode,
|
||||
ditch_side=side,
|
||||
ditch_type=str(design.get("ditch_type", "standard")),
|
||||
ditch_type=str(design.get("ditch_type") or "standard"), # 측구 끔 = None 저장분
|
||||
paved=bool(design.get("paved", False)),
|
||||
standard=stored_standard,
|
||||
rock_boundary_offset_m=design.get("rock_boundary_offset_m"),
|
||||
two_stage_slope=bool(design.get("two_stage_slope", False)),
|
||||
cut_slope_ratio=stored_cut_slope(design), # 측점별 깎기 경사(429 ⑤)
|
||||
ditch_enabled=design.get("ditch_enabled"),
|
||||
ditch_choice=design.get("ditch_choice"),
|
||||
surface_drop_m=ford_drop_at(float(chainage), ford_drops),
|
||||
berm=stored_berm(design), # 소단 · 쪽마다 옮긴 깊이(429 ⑤)
|
||||
**curve_widening_args(cross_record),
|
||||
**stored_shoulder_extend(design), # 노견 확장(139-6)
|
||||
**stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4)
|
||||
)
|
||||
next_design["status"] = design.get("status", "provisional")
|
||||
next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested)
|
||||
# 개별 표시 반폭 등 계산과 무관한 표시 설정은 그대로 이월한다.
|
||||
if design.get("display_half_width_m") is not None:
|
||||
next_design["display_half_width_m"] = design["display_half_width_m"]
|
||||
# 사용자 조작값 승계(조절 · 소단 · 깎기 경사 · 글) — `enforce_design_lines` 와 같음
|
||||
for key in USER_TOUCHED_KEYS:
|
||||
if design.get(key) is not None:
|
||||
next_design[key] = design[key]
|
||||
out.append((float(chainage), next_design))
|
||||
return out
|
||||
|
||||
|
||||
@@ -353,15 +353,14 @@ export function crossPatchesFromCache(
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에 얹힌 사용자 수정분을 영구저장소에 남긴다(임시저장용). 캐시가 없거나 수정분이
|
||||
* 없으면 아무것도 보내지 않는다. 저장 실패는 호출자가 처리한다.
|
||||
* 캐시에 얹힌 사용자 수정분을 영구저장소에 남긴다(노선 저장 뒤 · 임시저장용). 수정분이 없어도
|
||||
* 한 번 보냄 — 서버가 지금 종단으로 횡단을 다시 셈(429 ④ · 노선 뒤에 나간 계획고 초안).
|
||||
* 저장 실패는 호출자가 처리한다.
|
||||
*/
|
||||
export async function saveCachedCrossPatches(projectId: string, routeId: number): Promise<number> {
|
||||
const detail = cache.get(keyOf(projectId, routeId));
|
||||
if (!detail) return 0;
|
||||
const patches = crossPatchesFromCache(detail, kindChangedChainages(projectId));
|
||||
if (!patches.length) return 0;
|
||||
await saveSections(projectId, routeId, undefined, patches);
|
||||
const patches = detail ? crossPatchesFromCache(detail, kindChangedChainages(projectId)) : [];
|
||||
await saveSections(projectId, routeId, undefined, patches.length ? patches : undefined);
|
||||
clearState("kindreset", projectId); // 비울 자리를 보냈음(318-4)
|
||||
return patches.length;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* PLAN 429 단위검증 헬퍼 — [저장] 길 정합.
|
||||
* ④ 노선 저장 뒤 횡단 저장 = 고침이 없어도 한 번(`saveCachedCrossPatches`)
|
||||
* ⑨ 추가 측점 = 계산 묶음 입력(`plusStations` · stations 부터) · 가게 초안 바뀜 → 묶음에 넣음(`feedFacilities`)
|
||||
* TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 서버 창구 · 화면 모듈은 가짜. 짝 = `test_429_save_path.py`. */
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
globalThis.window = { sessionStorage: null, localStorage: null, setTimeout, clearTimeout };
|
||||
|
||||
/** 아무 이름이나 받는 가짜 모듈 — 부르면 undefined. */
|
||||
const anything = () =>
|
||||
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
|
||||
|
||||
function loader(stubs, real = () => false) {
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
const full = path.resolve(file);
|
||||
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 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
const target = path.join(path.dirname(full), `${request}.ts`);
|
||||
if (request.startsWith(".") && real(target)) return load(target);
|
||||
return anything();
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
};
|
||||
return load;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const out = {};
|
||||
|
||||
// ④ 고침이 없어도 횡단 저장 한 번 — 서버가 지금 종단으로 횡단을 다시 셈
|
||||
const sent = [];
|
||||
const store = loader({
|
||||
"./B06_Section_Api_Fetch": {
|
||||
saveSections: async (...args) => (sent.push(args.map((a) => a ?? null)), {}),
|
||||
fetchSectionDetail: async () => null,
|
||||
},
|
||||
})(path.join(ROOT, "B06_Section", "B06_Section_Section_Store.ts"));
|
||||
out.noPatchReturn = await store.saveCachedCrossPatches("p1", 9);
|
||||
out.noPatchSent = sent;
|
||||
|
||||
// ⑨ 묶음 — 추가 측점이 바뀌면 stations 부터 · 같은 값이면 안 셈
|
||||
const DESIGN = path.join(ROOT, "A00_Common", "design");
|
||||
const pipeMod = loader({}, () => true)(path.join(DESIGN, "logic", "design_pipeline.ts"));
|
||||
out.stationsDeps = pipeMod.STAGE_DEPS.stations;
|
||||
const ran = [];
|
||||
const stages = {};
|
||||
for (const name of [...pipeMod.BROWSER_STAGES, ...pipeMod.AFTER_STAGES])
|
||||
stages[name] = (ctx) => (ran.push(name), { name, plus: ctx.inputs.plusStations ?? null });
|
||||
stages.follow = () => (ran.push("follow"), null); // 입력 고침 없음
|
||||
const pipe = pipeMod.createDesignPipeline({ stages });
|
||||
await pipe.start({
|
||||
route: null,
|
||||
alignment: null,
|
||||
cross: null,
|
||||
pipes: [],
|
||||
structures: [],
|
||||
plusStations: [],
|
||||
});
|
||||
ran.length = 0;
|
||||
await pipe.update("plus", [40]);
|
||||
out.plusRan = [...ran];
|
||||
out.plusSeen = pipe.output("stations")?.plus ?? null;
|
||||
ran.length = 0;
|
||||
out.sameValue = pipe.update("plus", [40]); // 같은 값 = 차례 안 띄움(null)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
out.sameRan = [...ran];
|
||||
|
||||
// ⑨ 먹이기 — 가게 추가 측점 초안이 바뀌면 묶음에 · 비면 저장분 · 되돌리기는 입력과 견줘 다를 때만
|
||||
let restore = null;
|
||||
const listeners = new Map();
|
||||
const values = new Map();
|
||||
const fakeStore = {
|
||||
get: (key) => values.get(key) ?? null,
|
||||
set: (key, value) => (
|
||||
values.set(key, value),
|
||||
(listeners.get(key) ?? []).forEach((fn) => fn(value))
|
||||
),
|
||||
subscribe: (key, fn) => {
|
||||
listeners.set(key, [...(listeners.get(key) ?? []), fn]);
|
||||
return () => undefined;
|
||||
},
|
||||
};
|
||||
const fed = [];
|
||||
const inputNow = { plusStations: [12] };
|
||||
const feeder = loader({
|
||||
"../../B05_Profile/B05_Profile_UI_Profile_History": {
|
||||
onPageHistory: (hooks) => ((restore = hooks.restore), () => undefined),
|
||||
},
|
||||
"./design_save": { savingDesign: () => false },
|
||||
})(path.join(DESIGN, "design_pipeline_facilities.ts"));
|
||||
feeder.feedFacilities({
|
||||
projectId: "p1",
|
||||
store: fakeStore,
|
||||
ready: () => true,
|
||||
gone: () => false,
|
||||
saved: (key) => (key === "plusStations" ? [12] : []),
|
||||
input: (key) => inputNow[key] ?? null,
|
||||
routed: () => false,
|
||||
feed: (source, value) => fed.push([source, value]),
|
||||
resync: async () => undefined,
|
||||
});
|
||||
fakeStore.set("plusStations", [12, 30]);
|
||||
fakeStore.set("plusStations", null); // 초안 비움 = 저장분
|
||||
out.fed = [...fed];
|
||||
fed.length = 0;
|
||||
values.set("plusStations", [12, 55]); // 되돌리기 = 세션만 되씀(알림 없음)
|
||||
restore?.();
|
||||
out.restoreFed = fed.filter(([source]) => source === "plus");
|
||||
return out;
|
||||
}
|
||||
|
||||
run().then(
|
||||
(out) => console.log(JSON.stringify(out)),
|
||||
(error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
},
|
||||
);
|
||||
@@ -161,16 +161,16 @@ async function run() {
|
||||
out.afterSave = { dirty: b05.dirty(), draft: useDesignStore(null, 7).get("alignmentDraft") };
|
||||
out.order = DESIGN_SAVE_ORDER;
|
||||
|
||||
// ④ 구조물이 실패해도 다음 차례로(알림) · 확정 · 노선 없으면 안 나감
|
||||
// ④ 구조물이 실패하면 그 차례에서 멈춤 · 차례 이름째 알림(429 ⑨) · 노선 없으면 안 나감
|
||||
calls.length = 0;
|
||||
failStructures = true;
|
||||
await saveDesign(
|
||||
out.tolerantOk = await saveDesign(
|
||||
{
|
||||
projectId: "p1",
|
||||
routeId: () => 7,
|
||||
steps: { uphill: record("uphill:screen") },
|
||||
cross: async (confirm) => calls.push(`cross:${confirm}`),
|
||||
failed: () => calls.push("failed"),
|
||||
failed: (error) => (calls.push("failed"), (out.tolerantError = error.message)),
|
||||
},
|
||||
{ confirm: true },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""PLAN 429 — [저장] 길 정합(분석 424 9장 ④⑤⑧⑨).
|
||||
|
||||
- ④ 노선 [저장] 뒤 횡단 저장 = 고침이 없어도 한 번(서버가 지금 종단으로 횡단을 다시 셈 · 뒤에
|
||||
나간 계획고 초안이 횡단 설계 높이에 실림) · 서버 [저장]이 횡단 설계 높이를 저장 종단에서 냄.
|
||||
- ⑤ 상단측 차례(`sync_uphill_overrides_into_designs`)가 소단 · 깎기 경사 · 조절 값을 남김.
|
||||
- ⑨ 추가 측점 = 계산 묶음 입력(stations 부터 다시) · 가게 초안 바뀜 · 되돌리기 → 묶음에 넣음.
|
||||
[저장] 차례 실패 = 그 차례에서 멈춤(`test_design_store_save.py`).
|
||||
Node 헬퍼 = `helper_429_save_path.cjs`.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B06_Section.B06_Section_Engine_Design import compute_cross_design
|
||||
from B06_Section.B06_Section_Router_Design import enforce_design_lines
|
||||
from config.config_system import STANDARD_CROSS_SECTION
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out():
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_429_save_path.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _ground() -> list[dict]:
|
||||
"""왼쪽이 높은 비탈 — 왼쪽 깎기 · 오른쪽 쌓기(편절)."""
|
||||
return [
|
||||
{"offset_m": o / 2, "elevation_m": round(100.0 + 0.25 * o / 2, 4), "valid": True}
|
||||
for o in range(-40, 41)
|
||||
]
|
||||
|
||||
|
||||
# ── ④ ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_노선_저장_뒤_고침이_없어도_횡단_저장_한_번(out):
|
||||
assert out["noPatchSent"] == [["p1", 9, None, None]] # 본문 없이 = 서버가 다시 셈만
|
||||
assert out["noPatchReturn"] == 0
|
||||
|
||||
|
||||
def test_서버_저장이_횡단_높이를_저장_종단에서(tmp_path):
|
||||
"""초안 계획고(101.2)가 종단 정본에 나간 뒤 [저장] 다시 셈 = 그 높이 · 옛 설계(100.0) 아님."""
|
||||
old = compute_cross_design(
|
||||
_ground(), 100.0, standard=STANDARD_CROSS_SECTION, ground_type="soil",
|
||||
section_mode="left_cut", ditch_side="left",
|
||||
) # fmt: skip
|
||||
longitudinal = {
|
||||
"design_profiles": [
|
||||
{"samples": [{"chainage_m": 0.0, "elevation_m": 101.2},
|
||||
{"chainage_m": 40.0, "elevation_m": 101.2}]}
|
||||
]
|
||||
} # fmt: skip
|
||||
sections = [{"chainage_m": 20.0, "samples": _ground(), "design": old}]
|
||||
assert enforce_design_lines(longitudinal, sections, tmp_path, STANDARD_CROSS_SECTION) == 1
|
||||
assert sections[0]["design"]["design_elevation_m"] == pytest.approx(101.2, abs=1e-6)
|
||||
|
||||
|
||||
# ── ⑤ ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_상단측_차례가_소단_깎기경사_조절값_남김(tmp_path, monkeypatch):
|
||||
import B05_Profile.B05_Profile_Router_Confirm as confirm
|
||||
import B06_Section.B06_Section_Repository as repo
|
||||
import B06_Section.B06_Section_Repository_Bulk as bulk
|
||||
import B06_Section.B06_Section_Router_Design as design_mod
|
||||
|
||||
user = {
|
||||
"cut_slope_ratio_user": 0.7,
|
||||
"berm": {"width_m": 1.0, "interval_m": 3.0, "slope_deg": 0.0, "depth_m": 1.0},
|
||||
"berm_depth_left_m": 2.0,
|
||||
"basin_adjust": {"w": 1.5},
|
||||
"structure_note": "글",
|
||||
"shoulder_extend_right_m": 0.3,
|
||||
}
|
||||
before = compute_cross_design(
|
||||
_ground(), 100.0, standard=STANDARD_CROSS_SECTION, ground_type="soil",
|
||||
section_mode="right_cut", ditch_side="right",
|
||||
) # fmt: skip
|
||||
stored = {**before, **user}
|
||||
long_file = tmp_path / "long.json"
|
||||
long_file.write_text("{}", encoding="utf-8")
|
||||
|
||||
async def designs(_connection, _route_id):
|
||||
return [{"chainage_m": 20.0, "design": stored}]
|
||||
|
||||
async def longitudinal(*_args):
|
||||
return {"data": {"options": {"standard_cross_section": STANDARD_CROSS_SECTION}}}
|
||||
|
||||
written: list = []
|
||||
|
||||
async def merge(_connection, *, route_id, entries, replace, project_id=None):
|
||||
written.extend(entries)
|
||||
|
||||
monkeypatch.setattr(repo, "get_cross_section_designs", designs)
|
||||
monkeypatch.setattr(confirm, "get_longitudinal_section", longitudinal)
|
||||
monkeypatch.setattr(bulk, "merge_cross_section_designs", merge)
|
||||
monkeypatch.setattr(design_mod, "resolve_longitudinal_path", lambda *_: long_file)
|
||||
monkeypatch.setattr(
|
||||
design_mod, "read_cross_design_inputs", lambda *_a: (_ground(), 100.0, False, {})
|
||||
)
|
||||
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda _root: [])
|
||||
|
||||
asyncio.run(
|
||||
confirm.sync_uphill_overrides_into_designs(
|
||||
None, "p", 1, tmp_path, "long.json", [{"chainage_m": 20.0, "side": "left"}]
|
||||
)
|
||||
)
|
||||
assert len(written) == 1
|
||||
chainage, after = written[0]
|
||||
assert chainage == 20.0 and before["ditch_type"] is None # 측구 끔 저장분도 셈
|
||||
for key, value in user.items():
|
||||
assert after[key] == value, key # 값 남음
|
||||
assert after["cut_slope_ratio"] == pytest.approx(0.7) # 깎기 경사가 기하에 실림(키만 아님)
|
||||
assert after["shoulder_extend_right_m"] == pytest.approx(0.3)
|
||||
|
||||
|
||||
# ── ⑨ ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_추가_측점이_묶음_입력(out):
|
||||
assert "input.plusStations" in out["stationsDeps"]
|
||||
assert out["plusRan"][0] == "stations" and "route" not in out["plusRan"] # stations 부터
|
||||
assert "cross" in out["plusRan"] # 뒤 단계까지 이어짐
|
||||
assert out["plusSeen"] == [40]
|
||||
assert out["sameValue"] is None and out["sameRan"] == [] # 같은 값은 안 셈
|
||||
|
||||
|
||||
def test_가게_추가_측점_초안이_묶음에(out):
|
||||
assert out["fed"] == [["plus", [12, 30]], ["plus", [12]]] # 초안 · 비우면 저장분
|
||||
assert out["restoreFed"] == [["plus", [12, 55]]] # 되돌리기 = 입력과 다르면 넣음
|
||||
@@ -77,8 +77,8 @@ def test_저장이_창구를_다섯_다_거친다():
|
||||
assert token in save, f"{token} 가 저장 한 줄기에서 사라짐"
|
||||
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
|
||||
assert "ctx.flushCulvertOptions()" in persist, "B06 조정창 구간값이 저장에서 빠짐"
|
||||
# 하나가 실패해도 나머지가 나가야 한다 — 관·구조물·계획선은 `catch` 로 감싸 둔다.
|
||||
assert save.count(".catch(") >= 3, "실패 격리(catch)가 줄었음"
|
||||
# 차례가 실패하면 그 차례에서 멈추고 이름째 알림(429 ⑨ · 뒤 차례가 옛 정본 위에 얹히지 않게).
|
||||
assert "throw stepFailed(step, error)" in save, "실패한 차례에서 멈춤이 빠짐"
|
||||
order = save[save.index("DESIGN_SAVE_ORDER = [") :]
|
||||
order = order[: order.index("]")]
|
||||
names = ["pipes", "structures", "alignment", "culvertOptions", "uphill", "cross"]
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
- 가게(`design_store`) — 밑은 등록 키 · 같은 칸이면 어느 가게든 같은 값 · 듣는 쪽.
|
||||
- 계획고 가게 한 벌 — B05 종단 패널 · B06 계획선 편집이 같은 벌을 씀(옛 두 가게 덮어쓰기 흠).
|
||||
- 저장 한 줄기(`saveDesign`) — 관 → 구조물 → 계획고 → 암거 옵션 → 오르막 → 횡단 · 관 · 구조물 ·
|
||||
계획고는 실패해도 다음 차례로.
|
||||
- 저장 한 줄기(`saveDesign`) — 관 → 구조물 → 계획고 → 암거 옵션 → 오르막 → 횡단 · 어느 차례든
|
||||
실패하면 거기서 멈추고 차례 이름째 알림(429 ⑨).
|
||||
Node 헬퍼(`helper_design_store_save.cjs`)가 TS 를 트랜스파일해 돌린다.
|
||||
"""
|
||||
|
||||
@@ -76,13 +76,12 @@ def test_저장_차례(out):
|
||||
assert out["afterSave"] == {"dirty": False, "draft": None}
|
||||
|
||||
|
||||
def test_구조물_실패는_알리고_넘어감_확정_노선없음(out):
|
||||
calls = out["tolerant"]
|
||||
assert "toast:error" in calls
|
||||
assert calls.index("structures") < calls.index("toast:error") < calls.index("uphill:screen")
|
||||
assert "uphill" not in calls, "보기 몫이 기본 창구를 갈음"
|
||||
assert calls[-2:] == ["cross:true", "overlay:off"]
|
||||
assert "failed" not in calls
|
||||
def test_구조물_실패는_멈추고_알림_노선없음(out):
|
||||
"""429 ⑨ — 차례가 실패하면 뒤 차례(상단측 · 횡단)를 안 보냄 · 차례 이름째 알림."""
|
||||
assert out["tolerant"] == ["overlay:on", "pipes", "structures", "failed", "overlay:off"]
|
||||
assert out["tolerantOk"] is False
|
||||
assert out["tolerantError"].startswith("구조물 차례에서 멈췄습니다")
|
||||
assert "structures 실패" in out["tolerantError"] # 까닭 그대로
|
||||
assert out["noRouteSave"] is False and out["noRouteCalls"] == []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user