Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
* 초안이 있으면 측점을 냄(새로고침 · 다시 열기 = 초안 측점 · 466).
|
||||
* 노선 초안을 버리면(되돌리기 · [저장 전으로]) 초안 전 상세 · 횡단 초안 칸 그대로(349-0k · 349-0o) · 가게 측점 ·
|
||||
* 종단 = 그 상세 측점 · 저장 기준(466) · 그 뒤 같은 측점 목록은 가게에 다시 안 냄(상세 측점이 묶음과 달라도 · 349-0q).
|
||||
* [저장](sub5)이 서버 정본 — 노선을 먼저 보내고 `pendingAlignment(DRAFT_ROUTE_ID)` 를 새 노선에 보냄.
|
||||
* [저장]이 서버 정본 — 노선 바뀐 [저장]의 계획고 = 저장분 옮김 대 손댐만(`routeGradeTouch` · 서버가 자기 옮김 위에 · 498-2).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDesignPipeline, type DesignPipeline } from "./logic/design_pipeline";
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
createPlanlineStage,
|
||||
gradeRange,
|
||||
routeRange,
|
||||
type GradeTouch,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import { groundPviAt, profileAt } from "./design_calc_profile";
|
||||
import {
|
||||
@@ -127,6 +128,7 @@ interface PagePipeline {
|
||||
detail(): SectionDetailResponse | null;
|
||||
/** 노선 초안 버림 동안(그리기 멈춤 ~ 풂) — 저장 노선 낡음 다시 셈을 안 띄움(472-5 덧). */
|
||||
holding(): boolean;
|
||||
touch(): Promise<GradeTouch | null>;
|
||||
commit(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -137,6 +139,9 @@ type Facilities = Pick<DesignStoreValues, "pipes" | "structures">;
|
||||
const pages = new Map<string, PagePipeline>();
|
||||
const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
|
||||
|
||||
/** 노선 바뀐 [저장]의 계획고 손댐(498-2) — 묶음이 없거나 못 가르면 null(본문 = 노선 0 칸 통째 · 옛 길). */
|
||||
export const routeGradeTouch = async (id: string) => (await pages.get(id)?.touch()) ?? null;
|
||||
|
||||
/** 편집기에 넘길 다리 — 그 프로젝트 묶음을 처음 부를 때 세움. */
|
||||
export function routeDraftBridge(
|
||||
projectId: string,
|
||||
@@ -271,7 +276,12 @@ function createPagePipeline(
|
||||
route.curves,
|
||||
(patch.pipes ?? inputs.pipes ?? []) as PlanPipe[],
|
||||
(patch.structures ?? inputs.structures ?? []) as PlanStructure[],
|
||||
{ intervalM: options.stationIntervalM, types: enabled, ...snapOptions(inputs.route, plan) },
|
||||
{
|
||||
intervalM: options.stationIntervalM,
|
||||
types: enabled,
|
||||
...snapOptions(inputs.route, plan),
|
||||
plus: patch.plusStations ?? inputs.plusStations ?? undefined, // stations 단계와 같게(498-2)
|
||||
},
|
||||
).map((station) => station.chainageM),
|
||||
});
|
||||
/** cross 서버 단계 맥락(511-B B-4) — 저장 노선 · 상세 · 시설 · 편집(`design_pipeline_cross_server`). */
|
||||
@@ -678,6 +688,7 @@ function createPagePipeline(
|
||||
ownsCross: Boolean(options.savedCross),
|
||||
detail: () => draftDetailOf(projectId),
|
||||
holding: () => holding,
|
||||
touch: async () => (await pipe.settled(), followStage.touch(pageSaved.storedEdits(), pipe)),
|
||||
commit: () => void (beforeDraft = null),
|
||||
dispose() {
|
||||
disposed = true;
|
||||
|
||||
@@ -82,6 +82,11 @@ export function createPageSaved(
|
||||
savedSource,
|
||||
canonChanged,
|
||||
savedEdits,
|
||||
/** 서버 저장분 계획고 편집(세션 초안 뺌) — 노선 바뀐 [저장]에서 서버가 옮기는 그 값(498-2). */
|
||||
storedEdits(): AlignmentEdits {
|
||||
const longitudinal = canonDetail()?.longitudinal;
|
||||
return (longitudinal && readAlignment(longitudinal)?.edits) ?? emptyEdits();
|
||||
},
|
||||
storedPvi,
|
||||
crossNow,
|
||||
drafted,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* 노선(346-3 · 6-B) — 노선 초안이 마지막 저장과 다르면 같은 본문에 `route`(계약 `RouteDraft`) · `carry`(옛 노선
|
||||
* 세션 횡단 고침 원본)를 실음. 서버가 맨 앞에서 노선 저장(관 · 구조물 · 추가 측점 좌표 따라감 · 종횡단 · 계획고
|
||||
* 이월) → 관 · 구조물 초안 다시 얹기 → 새 노선으로 위 차례 → 횡단 = 세션 고침을 측점 열쇠로 넘긴 patch(같은 TS ·
|
||||
* Node). 이 때 조각은 기본 조각만(화면 몫 일 = 옛 노선 판이라 건너뜀) · 계획고 초안 = 노선 초안 칸 ?? 옛 노선 칸 ·
|
||||
* Node). 이 때 조각은 기본 조각만(화면 몫 일 = 옛 노선 판이라 건너뜀) · 계획고 = 손댐만(서버 옮김 위에 · 498-2) ·
|
||||
* 손 상단측은 안 보냄(요구 274) · [확정]도 [저장]으로(새 노선을 본 뒤 다시 [확정]) · 다 저장하면 페이지 다시 세움.
|
||||
*
|
||||
* ⚠ 차례를 바꾸면 결과가 갈린다 — 관 목록은 스냅샷이라 구간값(암거 옵션)보다 먼저, 상단측은
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
} from "../../B05_Profile/B05_Profile_UI_RouteEdit_Apply";
|
||||
import type { RouteSaveBody } from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||
import { designSignature, markDesignClean } from "./design_leave";
|
||||
import { routeGradeTouch } from "./design_pipeline_page";
|
||||
import type { GradeTouch } from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
|
||||
export interface DesignSaveContext {
|
||||
projectId: string;
|
||||
@@ -166,10 +168,12 @@ export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] })
|
||||
/** 노선 초안 동안의 계획고 초안 칸 — `design_pipeline_page` 의 `DRAFT_ROUTE_ID` 와 같은 값(sub7). */
|
||||
const DRAFT_ROUTE_ID = 0;
|
||||
|
||||
/** 계획고 조각 — 노선이 바뀌면 노선 초안 칸(`DRAFT_ROUTE_ID`) ?? 옛 노선 칸의 초안(서버가 새 노선 번호로 고쳐
|
||||
* 씀 · 측점이 없는 자리는 버리고 알림 · 213-2) · 저장 뒤 두 칸 다 비움. */
|
||||
function alignmentPart(routeId: number, moved: boolean): SavePart | null {
|
||||
const draft = (moved ? pendingAlignment(DRAFT_ROUTE_ID) : null) ?? pendingAlignment(routeId);
|
||||
/** 계획고 조각 — 노선이 바뀌면 사용자 손댐(`touch` · 서버가 자기 옮김 위에 얹음 · 498-2) · 못 가르면 노선 초안
|
||||
* 칸(`DRAFT_ROUTE_ID`) ?? 옛 노선 칸의 초안 통째(서버가 새 노선 번호로 고쳐 씀) · 측점이 없는 자리는 버리고 알림
|
||||
* (213-2) · 저장 뒤 두 칸 다 비움. */
|
||||
function alignmentPart(routeId: number, moved: boolean, touch: GradeTouch | null): SavePart | null {
|
||||
const draft =
|
||||
touch?.edits ?? (moved ? pendingAlignment(DRAFT_ROUTE_ID) : null) ?? pendingAlignment(routeId);
|
||||
if (!draft) return null;
|
||||
return {
|
||||
body: { route_id: routeId, ...draft },
|
||||
@@ -188,6 +192,7 @@ async function defaultPart(
|
||||
routeId: number,
|
||||
confirm: boolean,
|
||||
moved: boolean,
|
||||
touch: GradeTouch | null,
|
||||
): Promise<SavePart | null> {
|
||||
switch (key) {
|
||||
case "settings": // 노선 뒤 — 노선 저장이 새로 만든 종단에도 경고 기준이 얹힘(448)
|
||||
@@ -199,7 +204,7 @@ async function defaultPart(
|
||||
case "plusStations": // 계획고 앞 — 추가 측점 자리의 계획고 편집이 버려지지 않게(368-2)
|
||||
return plusStationsPart(projectId);
|
||||
case "alignment":
|
||||
return alignmentPart(routeId, moved);
|
||||
return alignmentPart(routeId, moved, touch);
|
||||
case "culvertOptions":
|
||||
return culvertOptionsPart(stateKey("culvertopt", projectId, routeId));
|
||||
case "uphill":
|
||||
@@ -257,12 +262,13 @@ export async function saveDesign(
|
||||
const parts: Partial<Record<SavePartKey, SavePart | null>> = {};
|
||||
// 노선이 바뀌면 횡단 = 옛 노선 세션 고침 원본(서버가 새 측점에 넘김) · 화면 몫 조각은 옛 노선 판이라 안 씀
|
||||
const carried = route ? routeCarryPart(ctx.projectId, routeId) : null;
|
||||
const touch = route ? await routeGradeTouch(ctx.projectId) : null;
|
||||
for (const key of SAVE_PART_KEYS) {
|
||||
if (key === "cross") {
|
||||
parts.cross = carried ? carried.cross : await ctx.crossPart(confirm);
|
||||
continue;
|
||||
}
|
||||
const base = await defaultPart(key, ctx.projectId, routeId, confirm, !!route);
|
||||
const base = await defaultPart(key, ctx.projectId, routeId, confirm, !!route, touch);
|
||||
const own = route ? undefined : ctx.parts?.[key];
|
||||
parts[key] = own ? await own(base, confirm) : base;
|
||||
}
|
||||
@@ -272,7 +278,7 @@ export async function saveDesign(
|
||||
routeId,
|
||||
confirm,
|
||||
parts,
|
||||
route ? { route, carry: carried?.carry } : {},
|
||||
route ? { route, carry: carried?.carry, alignment_cleared: touch?.cleared } : {},
|
||||
);
|
||||
if (result.waited) showToast("앞 저장이 끝나기를 기다린 뒤 저장했습니다.", "info");
|
||||
// 저장 도중 고침(가림막 동안 키 · 되돌리기 포함 · 505-2) — 조각 정리가 그 고침까지 비우므로 건너뜀(서버는 낸
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function postDesignSave(
|
||||
routeId: number,
|
||||
confirm: boolean,
|
||||
parts: Partial<Record<SavePartKey, SavePart | null>>,
|
||||
extra: { route?: unknown; carry?: unknown } = {},
|
||||
extra: { route?: unknown; carry?: unknown; alignment_cleared?: unknown } = {},
|
||||
): Promise<DesignSaveResult> {
|
||||
const body: Record<string, unknown> = { route_id: routeId, confirm, ...extra };
|
||||
for (const key of SAVE_PART_KEYS) {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*
|
||||
* follow — 관 · 구조물 좌표 따라감(`common_util_route_follow`) · 계획고 편집 옮김(`Grade_Carry`
|
||||
* `carryEdits` · 옛 노선 `before.route` 가 있을 때만 · 기준 = 마지막 종단 편집 때 한 벌 · 400) ·
|
||||
* 추가 측점 좌표 따라감(459 · `followPlusGround` · 좌표 들고 감 = 서버 [저장] 자리 · 493-3).
|
||||
* 추가 측점 좌표 따라감(459 · `followPlusGround` · 좌표 들고 감 = 서버 [저장] 자리 · 493-3) ·
|
||||
* 노선 바뀐 [저장]의 계획고 손댐(`touch` · 저장분 옮김과 다른 것만 · 498-2).
|
||||
* planline — 노선 초안이 없으면 저장 종단 기준 그대로 · 있으면 새 측점마다 같은 자리는 저장 꼭짓점
|
||||
* 높이 · 바뀐 자리만 새 지반 첫 계획선(`Grade_Ground` · `carryBase`) · 옮긴 시설 측점(바뀐 자리 ·
|
||||
* 사용자가 옮김)에 427 꼭짓점 다시 세움(459 · 짝 = 파이썬 `B05_Profile_Route_Save_Parts`
|
||||
@@ -15,12 +16,14 @@
|
||||
* 화면 · fetch · 세션 없음(순수) — 저장 기준 · 저장 노선은 부르는 쪽이 넣음.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DesignPipeline } from "../../A00_Common/design/logic/design_pipeline";
|
||||
import type {
|
||||
FollowPatch,
|
||||
PipelineInputs,
|
||||
PipelineStation,
|
||||
RangePart,
|
||||
RouteLine,
|
||||
StageContext,
|
||||
StageFn,
|
||||
Vertex,
|
||||
} from "../../A00_Common/design/logic/design_pipeline_types";
|
||||
@@ -120,11 +123,16 @@ export function createFollowStage(deps: {
|
||||
savedPlus?(): ReadonlyArray<FollowingStructure>;
|
||||
/** 새 노선 측점 누가거리(stations 단계와 같은 셈) — 없으면 일반측점 격자. */
|
||||
stationsAt?(route: RouteLine, patch: FollowPatch, inputs: PipelineInputs): number[];
|
||||
}): StageFn<"follow"> {
|
||||
}): FollowStage {
|
||||
let ref: GradeRef | null = null;
|
||||
let carried: unknown = null; // 마지막으로 낸 편집 — 입력이 이것이면 기준 그대로
|
||||
return (ctx) => {
|
||||
if (!ctx.inputs.route && deps.saved) return deps.saved();
|
||||
/** 이 노선 초안 첫 기준(저장 노선 · 자동 꼭짓점 · 반 간격) — 저장분 옮김을 다시 셈(498-2). */
|
||||
let first: { line: XY[]; basePvi: AlignmentBase["basePvi"]; half: number } | null = null;
|
||||
const stage = (ctx: StageContext<"follow">): FollowPatch => {
|
||||
if (!ctx.inputs.route && deps.saved) {
|
||||
first = null;
|
||||
return deps.saved();
|
||||
}
|
||||
const before = ctx.before.route?.line ?? deps.savedLine();
|
||||
const now = ctx.out.route;
|
||||
if (!before || !now) return {};
|
||||
@@ -163,9 +171,10 @@ export function createFollowStage(deps: {
|
||||
alignment && planline
|
||||
? { line: xy(before), basePvi: planline.basePvi, edits: alignment as AlignmentEdits }
|
||||
: null;
|
||||
const half = (planline?.policy.station_interval_m ?? SECTION_STATION_INTERVAL_M) / 2;
|
||||
if (ref && !first) first = { line: ref.line, basePvi: ref.basePvi, half };
|
||||
if (ref && ctx.before.route) {
|
||||
const span = changedSpan(ref.line, line);
|
||||
const half = (planline?.policy.station_interval_m ?? SECTION_STATION_INTERVAL_M) / 2;
|
||||
const stations = span ? (deps.stationsAt?.(now, patch, ctx.inputs) ?? gridOf(now)) : [];
|
||||
carried = patch.alignment = span
|
||||
? carryEdits(ref.line, line, ref.basePvi, ref.edits, stations, half)
|
||||
@@ -173,6 +182,55 @@ export function createFollowStage(deps: {
|
||||
}
|
||||
return patch;
|
||||
};
|
||||
return Object.assign(stage, {
|
||||
touch(saved: AlignmentEdits, pipe: PipelineReader): GradeTouch | null {
|
||||
const route = pipe.output("route");
|
||||
const now = pipe.input("alignment") as AlignmentEdits | null;
|
||||
if (!first || !route || !now || !pipe.input("route")) return null;
|
||||
const line = xy(route.line);
|
||||
if (!changedSpan(first.line, line)) return gradeTouch(now, saved);
|
||||
const inputs = Object.fromEntries(INPUT_KEYS.map((key) => [key, pipe.input(key)]));
|
||||
const stations =
|
||||
deps.stationsAt?.(route, {}, inputs as unknown as PipelineInputs) ?? gridOf(route);
|
||||
return gradeTouch(
|
||||
now,
|
||||
carryEdits(first.line, line, first.basePvi, saved, stations, first.half),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 묶음 읽기 — 지금 입력 · 출력(`DesignPipeline` 의 읽는 쪽). */
|
||||
type PipelineReader = Pick<DesignPipeline, "input" | "output">;
|
||||
const INPUT_KEYS = ["route", "alignment", "cross", "pipes", "structures", "plusStations"] as const;
|
||||
|
||||
/** follow 단계 + 노선 바뀐 [저장]의 계획고 손댐 — 묶음 계획고 대 이 초안 첫 기준에서 `saved` 를 지금 노선 ·
|
||||
* 측점에 한 번에 옮긴 값(서버 노선 차례 옮김과 같은 셈) · 첫 기준이 없으면(열 때 초안 · 서버 follow) null · 498-2. */
|
||||
export type FollowStage = StageFn<"follow"> & {
|
||||
touch(saved: AlignmentEdits, pipe: PipelineReader): GradeTouch | null;
|
||||
};
|
||||
|
||||
/** 노선 바뀐 [저장]의 계획고 손댐(498-2) — 사용자가 고친 열쇠(값) · 지운 열쇠. */
|
||||
export interface GradeTouch {
|
||||
edits: AlignmentEdits;
|
||||
cleared: { station_offsets: string[]; curve_radii: string[] };
|
||||
}
|
||||
|
||||
/** 묶음 계획고(`now`) 대 저장분 옮김(`carried`) — 다른 값 · 새 열쇠 = 손댐 · 옮김에만 있는 열쇠 = 지움. 서버가 자기
|
||||
* 옮김 위에 이것만 얹음(관 · 구조물 다시 얹기와 같은 꼴 · 498-2). */
|
||||
export function gradeTouch(now: AlignmentEdits, carried: AlignmentEdits): GradeTouch {
|
||||
const out: GradeTouch = {
|
||||
edits: emptyEdits(),
|
||||
cleared: { station_offsets: [], curve_radii: [] },
|
||||
};
|
||||
for (const field of ["station_offsets", "curve_radii"] as const) {
|
||||
const was = carried[field] ?? {};
|
||||
const is = now[field] ?? {};
|
||||
for (const [key, value] of Object.entries(is))
|
||||
if (!(key in was) || Math.abs(was[key] - value) > 1e-6) out.edits[field][key] = value;
|
||||
out.cleared[field] = Object.keys(was).filter((key) => !(key in is));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 같은 꼭짓점 자리(m) — `seatBornNodes` · 파이썬 `_SAME_STATION_M` 과 같은 0.006. */
|
||||
|
||||
@@ -36,7 +36,11 @@ from B05_Profile.B05_Profile_Schema import (
|
||||
)
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureSaveRequest
|
||||
from B06_Section import B06_Section_Router_DesignSave_Progress as progress
|
||||
from B06_Section.B06_Section_Router_DesignSave_Route import RouteCarryRequest, StepFailed
|
||||
from B06_Section.B06_Section_Router_DesignSave_Route import (
|
||||
AlignmentCleared,
|
||||
RouteCarryRequest,
|
||||
StepFailed,
|
||||
)
|
||||
from B06_Section.B06_Section_Router_Stations import PlusStationsRequest
|
||||
from B06_Section.B06_Section_Schema import SectionConfirmRequest
|
||||
from common_util import common_util_save_guard as guard
|
||||
@@ -70,6 +74,9 @@ class DesignSaveRequest(BaseModel):
|
||||
route: RouteSaveRequest | None = None
|
||||
#: 노선 바뀐 저장의 옛 노선 세션 칸(측점 열쇠 넘김 입력)
|
||||
carry: RouteCarryRequest | None = None
|
||||
#: 노선 바뀐 저장의 계획고 지운 열쇠 — 오면 `alignment` = 사용자 손댐(서버 옮김 위에 얹음 ·
|
||||
#: 498-2) · 없으면 `alignment` 통째(옛 길)
|
||||
alignment_cleared: AlignmentCleared | None = None
|
||||
|
||||
|
||||
def _body(response: Any) -> dict[str, Any]:
|
||||
@@ -115,7 +122,6 @@ async def _run_steps(
|
||||
get_pipe_points,
|
||||
put_pipe_points,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router import save_profile_alignment
|
||||
from B05_Profile.B05_Profile_Router_Lifecycle import confirm_latest_route, save_page_settings
|
||||
from B05_Profile.B05_Profile_Structures_Router import write_structures
|
||||
from B06_Section.B06_Section_Router_Confirm import confirm_sections, save_sections
|
||||
@@ -175,6 +181,8 @@ async def _run_steps(
|
||||
# 계획고 초안 = 새 노선에(기하 이월 · 서버가 없는 자리 버림)
|
||||
if request.alignment is not None:
|
||||
request.alignment = request.alignment.model_copy(update={"route_id": route_id})
|
||||
# 노선 바뀜 + 손댐 = 서버 옮김(관 · 구조물 · 추가 측점 차례 뒤 새 종단 편집) 위에 손댐만(498-2)
|
||||
touched = moved and request.alignment_cleared is not None
|
||||
if request.settings is not None:
|
||||
await step("settings", save_page_settings(project_id, request.settings))
|
||||
if request.pipes is not None or request.confirm:
|
||||
@@ -192,7 +200,12 @@ async def _run_steps(
|
||||
if request.plus_stations is not None:
|
||||
await step("plusStations", put_plus_stations(project_id, request.plus_stations))
|
||||
if request.alignment is not None:
|
||||
await step("alignment", save_profile_alignment(project_id, request.alignment))
|
||||
alignment = request.alignment
|
||||
if touched:
|
||||
alignment = moving.touched_alignment(
|
||||
project_id, route_id, alignment, request.alignment_cleared
|
||||
)
|
||||
await step("alignment", _save_alignment(project_id, alignment))
|
||||
if request.culvert_options:
|
||||
stored = await step("culvertOptions", get_pipe_points(project_id))
|
||||
points = _patched_points(stored, request.culvert_options)
|
||||
@@ -241,6 +254,15 @@ async def _run_steps(
|
||||
return out
|
||||
|
||||
|
||||
async def _save_alignment(project_id: UUID, alignment: Any) -> Any:
|
||||
"""계획고 차례 — 본문(또는 본문을 내는 약속 · 노선 바뀐 손댐 얹기 · 498-2)을 저장."""
|
||||
from B05_Profile.B05_Profile_Router import save_profile_alignment
|
||||
|
||||
if not isinstance(alignment, ProfileAlignmentSaveRequest):
|
||||
alignment = await alignment
|
||||
return await save_profile_alignment(project_id, alignment)
|
||||
|
||||
|
||||
async def _save_stages(project_id: str) -> None:
|
||||
"""B05 · B06 한 본문 [저장] — 단계 2 · 3(실패면 한 본문 되돌림이 단계 행도 되살림)."""
|
||||
from common_util.common_util_workflow_state import mark_stage
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
노선 저장(`/route/replan` 체인 · 관 · 구조물 · 추가 측점 좌표 따라감 · 종횡단 · 계획고 ·
|
||||
횡단 이월)
|
||||
→ 관 · 구조물 초안 다시 얹기(Node `facility.rebase` · 서버 따라감 위에)
|
||||
→ 6-A 차례(새 노선) → 횡단 = 옛 노선 세션 칸을 새 측점에 넘긴 patch(Node `stations.carry`)
|
||||
→ 6-A 차례(새 노선 · 계획고 = 서버 옮김 위에 사용자 손댐만 · 498-2)
|
||||
→ 횡단 = 옛 노선 세션 칸을 새 측점에 넘긴 patch(Node `stations.carry`)
|
||||
|
||||
브라우저가 하던 다시 얹기(`rebasePipeDraft` · `carryAfterRouteSave`)와 같은 TS 를 서버가 부름 —
|
||||
브라우저는 노선 초안 + 사용자 칸만 보냄. 손 상단측은 안 넘김(요구 274 · 새 지형이 정함).
|
||||
@@ -36,6 +37,15 @@ class RouteCarryRequest(BaseModel):
|
||||
pipe_ids: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AlignmentCleared(BaseModel):
|
||||
"""노선 바뀐 [저장]에서 사용자가 지운 계획고 열쇠(498-2 · 브라우저 `gradeTouch`)."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
station_offsets: list[str] = Field(default_factory=list)
|
||||
curve_radii: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class StepFailed(Exception):
|
||||
def __init__(self, step: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
@@ -121,3 +131,46 @@ async def carry_cross(
|
||||
**(carry or RouteCarryRequest()).model_dump(mode="json"),
|
||||
}
|
||||
return _node(["stations.carry"], {"carry": inputs}, "cross")
|
||||
|
||||
|
||||
def overlay_edits(
|
||||
carried: dict[str, Any], touch: dict[str, Any], cleared: AlignmentCleared
|
||||
) -> dict[str, dict[str, float]]:
|
||||
"""서버 옮김 편집 위에 사용자 손댐만(498-2) — 지운 · 고친 열쇠 자리(0.006 m 안)의 옮김 값을 걷고
|
||||
손댐 값을 얹음. 손대지 않은 옮김 열쇠는 서버 측점 그대로(브라우저 열쇠로 안 덮음)."""
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import SAME_STATION_SNAP_M
|
||||
|
||||
out: dict[str, dict[str, float]] = {}
|
||||
for field in ("station_offsets", "curve_radii"):
|
||||
merged = {str(key): float(value) for key, value in (carried.get(field) or {}).items()}
|
||||
mine = {str(key): float(value) for key, value in (touch.get(field) or {}).items()}
|
||||
for key in [*getattr(cleared, field), *mine]:
|
||||
for near in [k for k in merged if abs(float(k) - float(key)) < SAME_STATION_SNAP_M]:
|
||||
del merged[near]
|
||||
out[field] = {**merged, **mine}
|
||||
return out
|
||||
|
||||
|
||||
async def touched_alignment(
|
||||
project_id: UUID, route_id: int, touch: Any, cleared: AlignmentCleared
|
||||
) -> Any:
|
||||
"""노선 바뀐 [저장] 계획고 본문(498-2) = 새 종단 편집(노선 차례 서버 옮김) ⊕ 사용자 손댐."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Schema import ProfileAlignmentSaveRequest
|
||||
from B06_Section.B06_Section_Repository import get_longitudinal_section
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
async with get_db_pool().acquire() as connection:
|
||||
row = await get_longitudinal_section(connection, project_id, route_id)
|
||||
stored = await get_project_storage_relative_path(connection, project_id)
|
||||
if not row:
|
||||
raise StepFailed("alignment", "새 노선 종단을 못 읽었습니다")
|
||||
path = Path(resolve_stored_project_path(stored)) / str(row["longitudinal_file_path"])
|
||||
longitudinal = json.loads(path.read_text(encoding="utf-8"))
|
||||
carried = (longitudinal.get("profile_alignment") or {}).get("edits") or {}
|
||||
edits = overlay_edits(carried, touch.edits() if touch is not None else {}, cleared)
|
||||
return ProfileAlignmentSaveRequest(route_id=route_id, **edits)
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/* 498-2 노선 바뀐 [저장] 계획고 손댐 — 진짜 TS(`logic/B05_Profile_Pipeline_Grade` follow · `gradeTouch`)를
|
||||
* Node 로 돌림. 표준출력 JSON 한 벌.
|
||||
*/
|
||||
const path = require("path");
|
||||
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
|
||||
function loadTs(file, stubs = {}) {
|
||||
return makeLoader({
|
||||
root: ROOT,
|
||||
cache: false,
|
||||
requireFor: (full) => (request) => {
|
||||
if (stubs[request]) return stubs[request];
|
||||
throw new Error(`stub 없음: ${request} (${file})`);
|
||||
},
|
||||
}).loadTs(path.join(ROOT, file));
|
||||
}
|
||||
|
||||
const straightJudge = loadTs("B05_Profile/logic/B05_Profile_Profile_Straight.ts");
|
||||
const curveLaw = loadTs("B05_Profile/logic/B05_Profile_Profile_CurveLaw.ts");
|
||||
const pivot = loadTs("B05_Profile/logic/B05_Profile_UI_Profile_Pivot.ts", {
|
||||
"./B05_Profile_Profile_Straight": straightJudge,
|
||||
});
|
||||
const alignment = loadTs("B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts", {
|
||||
"./B05_Profile_UI_Profile_Pivot": pivot,
|
||||
"./B05_Profile_Profile_Straight": straightJudge,
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
});
|
||||
const carry = loadTs("B05_Profile/logic/B05_Profile_Grade_Carry.ts", {
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
});
|
||||
const grade = loadTs("B05_Profile/logic/B05_Profile_Pipeline_Grade.ts", {
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
"../../common_util/common_util_route_follow": {},
|
||||
"../../common_util/common_util_structure_station": {
|
||||
PLUS_OWNER_PREFIX: "plus-",
|
||||
SAME_STATION_M: 0.005,
|
||||
},
|
||||
"./B05_Profile_Grade_Seat": loadTs("B05_Profile/logic/B05_Profile_Grade_Seat.ts", {
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
}),
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
"./B05_Profile_Grade_Carry": carry,
|
||||
"./B05_Profile_Sections_Stations": { SECTION_STATION_INTERVAL_M: 20 },
|
||||
"./B05_Profile_Grade_Ground": loadTs("B05_Profile/logic/B05_Profile_Grade_Ground.ts", {
|
||||
"./B05_Profile_Profile_CurveLaw": curveLaw,
|
||||
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||
}),
|
||||
});
|
||||
|
||||
const routeOf = (line) => {
|
||||
const chainage = [0];
|
||||
for (let i = 1; i < line.length; i += 1)
|
||||
chainage.push(
|
||||
chainage[i - 1] + Math.hypot(line[i][0] - line[i - 1][0], line[i][1] - line[i - 1][1]),
|
||||
);
|
||||
return { line, curves: [], chainage, lengthM: chainage[chainage.length - 1] };
|
||||
};
|
||||
/** 10 m 격자 + 끝 — 묶음 stations 단계 자리. */
|
||||
const gridOf = (route) => {
|
||||
const out = [];
|
||||
for (let s = 0; s < route.lengthM - 1e-6; s += 10) out.push(Math.round(s * 1000) / 1000);
|
||||
return [...out, Math.round(route.lengthM * 1000) / 1000];
|
||||
};
|
||||
|
||||
const SAVED_LINE = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
];
|
||||
const STEPS = [
|
||||
[
|
||||
[0, 0],
|
||||
[95, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
],
|
||||
[
|
||||
[0, 0],
|
||||
[90, 0],
|
||||
[100, 100],
|
||||
[100, 200],
|
||||
],
|
||||
];
|
||||
const BASE = [0, 100, 200, 300].map((chainage_m) => ({ chainage_m, elevation_m: 500 }));
|
||||
const SAVED = { station_offsets: { "250.000": 0.5 }, curve_radii: {} };
|
||||
|
||||
/** 저장 노선에서 노선 걸음 둘 — 걸음 사이 `between` 이 사용자 계획고 조작(묶음 입력 고침). */
|
||||
function run(between, after) {
|
||||
const stage = grade.createFollowStage({
|
||||
savedLine: () => null,
|
||||
stationsAt: (route) => gridOf(route),
|
||||
});
|
||||
const planline = { policy: { station_interval_m: 20 }, basePvi: BASE };
|
||||
let now = SAVED;
|
||||
let before = routeOf(SAVED_LINE);
|
||||
let route = before;
|
||||
STEPS.forEach((line, index) => {
|
||||
route = routeOf(line);
|
||||
const patch = stage({
|
||||
inputs: { route: { nodes: [] }, pipes: null, structures: null, alignment: now },
|
||||
out: { route },
|
||||
before: { route: before, planline },
|
||||
});
|
||||
if (patch.alignment) now = patch.alignment;
|
||||
if (index === 0 && between) now = between(now);
|
||||
before = route;
|
||||
});
|
||||
if (after) now = after(now);
|
||||
const reader = {
|
||||
input: (key) => ({ route: { nodes: [] }, alignment: now })[key] ?? null,
|
||||
output: (key) => (key === "route" ? route : null),
|
||||
};
|
||||
return { now, touch: stage.touch(SAVED, reader) };
|
||||
}
|
||||
|
||||
const bump = (key, by) => (edits) => ({
|
||||
...edits,
|
||||
station_offsets: { ...edits.station_offsets, [key]: (edits.station_offsets[key] ?? 0) + by },
|
||||
});
|
||||
const drop = (edits) => {
|
||||
const offsets = { ...edits.station_offsets };
|
||||
delete offsets[Object.keys(offsets).sort()[0]];
|
||||
return { ...edits, station_offsets: offsets };
|
||||
};
|
||||
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
plain: run(null, null),
|
||||
lateBump: run(null, bump("60.000", 0.3)),
|
||||
midBump: run(bump("40.000", 0.2), null),
|
||||
removed: run(null, drop),
|
||||
noFirst: grade.createFollowStage({ savedLine: () => null }).touch(SAVED, {
|
||||
input: () => null,
|
||||
output: () => null,
|
||||
}),
|
||||
pure: grade.gradeTouch(
|
||||
{ station_offsets: { "10.000": 1, "20.000": 2.5 }, curve_radii: { "30.000": 80 } },
|
||||
{ station_offsets: { "10.000": 1, "20.000": 2, "40.000": 3 }, curve_radii: {} },
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -119,6 +119,8 @@ const STUBS = {
|
||||
// 472-6 한 본문 창구 — 보낸 칸만 적음 · 구조물 실패 = 서버가 그 차례에서 되돌림
|
||||
"./design_save_api": saveApiStub,
|
||||
"./design_leave": { designSignature: () => "", markDesignClean: () => undefined },
|
||||
// 498-2 노선 바뀐 [저장] 계획고 손댐 — 묶음 없음 = 노선 0 칸 통째(옛 길)
|
||||
"./design_pipeline_page": { routeGradeTouch: async () => null },
|
||||
};
|
||||
|
||||
const { loadTs } = makeLoader({
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""498-2 노선 바뀐 [저장] 계획고 — 브라우저는 손댐(입력)만 · 서버가 자기 옮김 위에 한 번.
|
||||
|
||||
- 브라우저(`B05_Profile_Pipeline_Grade` follow `touch` · `gradeTouch`) — 노선만 끌었으면 손댐 0 ·
|
||||
끈 뒤 ▲ = 그 열쇠만 · 끄는 중 ▲ = 그 열쇠 포함 묶음 값 그대로 · 지운 열쇠 = `cleared` ·
|
||||
첫 기준 없으면 null(옛 길 · 0 칸 통째).
|
||||
- 서버(`B06_Section_Router_DesignSave_Route.overlay_edits` · `touched_alignment`) — 손대지 않은 옮김
|
||||
열쇠는 서버 측점 그대로 · 고친 · 지운 열쇠 자리(0.006 m 안)만 걷고 손댐 값을 얹음 · 브라우저 측점
|
||||
열쇠로 서버 열쇠를 덮지 않음(옛 흠 = 두 쪽 측점이 다르면 어긋난 편집을 지움).
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B06_Section import B06_Section_Router_DesignSave_Route as moving
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts() -> dict:
|
||||
proc = subprocess.run(
|
||||
["node", str(HERE / "helper_498_2_grade_touch.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def _empty(touch: dict) -> bool:
|
||||
return not any(touch["edits"].values()) and not any(touch["cleared"].values())
|
||||
|
||||
|
||||
def test_노선만_끌면_손댐_0(ts):
|
||||
assert ts["plain"]["now"]["station_offsets"], "옮긴 편집이 있어야 시험이 뜻을 가짐"
|
||||
assert _empty(ts["plain"]["touch"])
|
||||
|
||||
|
||||
def test_끈_뒤_올림은_그_열쇠만(ts):
|
||||
touch = ts["lateBump"]["touch"]
|
||||
assert touch["edits"]["station_offsets"] == {"60.000": 0.3}
|
||||
assert not any(touch["cleared"].values())
|
||||
|
||||
|
||||
def test_끄는_중_올림은_묶음_값_그대로(ts):
|
||||
now, touch = ts["midBump"]["now"], ts["midBump"]["touch"]
|
||||
assert touch["edits"]["station_offsets"]["40.000"] == pytest.approx(0.2)
|
||||
for key, value in touch["edits"]["station_offsets"].items():
|
||||
assert now["station_offsets"][key] == value
|
||||
|
||||
|
||||
def test_지운_열쇠는_cleared(ts):
|
||||
touch = ts["removed"]["touch"]
|
||||
assert touch["cleared"]["station_offsets"] == ["200.000"]
|
||||
assert not any(touch["edits"].values())
|
||||
|
||||
|
||||
def test_첫_기준_없으면_null_옛_길(ts):
|
||||
assert ts["noFirst"] is None
|
||||
|
||||
|
||||
def test_gradeTouch_순수(ts):
|
||||
assert ts["pure"] == {
|
||||
"edits": {"station_offsets": {"20.000": 2.5}, "curve_radii": {"30.000": 80}},
|
||||
"cleared": {"station_offsets": ["40.000"], "curve_radii": []},
|
||||
}
|
||||
|
||||
|
||||
def test_서버_옮김_위에_손댐만():
|
||||
carried = {
|
||||
"station_offsets": {"960.000": 0.005, "970.000": 0.64, "980.000": 0.014},
|
||||
"curve_radii": {"500.000": 120.0},
|
||||
}
|
||||
touch = {"station_offsets": {"970.003": 0.7, "1000.000": 0.2}, "curve_radii": {}}
|
||||
cleared = moving.AlignmentCleared(station_offsets=["980.000"], curve_radii=["500.004"])
|
||||
assert moving.overlay_edits(carried, touch, cleared) == {
|
||||
# 960 = 손 안 댐(서버 값) · 970 = 0.006 m 안 손댐이 갈음 · 980 지움 · 1000 새 손댐
|
||||
"station_offsets": {"960.000": 0.005, "970.003": 0.7, "1000.000": 0.2},
|
||||
"curve_radii": {},
|
||||
}
|
||||
|
||||
|
||||
def test_손댐_0이면_서버_옮김_그대로():
|
||||
carried = {"station_offsets": {"960.000": 0.005, "970.000": 0.64}, "curve_radii": {}}
|
||||
out = moving.overlay_edits(carried, {}, moving.AlignmentCleared())
|
||||
assert out == {"station_offsets": carried["station_offsets"], "curve_radii": {}}
|
||||
|
||||
|
||||
def test_계획고_본문은_새_종단_편집에서(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
from B03_FileInput import B03_FileInput_Repository as files
|
||||
from B05_Profile.B05_Profile_Schema import ProfileAlignmentSaveRequest
|
||||
from B06_Section import B06_Section_Repository as repo
|
||||
from common_util import common_util_storage as storage
|
||||
from config import config_db
|
||||
|
||||
(tmp_path / "long.json").write_text(
|
||||
json.dumps({"profile_alignment": {"edits": {"station_offsets": {"970.000": 0.64}}}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
class Pool:
|
||||
def acquire(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
return None
|
||||
|
||||
async def __aexit__(self, *_):
|
||||
return False
|
||||
|
||||
async def longitudinal(_connection, _project, route_id):
|
||||
assert route_id == 820, "새 노선 번호로 읽어야 함"
|
||||
return {"longitudinal_file_path": "long.json"}
|
||||
|
||||
async def stored(_connection, _project):
|
||||
return "p"
|
||||
|
||||
monkeypatch.setattr(config_db, "get_db_pool", lambda: Pool())
|
||||
monkeypatch.setattr(repo, "get_longitudinal_section", longitudinal)
|
||||
monkeypatch.setattr(files, "get_project_storage_relative_path", stored)
|
||||
monkeypatch.setattr(storage, "resolve_stored_project_path", lambda _p: str(tmp_path))
|
||||
touch = ProfileAlignmentSaveRequest(route_id=819, station_offsets={"1000.000": 0.2})
|
||||
body = asyncio.run(moving.touched_alignment("pid", 820, touch, moving.AlignmentCleared()))
|
||||
assert body.route_id == 820
|
||||
assert body.station_offsets == {"970.000": 0.64, "1000.000": 0.2}
|
||||
Reference in New Issue
Block a user