Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/* 「계산 중」 점(386-2) — 단추 묶음 위 경계선 오른쪽 끝 · 묶음 차례가 떠 있는 동안만. */
|
||||
.design-dock {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.b05-route__dock.ui-sidebar-actions > .design-dock__busy {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
animation: design-dock-busy 0.8s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.b05-route__dock.ui-sidebar-actions > .design-dock__busy[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes design-dock-busy {
|
||||
from {
|
||||
opacity: 0.35;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.b05-route__dock.ui-sidebar-actions > .design-dock__busy {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface DesignDock {
|
||||
setActions: (actions: Partial<DesignDockActions>) => void;
|
||||
/** 보기가 바뀌면 [보기 전환] 글자가 따라 바뀐다. */
|
||||
setView: (view: DesignView) => void;
|
||||
/** 「계산 중」 점 — 묶음 차례가 떠 있는 동안 켬(386-2). */
|
||||
setBusy: (busy: boolean) => void;
|
||||
}
|
||||
|
||||
function label(key: keyof typeof ui_locales): string {
|
||||
@@ -60,7 +62,13 @@ export function createDesignDock(
|
||||
let actions = { ...initial };
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.className = "b05-route__dock ui-sidebar-actions";
|
||||
root.className = "b05-route__dock ui-sidebar-actions design-dock";
|
||||
const busy = document.createElement("span");
|
||||
busy.className = "design-dock__busy";
|
||||
busy.title = label("Design_Busy");
|
||||
busy.setAttribute("role", "status");
|
||||
busy.setAttribute("aria-label", label("Design_Busy"));
|
||||
busy.hidden = true;
|
||||
const divider = document.createElement("hr");
|
||||
divider.className = "b05-structure__divider";
|
||||
const row = document.createElement("div");
|
||||
@@ -114,7 +122,7 @@ export function createDesignDock(
|
||||
};
|
||||
|
||||
row.append(resetButton, saveButton, confirmButton, switchButton);
|
||||
root.append(...above, divider, row);
|
||||
root.append(...above, divider, row, busy);
|
||||
sync();
|
||||
|
||||
return {
|
||||
@@ -127,5 +135,8 @@ export function createDesignDock(
|
||||
current = next;
|
||||
sync();
|
||||
},
|
||||
setBusy: (next) => {
|
||||
busy.hidden = !next;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { usePipeIds } from "./design_pipe_ids";
|
||||
import { pinDraftProject } from "./design_store";
|
||||
import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center";
|
||||
import { reconcilePagePipeline } from "./design_pipeline_page";
|
||||
import { forgetPageData, onPageBusy } from "./design_page_data";
|
||||
import "./design_page.css";
|
||||
|
||||
export type { DesignPageShared } from "./design_shared";
|
||||
@@ -145,11 +146,14 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
};
|
||||
guardDesignLeave(projectId, page, () => saveSharedDesign(shared, false)); // 떠남 확인(346-4)
|
||||
setRouteShiftOpener(() => shared.ensureView("profile"), showToast); // 중심점 좌우 = 편집기 숨긴 채(356-3)
|
||||
let busy = false; // 「계산 중」 점 — 좌측 패널이 늦게 서도 지금 상태로(386-2)
|
||||
const stopBusy = onPageBusy(projectId, (now) => shared.panel?.dock.setBusy((busy = now)));
|
||||
|
||||
/** 한 벌이 보기를 따라간다 — 좌측 칸 · 단추 글자 · 종단도 판 · 진행 단계 · 제목. */
|
||||
const applyView = (view: DesignView): void => {
|
||||
main.dataset.view = view;
|
||||
shared.panel?.setView(view);
|
||||
shared.panel?.dock.setBusy(busy);
|
||||
shared.profile?.setView(view);
|
||||
layout?.setTitle(ui_locales[VIEW_TITLE[view]][currentLanguageIndex]);
|
||||
layout?.setActiveStep(VIEW_STEP[view]);
|
||||
@@ -269,6 +273,8 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
slots.forEach((slot) => slot.dispose());
|
||||
slots.clear();
|
||||
shared.profile?.dispose();
|
||||
stopBusy();
|
||||
forgetPageData(projectId); // 다시 열면 저장분 새로(386-2)
|
||||
delete (window as unknown as { __designPage?: unknown }).__designPage;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_page_data.ts
|
||||
* B05 · B06 한 페이지 자료 줄(PLAN 386-2) — 저장분(구조물 · 관 · 정의 판 확인)을 페이지에서 한 번만 받음.
|
||||
*
|
||||
* 받는 쪽 = 묶음(`design_pipeline_page`) · 종단 보기 구조물 칸 · 횡단 보기 구조물 칸 · 초안 측점
|
||||
* (`B06_Section_Section_Drafts`) · [저장] 관 얹기(`design_save`) — 보기 전환 · 다시 보임 = 요청 0.
|
||||
* 다시 받기 = 저장 정본이 바뀐 뒤 한 번 — `invalidateSectionDetail` → `forgetDraftStations` → 여기 비움
|
||||
* ([저장] · [확정] · [초기화] · 노선 [적용] · 파일 다시 올림) · 페이지를 떠나면 비움.
|
||||
* 종류 목록은 `fetchStructureTypes` 가 탭 수명 동안 쥠 — 정의 판 확인만 페이지마다 한 번.
|
||||
* 돌려줄 때마다 사본 — 받는 쪽이 고쳐도 다른 쪽 저장분이 안 바뀜.
|
||||
* 「계산 중」 — 묶음 차례가 떠 있는 동안 좌측 아래 점(`onPageBusy`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { fetchDetailPipePoints } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
dropStaleStructureTypes,
|
||||
fetchStructures,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
|
||||
const memo = new Map<string, Promise<unknown>>();
|
||||
|
||||
function once<T>(key: string, load: () => Promise<T>): Promise<T> {
|
||||
let hit = memo.get(key) as Promise<T> | undefined;
|
||||
if (!hit) {
|
||||
hit = load();
|
||||
memo.set(key, hit);
|
||||
hit.catch(() => memo.get(key) === hit && memo.delete(key)); // 실패는 다음에 다시 물음
|
||||
}
|
||||
return hit.then((value) => structuredClone(value));
|
||||
}
|
||||
|
||||
/** 저장 구조물(structures.json · 판번호 · 정의 판). */
|
||||
export const pageStructures = (projectId: string): ReturnType<typeof fetchStructures> =>
|
||||
once(`structures:${projectId}`, () => fetchStructures(projectId));
|
||||
|
||||
/** 저장 관(pipe_points.json · 유역). */
|
||||
export const pagePipes = (projectId: string): ReturnType<typeof fetchDetailPipePoints> =>
|
||||
once(`pipes:${projectId}`, () => fetchDetailPipePoints(projectId));
|
||||
|
||||
/** 정의 판이 바뀌었으면 종류 캐시 버림 — 페이지마다 한 번(탭이 다시 보일 때는 `onStaleStructureTypes`). */
|
||||
export const pageTypesChecked = (projectId: string): Promise<boolean> =>
|
||||
once(`types:${projectId}`, () => dropStaleStructureTypes());
|
||||
|
||||
/** 저장 정본이 바뀜 · 페이지를 떠남 — 다음에 부르면 한 번 다시 받음. */
|
||||
export function forgetPageData(projectId: string): void {
|
||||
for (const key of [...memo.keys()]) if (key.endsWith(`:${projectId}`)) memo.delete(key);
|
||||
}
|
||||
|
||||
const busy = new Map<string, number>();
|
||||
const busyListeners = new Map<string, Set<(busy: boolean) => void>>();
|
||||
|
||||
const notify = (projectId: string): void => {
|
||||
const now = (busy.get(projectId) ?? 0) > 0;
|
||||
busyListeners.get(projectId)?.forEach((fn) => fn(now));
|
||||
};
|
||||
|
||||
/** 묶음 차례 하나가 뜸 — 끝나면(밀림 · 실패 포함) 내림. */
|
||||
export function trackPageBusy(projectId: string, run: Promise<unknown> | null): void {
|
||||
if (!run) return;
|
||||
busy.set(projectId, (busy.get(projectId) ?? 0) + 1);
|
||||
notify(projectId);
|
||||
const done = (): void => {
|
||||
busy.set(projectId, Math.max(0, (busy.get(projectId) ?? 1) - 1));
|
||||
notify(projectId);
|
||||
};
|
||||
run.then(done, done);
|
||||
}
|
||||
|
||||
/** 「계산 중」 켬 · 끔 듣기 — 떼는 함수를 돌려줌. */
|
||||
export function onPageBusy(projectId: string, fn: (busy: boolean) => void): () => void {
|
||||
const set = busyListeners.get(projectId) ?? new Set();
|
||||
busyListeners.set(projectId, set.add(fn));
|
||||
fn((busy.get(projectId) ?? 0) > 0);
|
||||
return () => set.delete(fn);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_base.ts
|
||||
* 묶음 planline 의 저장 노선 종단 기준(PLAN 386-3) — 측점 한 벌을 묶음 입력(관 · 구조물)으로 셈.
|
||||
*
|
||||
* 종단도 `listed()`(design_profile)와 같은 셈 — 저장 상세 측점 + 구조물 · 관이 심는 측점(`listedBase`).
|
||||
* 심는 측점 = 종단 보기 구조물 칸(`syncCrossDrainStations`) · 횡단 보기(`planMarkStations`)와 같은
|
||||
* `planStations` → `plannedIrregular` — 보기가 종단도에 늦게 심어도 묶음은 다시 안 돎(옛 `baseMoved` 지움).
|
||||
* 종류 = 구조물 정의 전부(보기와 같음). 화면 모듈을 끌어 묶음 대신 페이지(`design_shared`)가 옵션으로 넘김.
|
||||
* ========================================================================== */
|
||||
|
||||
import { listedBase } from "./design_profile_base";
|
||||
import { plannedIrregular } from "./design_structure_status";
|
||||
import { withPipeIds } from "./design_pipe_ids";
|
||||
import { planStations, type PlanType } from "../../common_util/common_util_structure_station";
|
||||
import type { PipelineInputs } from "./logic/design_pipeline_types";
|
||||
import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
|
||||
import {
|
||||
toAlignmentBase,
|
||||
type AlignmentBase,
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
|
||||
/** 저장 상세(종단도가 그리는 것) + 지금 입력이 심는 측점 = 저장 노선 종단 기준. 상세가 없으면 종단도 기준. */
|
||||
export function plantedBase(
|
||||
source: { base: AlignmentBase | null; detail?: SectionDetailResponse | null },
|
||||
inputs: Pick<PipelineInputs, "pipes" | "structures">,
|
||||
types: ReadonlyArray<PlanType>,
|
||||
): AlignmentBase | null {
|
||||
const detail = source.detail ?? null;
|
||||
const stored = detail && readAlignment(detail.longitudinal);
|
||||
if (!detail || !stored) return source.base;
|
||||
const pipes = withPipeIds((inputs.pipes ?? []) as Array<{ chainage_m: number }>);
|
||||
const structures = (inputs.structures ?? []) as Parameters<typeof planStations>[1];
|
||||
const planted = plannedIrregular(planStations(pipes, structures, types), 20); // 간격 = 이름 몫 · 기준 무관
|
||||
return listedBase(toAlignmentBase(stored), detail, planted);
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline
|
||||
import { createStationsStage, stationDiff } from "./logic/design_pipeline_stations";
|
||||
import type {
|
||||
ChangeSource,
|
||||
CrossOutput,
|
||||
FollowPatch,
|
||||
PipelineInputs,
|
||||
PipelineOutputs,
|
||||
@@ -38,7 +37,6 @@ import type {
|
||||
PipelineStation,
|
||||
RangePart,
|
||||
RouteDraft,
|
||||
StageFn,
|
||||
Vertex,
|
||||
} from "./logic/design_pipeline_types";
|
||||
import { useDesignStore, type DesignStoreValues } from "./design_store";
|
||||
@@ -47,13 +45,11 @@ import {
|
||||
placedOnLine as placed,
|
||||
type FollowingStructure,
|
||||
} from "../../common_util/common_util_route_follow";
|
||||
import { fetchDetailPipePoints } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createCrossStage } from "../../B06_Section/logic/B06_Section_Cross_Stage";
|
||||
import {
|
||||
emptyEdits,
|
||||
toAlignmentBase,
|
||||
type AlignmentBase,
|
||||
type AlignmentEdits,
|
||||
type ProfileAlignment,
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
@@ -69,7 +65,10 @@ import {
|
||||
type RoutePlanResponse,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||
import { createHaulStage } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
|
||||
import { fetchStructures, fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
|
||||
import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
||||
export type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
||||
import {
|
||||
groundStageDeps,
|
||||
loadDesignTerrainGrid,
|
||||
@@ -90,7 +89,6 @@ import {
|
||||
crossWithBerm,
|
||||
crossWithUphill,
|
||||
draftSectionDetail,
|
||||
type BermAt,
|
||||
type CrossInput,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import { buildStations } from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
|
||||
@@ -114,52 +112,6 @@ export const DRAFT_ROUTE_ID = 0;
|
||||
/** 입력 ↔ 묶음 입력 맞춤 상한(바퀴) — 넘으면 「계산 불안정」(365-2). */
|
||||
export const RECONCILE_ROUNDS = 3;
|
||||
|
||||
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
||||
export interface ProfileLink {
|
||||
/** 저장 노선의 종단 기준(측점 한 벌) · 지금 계획고 편집 · 저장 상세 · 노선 번호. */
|
||||
pipelineSource(): {
|
||||
base: AlignmentBase | null;
|
||||
edits: AlignmentEdits;
|
||||
detail?: SectionDetailResponse | null;
|
||||
routeId?: number | null;
|
||||
};
|
||||
/** 노선 초안으로 셈한 첫 계획선 · 옮긴 계획고를 그림. */
|
||||
adoptPipeline(base: AlignmentBase, edits: AlignmentEdits, draft: boolean): void;
|
||||
/** 새 노선 횡단이 왔음 — 다시 그림(유토곡선). */
|
||||
redraw?(): void;
|
||||
/** 배수유역 판 — drainage 단계. */
|
||||
side?: { pipelineStage: StageFn<"drainage"> } | null;
|
||||
}
|
||||
|
||||
export interface PagePipelineOptions {
|
||||
stationIntervalM?: number;
|
||||
/** 노선 초안 동안 종단 · 횡단이 바뀜 — 3D 「낡음」. */
|
||||
onCorridorStale?: () => void;
|
||||
/** 노선 조작 뒤 follow 가 가게 관 · 구조물을 다시 씀(목록 · 알약 다시) — `far` = 새 노선에서
|
||||
* `STRUCTURE_ROUTE_DRIFT_MAX_M` 넘게 먼 구조물 수(초안을 버려 되돌렸으면 0). */
|
||||
onFacilitiesFollowed?: (far: number) => void;
|
||||
/** 새 노선 상세에 관 · 구조물 측점 폐회로 면적을 얹음(상세 가게 · 서버와 같은 얹기 · 349-0j) — 화면
|
||||
* 부품 계산이라 페이지가 넘김(묶음은 안 끌어옴). */
|
||||
structureAreas?: (detail: SectionDetailResponse) => void;
|
||||
/** 노선 초안 시작(349-0o) — 초안 전 상세 사본(`saved` · 없으면 null · 초안 둔 채 새로고침)을 맡김 · 돌려준
|
||||
* 함수 = 초안을 버린 차례 끝에 부름(`stations` = 묶음 저장 노선 측점) · 그 값 = 되돌림 길 한 단어(묶음 기록
|
||||
* `restoreWhy`). 끝난 뒤 종단도가 저장 기준으로 돌아옴. */
|
||||
holdDraft?: (
|
||||
routeId: number,
|
||||
saved: SectionDetailResponse | null,
|
||||
) => (stations: PipelineStation[]) => Promise<string>;
|
||||
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||
onUnstable?: () => void;
|
||||
/** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */
|
||||
bermAt?: (structures: unknown) => BermAt;
|
||||
/** 저장 노선 cross = 다시 셈 창구 한 벌(365-4 · 측점이 상세와 다르면 null = 엔진) · 주면 종단도는 따로 안 셈. */
|
||||
savedCross?: (
|
||||
d: SectionDetailResponse,
|
||||
routeId: number,
|
||||
s: PipelineStation[],
|
||||
) => Promise<CrossOutput | null>;
|
||||
}
|
||||
|
||||
interface PagePipeline {
|
||||
pipe: DesignPipeline;
|
||||
bridge: RouteDraftBridge;
|
||||
@@ -234,7 +186,8 @@ function createPagePipeline(
|
||||
/** 노선 0 칸 — 종단도 편집 가게(`createProfileEditStore`)와 같은 자리(지금 프로젝트). */
|
||||
const draftAlignment = useDesignStore(null, DRAFT_ROUTE_ID);
|
||||
let plan: RoutePlanResponse | null = null;
|
||||
let types: PlanType[] = [];
|
||||
let types: PlanType[] = []; // 정의 전부(심는 측점) · 측점 열쇠 단계 = 켜진 것만
|
||||
let enabled: PlanType[] = [];
|
||||
let savedStructures: PlanStructure[] = [];
|
||||
let savedPipes: PlanPipe[] = [];
|
||||
let earthwork: MassHaulStageContext | null = null;
|
||||
@@ -308,8 +261,6 @@ function createPagePipeline(
|
||||
const stored = canon?.detail && readAlignment(canon.detail.longitudinal);
|
||||
return stored ? toAlignmentBase(stored).basePvi : null;
|
||||
};
|
||||
const baseMoved = () =>
|
||||
!dropping && !pipe.input("route") && !same(savedSource().base, pipe.output("planline"));
|
||||
/** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */
|
||||
const crossNow = (): PipelineCrossSource | null => {
|
||||
const routeId = canon?.routeId;
|
||||
@@ -357,9 +308,13 @@ function createPagePipeline(
|
||||
};
|
||||
return followStage({ ...ctx, inputs });
|
||||
},
|
||||
stations: createStationsStage({ intervalM: options.stationIntervalM, types: () => types }),
|
||||
stations: createStationsStage({ intervalM: options.stationIntervalM, types: () => enabled }),
|
||||
ground: createGroundStage(groundDeps),
|
||||
planline: createPlanlineStage({ savedBase: () => savedSource().base, savedLine, storedPvi }),
|
||||
planline: createPlanlineStage({
|
||||
savedBase: (at) => options.plantedBase?.(savedSource(), at, types) ?? savedSource().base,
|
||||
savedLine,
|
||||
storedPvi,
|
||||
}),
|
||||
profile: profileStage,
|
||||
// 단면 유형 = 새 지형 상단측 · 소단 = 지금 구조물(365-5) · 저장 노선 = 다시 셈 창구 한 벌(365-4) · 엔진은
|
||||
// 창구 출력을 앞 결과로 안 이어 씀(초안 = 새로고침)
|
||||
@@ -557,6 +512,7 @@ function createPagePipeline(
|
||||
dropping = null;
|
||||
Object.assign(given, inputs);
|
||||
const run = (lastRun = pipe.start(inputs));
|
||||
trackPageBusy(projectId, run); // 「계산 중」 점(386-2)
|
||||
openGeneration = pipe.generation;
|
||||
quietGeneration = inputs.route ? -1 : pipe.generation;
|
||||
void run.then(() => reconcile()); // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤
|
||||
@@ -568,7 +524,7 @@ function createPagePipeline(
|
||||
const feed = (source: ChangeSource, value: unknown, part: RangePart | null = null) => {
|
||||
given[SOURCE_INPUT[source]] = value as never;
|
||||
const run = pipe.update(source, value, part);
|
||||
if (run) lastRun = run;
|
||||
if (run) trackPageBusy(projectId, (lastRun = run));
|
||||
return run;
|
||||
};
|
||||
let reconciling: Promise<number> | null = null;
|
||||
@@ -597,35 +553,34 @@ function createPagePipeline(
|
||||
})().finally(() => (reconciling = null)));
|
||||
}
|
||||
|
||||
/** 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — [저장] 뒤 다시 받음(`restart`). */
|
||||
/** 저장분(자료 줄 한 번 · 386-2) · 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — 정본이 바뀌면 다시. */
|
||||
const loadPlan = (): Promise<void> =>
|
||||
fetchRoutePlan(projectId)
|
||||
.then((routePlan) => {
|
||||
Promise.all([
|
||||
fetchStructureTypes(true).catch(() => []),
|
||||
pageStructures(projectId).catch(() => null),
|
||||
pagePipes(projectId).catch(() => null),
|
||||
fetchRoutePlan(projectId),
|
||||
])
|
||||
.then(([structureTypes, list, pipes, routePlan]) => {
|
||||
types = structureTypes as PlanType[];
|
||||
enabled = types.filter((type) => (type as { enabled?: boolean }).enabled !== false);
|
||||
savedStructures = (list?.structures ?? []) as PlanStructure[];
|
||||
savedPipes = (pipes?.pipe_points ?? []).map(({ lonlat: _lonlat, ...pipe }) => pipe);
|
||||
plan = routePlan;
|
||||
savedStations = buildStations(savedLine()!, savedCurves(), savedPipes, savedStructures, {
|
||||
intervalM: options.stationIntervalM,
|
||||
types,
|
||||
types: enabled,
|
||||
stationSnap: routePlan.station_snap ?? false,
|
||||
});
|
||||
crossSource = null;
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
void Promise.all([
|
||||
fetchStructureTypes().catch(() => []),
|
||||
fetchStructures(projectId).catch(() => null),
|
||||
fetchDetailPipePoints(projectId).catch(() => null),
|
||||
loadDesignTerrainGrid(projectId).catch(() => null), // 지형 격자(없으면 서버 길)
|
||||
])
|
||||
.then(([structureTypes, list, pipes]) => {
|
||||
types = structureTypes as PlanType[];
|
||||
savedStructures = (list?.structures ?? []) as PlanStructure[];
|
||||
savedPipes = (pipes?.pipe_points ?? []).map(({ lonlat: _lonlat, ...pipe }) => pipe);
|
||||
return loadPlan();
|
||||
})
|
||||
// 지형 격자(없으면 서버 길) · 열 때 한 번 — 발자국 · 저장 안 한 노선 초안이 있으면 그 노선으로
|
||||
void Promise.all([loadPlan(), loadDesignTerrainGrid(projectId).catch(() => null)])
|
||||
.then(() => {
|
||||
ready = plan !== null;
|
||||
start(); // 열 때 한 번 — 발자국 · 저장 안 한 노선 초안이 있으면 그 노선으로
|
||||
start();
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
@@ -674,8 +629,8 @@ function createPagePipeline(
|
||||
},
|
||||
restart() {
|
||||
if (!started) return start();
|
||||
if (canonChanged()) return void loadPlan().then(start); // 갈아끼움 · 같은 상세 다시 그림은 안 셈
|
||||
if (baseMoved()) start(); // 저장 노선 기준이 바뀜(보기가 늦게 심은 측점 · 되돌린 뒤 · 초안 빼고 · 365-5)
|
||||
// 갈아끼움 · 같은 상세 다시 그림은 안 셈 · 보기가 심은 측점은 묶음이 입력으로 셈(386-3)
|
||||
if (canonChanged()) void loadPlan().then(start);
|
||||
},
|
||||
earthwork(context) {
|
||||
earthwork = context;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_pipeline_page_types.ts
|
||||
* 페이지 계산 묶음(`design_pipeline_page`)이 받는 것 — 종단도 고리 · 페이지 옵션(700줄 쪼갬 · 386-3).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { PlanType } from "../../common_util/common_util_structure_station";
|
||||
import type {
|
||||
CrossOutput,
|
||||
PipelineInputs,
|
||||
PipelineStation,
|
||||
StageFn,
|
||||
} from "./logic/design_pipeline_types";
|
||||
import type {
|
||||
AlignmentBase,
|
||||
AlignmentEdits,
|
||||
} from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
import type { BermAt } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
|
||||
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
||||
export interface ProfileLink {
|
||||
/** 저장 노선의 종단 기준(측점 한 벌) · 지금 계획고 편집 · 저장 상세 · 노선 번호. */
|
||||
pipelineSource(): {
|
||||
base: AlignmentBase | null;
|
||||
edits: AlignmentEdits;
|
||||
detail?: SectionDetailResponse | null;
|
||||
routeId?: number | null;
|
||||
};
|
||||
/** 노선 초안으로 셈한 첫 계획선 · 옮긴 계획고를 그림. */
|
||||
adoptPipeline(base: AlignmentBase, edits: AlignmentEdits, draft: boolean): void;
|
||||
/** 새 노선 횡단이 왔음 — 다시 그림(유토곡선). */
|
||||
redraw?(): void;
|
||||
/** 배수유역 판 — drainage 단계. */
|
||||
side?: { pipelineStage: StageFn<"drainage"> } | null;
|
||||
}
|
||||
|
||||
export interface PagePipelineOptions {
|
||||
stationIntervalM?: number;
|
||||
/** 노선 초안 동안 종단 · 횡단이 바뀜 — 3D 「낡음」. */
|
||||
onCorridorStale?: () => void;
|
||||
/** 노선 조작 뒤 follow 가 가게 관 · 구조물을 다시 씀(목록 · 알약 다시) — `far` = 새 노선에서
|
||||
* `STRUCTURE_ROUTE_DRIFT_MAX_M` 넘게 먼 구조물 수(초안을 버려 되돌렸으면 0). */
|
||||
onFacilitiesFollowed?: (far: number) => void;
|
||||
/** 새 노선 상세에 관 · 구조물 측점 폐회로 면적을 얹음(상세 가게 · 서버와 같은 얹기 · 349-0j) — 화면
|
||||
* 부품 계산이라 페이지가 넘김(묶음은 안 끌어옴). */
|
||||
structureAreas?: (detail: SectionDetailResponse) => void;
|
||||
/** 노선 초안 시작(349-0o) — 초안 전 상세 사본(`saved` · 없으면 null · 초안 둔 채 새로고침)을 맡김 · 돌려준
|
||||
* 함수 = 초안을 버린 차례 끝에 부름(`stations` = 묶음 저장 노선 측점) · 그 값 = 되돌림 길 한 단어(묶음 기록
|
||||
* `restoreWhy`). 끝난 뒤 종단도가 저장 기준으로 돌아옴. */
|
||||
holdDraft?: (
|
||||
routeId: number,
|
||||
saved: SectionDetailResponse | null,
|
||||
) => (stations: PipelineStation[]) => Promise<string>;
|
||||
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||
onUnstable?: () => void;
|
||||
/** 소단 구간(지금 구조물 목록 → 누가거리마다 제원) — 초안 cross 가 따라간 구조물로 소단을 셈(365-5). */
|
||||
bermAt?: (structures: unknown) => BermAt;
|
||||
/** 저장 노선 cross = 다시 셈 창구 한 벌(365-4 · 측점이 상세와 다르면 null = 엔진) · 주면 종단도는 따로 안 셈. */
|
||||
savedCross?: (
|
||||
d: SectionDetailResponse,
|
||||
routeId: number,
|
||||
s: PipelineStation[],
|
||||
) => Promise<CrossOutput | null>;
|
||||
/** 저장 노선 종단 기준 = 저장 상세 + 지금 입력(관 · 구조물)이 심는 측점(386-3 · `design_pipeline_base`) — 보기가
|
||||
* 종단도에 늦게 심어도 묶음은 다시 안 돎 · 없으면 종단도 기준 그대로. `types` = 구조물 정의 전부. */
|
||||
plantedBase?: (
|
||||
source: ReturnType<ProfileLink["pipelineSource"]>,
|
||||
inputs: PipelineInputs,
|
||||
types: ReadonlyArray<PlanType>,
|
||||
) => AlignmentBase | null;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { markDesignClean } from "./design_leave";
|
||||
import { resetDesign } from "./design_reset";
|
||||
import type { DesignView } from "./design_dock";
|
||||
import { routeDraftBridge, type PagePipelineOptions } from "./design_pipeline_page";
|
||||
import { plantedBase } from "./design_pipeline_base";
|
||||
import { SECTION_STATION_INTERVAL_M } from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
|
||||
import {
|
||||
applyStructureAreaRows,
|
||||
@@ -228,6 +229,7 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
profile.redraw();
|
||||
return detailCross(detail, stations);
|
||||
},
|
||||
plantedBase, // 저장 노선 종단 기준 = 입력이 심는 측점(386-3 · 보기 심기 무관)
|
||||
structureAreas: ({ cross_sections: rows }) =>
|
||||
applyStructureAreaRows(rows, structureAreaRows(rows)),
|
||||
// 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움
|
||||
@@ -340,12 +342,14 @@ async function routeChanged(shared: DesignPageShared): Promise<boolean> {
|
||||
*/
|
||||
export async function saveSharedDesign(shared: DesignPageShared, confirm: boolean): Promise<void> {
|
||||
if (await routeChanged(shared)) return;
|
||||
// [확정]은 횡단 본문(표준 횡단면 · 전 측점 면적)이 있어야 한다 — 숨긴 채라도 횡단 보기를 세운다.
|
||||
if (confirm) await shared.ensureView("cross");
|
||||
// [저장] · [확정] 본문 = 횡단 보기 한 벌(표준 횡단면 · 전 측점 면적 · 카드 고침) — 들어온 화면 무관 · 숨긴 채라도
|
||||
// 세움(B05 만 연 캐시 패치 길 지움 · 386-6)
|
||||
await shared.ensureView("cross");
|
||||
const profileSave = shared.bindings.profile?.save;
|
||||
const crossSave = shared.bindings.cross?.save;
|
||||
if (confirm && !crossSave?.cross) {
|
||||
showToast(L("B06_Profile_Confirm_Failed"), "error");
|
||||
const crossBody = crossSave?.cross;
|
||||
if (!crossBody) {
|
||||
showToast(L(confirm ? "B06_Profile_Confirm_Failed" : "B06_Profile_Save_Failed"), "error");
|
||||
return;
|
||||
}
|
||||
const profile = shared.profile;
|
||||
@@ -369,10 +373,7 @@ export async function saveSharedDesign(shared: DesignPageShared, confirm: boolea
|
||||
...profileSave?.steps,
|
||||
...crossSave?.steps,
|
||||
},
|
||||
cross: async (isConfirm) => {
|
||||
if (crossSave?.cross) await crossSave.cross(isConfirm);
|
||||
else await profileSave?.cross?.(isConfirm);
|
||||
},
|
||||
cross: crossBody,
|
||||
done: async (isConfirm) => {
|
||||
markDesignClean(shared.projectId); // 떠남 확인 — 지금 초안이 저장분(346-4)
|
||||
if (isConfirm) {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
|
||||
import { confirmRoute, type RouteLatestResponse } from "./B05_Profile_Api_Fetch";
|
||||
import type { DesignViewSave } from "../A00_Common/design/design_shared";
|
||||
import {
|
||||
invalidateSectionDetail,
|
||||
saveCachedCrossPatches,
|
||||
} from "../B06_Section/B06_Section_Section_Store";
|
||||
import { invalidateSectionDetail } from "../B06_Section/B06_Section_Section_Store";
|
||||
import type { createRoutePanel } from "./B05_Profile_UI_Panel";
|
||||
import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel";
|
||||
import type { createStructuresBridge } from "./B05_Profile_UI_Page_Structures";
|
||||
@@ -75,13 +72,7 @@ export function profileSaveParts(ctx: PageActionContext): DesignViewSave {
|
||||
);
|
||||
},
|
||||
},
|
||||
// 횡단 보기를 세운 적 없이 [저장]하면 — 캐시에 얹힌 횡단 수정분을 함께 남긴다. 안 보내면
|
||||
// 바로 아래 캐시 비우기에서 사라진다. 계획선 저장 **뒤에** 보내야 사용자 수정 1세트가
|
||||
// 최종본으로 얹힌다(2026-08-24 사용자 지적). 횡단 보기가 서 있으면 그쪽 본문이 나간다.
|
||||
cross: async () => {
|
||||
const routeId = ctx.latest()?.route?.id;
|
||||
if (routeId != null) await saveCachedCrossPatches(projectId, routeId);
|
||||
},
|
||||
// 횡단 본문은 늘 횡단 보기 몫 — [저장]이 숨긴 채라도 세움(386-6 · 옛 캐시 패치 길 지움)
|
||||
done: async () => {
|
||||
// 서버가 종단 정본의 계획선을 다시 썼다 — 공유 캐시를 비워 횡단 보기가 옛 계획선을 못 보게 한다.
|
||||
invalidateSectionDetail(projectId);
|
||||
|
||||
@@ -16,7 +16,6 @@ import { mountViewerStructureMenu } from "./B05_Profile_UI_Viewer_Menu";
|
||||
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
fetchStructures,
|
||||
dropStaleStructureTypes,
|
||||
fetchStructureTypes,
|
||||
staleDefinitionNotice,
|
||||
definitionStale,
|
||||
@@ -37,6 +36,7 @@ import { listPick } from "../common_util/common_util_structure_catalog";
|
||||
import { planStations } from "../common_util/common_util_structure_station";
|
||||
import { plannedIrregular } from "../A00_Common/design/design_structure_status";
|
||||
import { pipeChainageOf, pipeIdAt, withPipeIds } from "../A00_Common/design/design_pipe_ids";
|
||||
import { pageStructures, pageTypesChecked } from "../A00_Common/design/design_page_data";
|
||||
|
||||
/** 관 지점 한 건 — 유효직경·시설 종류·부속 옵션까지 그대로 받는다. */
|
||||
export interface PipeProjection {
|
||||
@@ -278,10 +278,10 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
// · 고르기 목록 — 켜진 종류만(B05 는 종전대로 여섯 군).
|
||||
// · 목록 이름 · 폼 · 알약 레인 · 군 판정 — **전부**. B06 에서 넣은 B군(측구·맹암거 등)은
|
||||
// 레지스트리에서 꺼져 있는데, 켜진 것만 주면 이름을 못 찾아 영문 키 · 「?」 알약으로 뜬다.
|
||||
await dropStaleStructureTypes(); // M02 에서 정의 판이 바뀌었으면 다시 받음(322-0)
|
||||
await pageTypesChecked(deps.projectId); // 정의 판 확인 · 저장분 = 페이지 자료 줄 한 번(322-0 · 386-2)
|
||||
const [allTypes, stored] = await Promise.all([
|
||||
fetchStructureTypes(true),
|
||||
fetchStructures(deps.projectId),
|
||||
pageStructures(deps.projectId),
|
||||
]);
|
||||
deps.panel().structures.setTypes(allTypes, listPick("b05")); // 표 목록 both(322-1)
|
||||
deps.profilePanel().setStructureTypes(allTypes);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
import type {
|
||||
FollowPatch,
|
||||
PipelineInputs,
|
||||
RangePart,
|
||||
StageFn,
|
||||
Vertex,
|
||||
@@ -109,8 +110,8 @@ export function createFollowStage(deps: {
|
||||
export const followStage = createFollowStage({ savedLine: () => null });
|
||||
|
||||
export interface PlanlineDeps {
|
||||
/** 저장 노선의 종단 기준(측점 한 벌 · 종단 보기 `listed`) — 없으면 아직 못 셈. */
|
||||
savedBase(): AlignmentBase | null;
|
||||
/** 저장 노선의 종단 기준(측점 한 벌 = 지금 입력이 심는 측점 · 386-3) — 없으면 아직 못 셈. */
|
||||
savedBase(inputs: PipelineInputs): AlignmentBase | null;
|
||||
/** 저장 노선(그린 선) — 바뀐 구간을 가름. */
|
||||
savedLine(): Vertex[] | null;
|
||||
/** 노선 초안 첫 계획선 꼭짓점 = 서버 저장 종단 — 보기가 심은 측점(초안 자리 관 등)에 안 흔들림(365-5) · 없으면 `savedBase`. */
|
||||
@@ -119,7 +120,7 @@ export interface PlanlineDeps {
|
||||
|
||||
export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
return (ctx) => {
|
||||
const saved = deps.savedBase();
|
||||
const saved = deps.savedBase(ctx.inputs);
|
||||
if (!saved) throw new Error("저장 종단 기준이 아직 없습니다.");
|
||||
const ground = ctx.out.ground;
|
||||
const route = ctx.out.route;
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
* 자리(No.N+0 · BP · EP)에 새로 놓은 구조물도 그 자리 구조물 없는 사본에 이름 · 형상(219 흠 · 322-3).
|
||||
* ========================================================================== */
|
||||
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { forgetPageData, pagePipes, pageStructures } from "../A00_Common/design/design_page_data";
|
||||
import {
|
||||
fetchStructures,
|
||||
fetchStructureTypes,
|
||||
readPendingStructures,
|
||||
type StructureInstance,
|
||||
@@ -44,8 +43,6 @@ const near = (a: number, b: number): boolean => Math.abs(a - b) < SAME_STATION_M
|
||||
const byChainage = (a: { chainage_m: number }, b: { chainage_m: number }) =>
|
||||
a.chainage_m - b.chainage_m;
|
||||
|
||||
/** 저장된 관 · 구조물 — 한쪽 초안만 있을 때 심는 자리 셈에 씀 · 저장 뒤(`forgetDraftStations`) 다시 받음. */
|
||||
const saved = new Map<string, Promise<[Pipes, StructureInstance[]]>>();
|
||||
/** 미리 뜬 횡단(`프로젝트:노선:누가거리`) — 못 뜬 자리는 null(다시 안 물음) · 저장 뒤 비움. */
|
||||
const previews = new Map<string, CrossSection | null>();
|
||||
/** 초안이 빼 둔 옛 자리 측점. */
|
||||
@@ -53,9 +50,9 @@ const parked = new WeakMap<SectionDetailResponse, CrossSection[]>();
|
||||
/** 상세마다 마지막 맞춤 번호 — 늦게 끝난 옛 맞춤이 새 맞춤을 덮지 않게. */
|
||||
const turns = new WeakMap<SectionDetailResponse, number>();
|
||||
|
||||
/** 저장 · 노선 바뀜 뒤 — 쥔 저장분 · 미리 뜬 횡단을 버림. */
|
||||
/** 저장 · 노선 바뀜 뒤 — 페이지 저장분(자료 줄 · 386-2) · 미리 뜬 횡단을 버림. */
|
||||
export function forgetDraftStations(projectId: string): void {
|
||||
saved.delete(projectId);
|
||||
forgetPageData(projectId);
|
||||
for (const key of [...previews.keys()]) if (key.startsWith(`${projectId}:`)) previews.delete(key);
|
||||
}
|
||||
|
||||
@@ -77,18 +74,12 @@ export function forgetPreviewAdjusts(
|
||||
}
|
||||
}
|
||||
|
||||
function savedOf(projectId: string): Promise<[Pipes, StructureInstance[]]> {
|
||||
let hit = saved.get(projectId);
|
||||
if (!hit) {
|
||||
hit = Promise.all([
|
||||
fetchDetailPipePoints(projectId).then((res) => res.pipe_points as Pipes),
|
||||
fetchStructures(projectId).then((res) => res.structures),
|
||||
]);
|
||||
hit.catch(() => saved.delete(projectId));
|
||||
saved.set(projectId, hit);
|
||||
}
|
||||
return hit;
|
||||
}
|
||||
/** 저장된 관 · 구조물 — 한쪽 초안만 있을 때 심는 자리 셈에 씀(페이지 자료 줄 한 번 · 저장 뒤 다시). */
|
||||
const savedOf = (projectId: string): Promise<[Pipes, StructureInstance[]]> =>
|
||||
Promise.all([
|
||||
pagePipes(projectId).then((res) => res.pipe_points as Pipes),
|
||||
pageStructures(projectId).then((res) => res.structures),
|
||||
]);
|
||||
|
||||
async function previewOf(
|
||||
projectId: string,
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
type DesignPageShared,
|
||||
} from "../A00_Common/design/design_shared";
|
||||
import {
|
||||
fetchStructures,
|
||||
dropStaleStructureTypes,
|
||||
fetchStructureTypes,
|
||||
staleDefinitionNotice,
|
||||
readPendingStructures,
|
||||
@@ -33,7 +31,7 @@ import {
|
||||
import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
|
||||
import { mergeFacilityOptions } from "../B05_Profile/logic/B05_Profile_UI_Drainage_Facility_Merge";
|
||||
import { fordWaitsOf, waitsFordHeight } from "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill";
|
||||
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import { pagePipes, pageStructures, pageTypesChecked } from "../A00_Common/design/design_page_data";
|
||||
import {
|
||||
linkFields,
|
||||
readGroupItems,
|
||||
@@ -375,14 +373,14 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
if (!deps.projectId) return;
|
||||
const projectId = deps.projectId;
|
||||
try {
|
||||
await dropStaleStructureTypes(); // M02 에서 정의 판이 바뀌었으면 다시 받음(322-0)
|
||||
await pageTypesChecked(projectId); // 정의 판 확인 · 저장분 = 페이지 자료 줄 한 번(322-0 · 386-2)
|
||||
const [types, stored, pipeResponse] = await Promise.all([
|
||||
// B06 은 **구조물 전체**를 넣을 수 있어야 한다(2026-09-07 사용자 지시 5). B05 목록에서
|
||||
// 빠져 있던 B군 종단배수·F군 생태/녹화도 여기서는 고를 수 있다 — 레지스트리 주석이
|
||||
// 「B06 개별 횡단도 옵션으로 재사용」이라 적어 둔 그 자리다.
|
||||
fetchStructureTypes(true),
|
||||
fetchStructures(projectId),
|
||||
fetchDetailPipePoints(projectId),
|
||||
pageStructures(projectId),
|
||||
pagePipes(projectId),
|
||||
]);
|
||||
section.setTypes(types, listPick("b06")); // 표 목록 both + b06(322-1)
|
||||
markTypes = types;
|
||||
|
||||
@@ -150,6 +150,12 @@ const panelModule = loadTs(
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
// 페이지 자료 줄(386-2) — 저장분 한 번
|
||||
"../A00_Common/design/design_page_data": {
|
||||
pageTypesChecked: async () => false,
|
||||
pageStructures: async () => ({ structures: [] }),
|
||||
pagePipes: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
"../common_util/common_util_summary_item": {
|
||||
linkFields: () => ({}),
|
||||
readGroupItems: (x) => x ?? {},
|
||||
|
||||
@@ -169,6 +169,12 @@ const panelModule = loadTs(
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
// 페이지 자료 줄(386-2) — 저장분 한 번
|
||||
"../A00_Common/design/design_page_data": {
|
||||
pageTypesChecked: async () => false,
|
||||
pageStructures: async () => ({ structures: [] }),
|
||||
pagePipes: async () => ({ pipe_points: PIPES, basins: [] }),
|
||||
},
|
||||
"../common_util/common_util_summary_item": {
|
||||
linkFields: () => ({}),
|
||||
readGroupItems: (x) => x ?? {},
|
||||
|
||||
@@ -35,15 +35,23 @@ const types = [
|
||||
options: [],
|
||||
},
|
||||
];
|
||||
const serverFetch = {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: [{ chainage_m: 95.38, facility: "pipe" }] }),
|
||||
};
|
||||
const serverStructures = {
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
fetchStructureTypes: async () => types,
|
||||
readPendingStructures: () => null,
|
||||
dropStaleStructureTypes: async () => false,
|
||||
};
|
||||
const drafts = loadTs("B06_Section/B06_Section_Section_Drafts.ts", {
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: [{ chainage_m: 95.38, facility: "pipe" }] }),
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
fetchStructureTypes: async () => types,
|
||||
readPendingStructures: () => null,
|
||||
},
|
||||
"../B04_PreProcess/B04_PreProcess_Api_Fetch": serverFetch,
|
||||
"../B05_Profile/B05_Profile_Api_Structures": serverStructures,
|
||||
// 페이지 자료 줄(386-2) — 진짜 · 같은 서버 흉내
|
||||
"../A00_Common/design/design_page_data": loadTs("A00_Common/design/design_page_data.ts", {
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": serverFetch,
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": serverStructures,
|
||||
}),
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => draftPipes },
|
||||
"../B05_Profile/B05_Profile_Api_PlusStations": { readPlusDraft: () => null },
|
||||
"@util/common_util_culvert_sets": {
|
||||
|
||||
@@ -24,6 +24,11 @@ const STUBS = {
|
||||
chainages.map((at) => ({ chainage_m: at, kind: "irregular", design: { fresh: true } })),
|
||||
},
|
||||
};
|
||||
// 페이지 자료 줄(386-2) — 진짜 design_page_data 가 같은 서버 흉내를 부름
|
||||
STUBS["../../B04_PreProcess/B04_PreProcess_Api_Fetch"] =
|
||||
STUBS["../B04_PreProcess/B04_PreProcess_Api_Fetch"];
|
||||
STUBS["../../B05_Profile/B05_Profile_Api_Structures"] =
|
||||
STUBS["../B05_Profile/B05_Profile_Api_Structures"];
|
||||
const cache = new Map();
|
||||
const load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
|
||||
@@ -475,6 +475,7 @@ const state = { draftPipes: null, savedPipes: [] };
|
||||
const loadDrafts = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Section_Drafts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_page_data.ts"), // 자료 줄(386-2)
|
||||
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_culvert_sets.ts"),
|
||||
],
|
||||
@@ -487,6 +488,14 @@ const loadDrafts = makeLoader(
|
||||
fetchStructureTypes: async () => [],
|
||||
readPendingStructures: () => null,
|
||||
},
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => ({ pipe_points: state.savedPipes }),
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => ({ structures: [] }),
|
||||
fetchStructureTypes: async () => [],
|
||||
readPendingStructures: () => null,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
|
||||
"./B06_Section_Api_Fetch": {
|
||||
previewStructureSections: async (_p, chainages) =>
|
||||
|
||||
@@ -313,6 +313,7 @@ const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedSt
|
||||
const load = makeLoader(
|
||||
[
|
||||
B06("B06_Section_Section_Drafts"),
|
||||
path.join(ROOT, "A00_Common", "design", "design_page_data.ts"), // 자료 줄(386-2)
|
||||
path.join(ROOT, "common_util", "common_util_culvert_sets.ts"),
|
||||
path.join(ROOT, "common_util", "common_util_structure_station.ts"),
|
||||
// 그림 자료 = 형상 얹기 한 벌(323-4)
|
||||
@@ -329,6 +330,14 @@ const state = { draftPipes: null, draftStructures: null, savedPipes: [], savedSt
|
||||
fetchStructureTypes: async () => TYPES,
|
||||
readPendingStructures: () => state.draftStructures,
|
||||
},
|
||||
"../../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 () => TYPES,
|
||||
readPendingStructures: () => state.draftStructures,
|
||||
},
|
||||
"../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => state.draftPipes },
|
||||
"./B06_Section_Api_Fetch": {
|
||||
previewStructureSections: async (_p, chainages) =>
|
||||
|
||||
@@ -540,7 +540,49 @@ async function run() {
|
||||
savedFirst: firstPvi(savedBase),
|
||||
};
|
||||
page.clearDrafts(C);
|
||||
|
||||
// ⑬ 보기가 늦게 심은 측점(386-3) — 종단도 기준이 바뀌어도(restart) 묶음은 안 돎 · 기준 = 입력이 심는 측점(옵션)
|
||||
const D = "proj-386";
|
||||
window.localStorage.setItem("frd_current_project_id", D);
|
||||
let viewBase = savedBase;
|
||||
const planted = [];
|
||||
pageMod.routeDraftBridge(
|
||||
D,
|
||||
{ ...link(), pipelineSource: () => ({ ...link().pipelineSource(), base: viewBase }) },
|
||||
{
|
||||
stationIntervalM: job.interval,
|
||||
plantedBase: (source, inputs) => {
|
||||
planted.push(Array.isArray(inputs.pipes) ? inputs.pipes.length : null);
|
||||
return source.base;
|
||||
},
|
||||
},
|
||||
);
|
||||
pageMod.pipelineEarthwork(D, { conversion: job.conversion });
|
||||
// 기록 상한(RUN_LOG_SIZE) 뒤라 길이 대신 새 기록을 셈
|
||||
const since = (old) => runs().filter((record) => !old.has(record));
|
||||
let old = new Set(runs());
|
||||
for (let i = 0; i < 400; i += 1) {
|
||||
const fresh = since(old);
|
||||
if (fresh.length && fresh.every((record) => record.outcome !== "running")) break;
|
||||
await tick(5);
|
||||
}
|
||||
await tick(20);
|
||||
const openRuns = since(old).length;
|
||||
old = new Set(runs());
|
||||
viewBase = {
|
||||
...savedBase,
|
||||
stations: [...savedBase.stations, { station_id: "irregular:late", chainage_m: 33 }],
|
||||
};
|
||||
pageMod.restartPagePipeline(D); // 종단도 setIrregularStations 꼴
|
||||
await tick(30);
|
||||
const late = { openRuns, lateRuns: since(old).length, planted: planted[0] ?? null };
|
||||
old = new Set(runs());
|
||||
late.same = await pageMod.reconcilePagePipeline(D);
|
||||
late.sameRuns = since(old).length;
|
||||
pageMod.disposePagePipeline(D);
|
||||
page.clearDrafts(D);
|
||||
return {
|
||||
late,
|
||||
kept,
|
||||
b05,
|
||||
b06,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/* PLAN 386-2 단위검증 헬퍼 — 진짜 `design_page_data.ts` 를 서버 흉내(받은 횟수 셈)로 돌림.
|
||||
* 결과 = 표준출력 JSON 한 벌. 짝 = `test_384_2_page_data.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 calls = { structures: 0, pipes: 0, version: 0 };
|
||||
let failPipes = false;
|
||||
const STUBS = {
|
||||
"../../B04_PreProcess/B04_PreProcess_Api_Fetch": {
|
||||
fetchDetailPipePoints: async () => {
|
||||
calls.pipes += 1;
|
||||
if (failPipes) throw new Error("끊김");
|
||||
return { pipe_points: [{ chainage_m: 10, facility: "pipe" }], basins: [] };
|
||||
},
|
||||
},
|
||||
"../../B05_Profile/B05_Profile_Api_Structures": {
|
||||
fetchStructures: async () => {
|
||||
calls.structures += 1;
|
||||
return { structures: [{ structure_id: "s1", chainage_m: 20 }], revision: calls.structures };
|
||||
},
|
||||
dropStaleStructureTypes: async () => {
|
||||
calls.version += 1;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
const js = ts.transpileModule(
|
||||
fs.readFileSync(path.join(ROOT, "A00_Common", "design", "design_page_data.ts"), "utf8"),
|
||||
{ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } },
|
||||
).outputText;
|
||||
const box = { exports: {} };
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (name) => {
|
||||
if (!STUBS[name]) throw new Error(`stub 없음: ${name}`);
|
||||
return STUBS[name];
|
||||
});
|
||||
const data = box.exports;
|
||||
const P = "p1";
|
||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
(async () => {
|
||||
const out = {};
|
||||
// 열 때 · 보기 전환 · 다시 보임 = 같은 저장분 — 서버는 한 번
|
||||
const [a, b] = await Promise.all([data.pageStructures(P), data.pageStructures(P)]);
|
||||
await data.pageStructures(P);
|
||||
await data.pagePipes(P);
|
||||
await data.pagePipes(P);
|
||||
await data.pageTypesChecked(P);
|
||||
await data.pageTypesChecked(P);
|
||||
out.open = { ...calls };
|
||||
// 사본 — 받는 쪽이 고쳐도 다음 받기는 그대로
|
||||
a.structures.push({ structure_id: "x" });
|
||||
out.copy = { same: a === b, after: (await data.pageStructures(P)).structures.length };
|
||||
// 다른 프로젝트는 따로
|
||||
await data.pageStructures("p2");
|
||||
out.other = calls.structures;
|
||||
// 저장 정본이 바뀜 — 비우면 다음에 한 번 다시
|
||||
data.forgetPageData(P);
|
||||
await data.pageStructures(P);
|
||||
await data.pageStructures(P);
|
||||
out.afterForget = { ...calls };
|
||||
// 실패는 기억 안 함 — 다음에 다시 물음
|
||||
data.forgetPageData(P);
|
||||
failPipes = true;
|
||||
out.failed = await data.pagePipes(P).then(
|
||||
() => "ok",
|
||||
() => "error",
|
||||
);
|
||||
failPipes = false;
|
||||
await data.pagePipes(P);
|
||||
out.retry = calls.pipes;
|
||||
// 「계산 중」 — 떠 있는 차례가 끝날 때까지 켬(밀림 · 실패도 끔)
|
||||
const seen = [];
|
||||
const stop = data.onPageBusy(P, (busy) => seen.push(busy));
|
||||
let finish;
|
||||
data.trackPageBusy(P, new Promise((resolve) => (finish = resolve)));
|
||||
data.trackPageBusy(P, Promise.reject(new Error("밀림")));
|
||||
data.trackPageBusy(P, null);
|
||||
await tick();
|
||||
out.midBusy = seen[seen.length - 1];
|
||||
finish();
|
||||
await tick();
|
||||
stop();
|
||||
data.trackPageBusy(P, Promise.resolve());
|
||||
await tick();
|
||||
out.busy = seen;
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/* 386-3 시험 묶음 진입점 — 묶음 planline 저장 기준(진짜 모듈 · vite ssr). 짝 = test_384_3_planted_base.py. */
|
||||
export { plantedBase } from "../../A00_Common/design/design_pipeline_base";
|
||||
export { usePipeIds } from "../../A00_Common/design/design_pipe_ids";
|
||||
export { catalogRow, useCatalogDefinition } from "../../common_util/common_util_structure_catalog";
|
||||
@@ -0,0 +1,124 @@
|
||||
/* 386-3 헬퍼 — 묶음 planline 저장 기준 = 저장 상세 + 지금 입력(관 · 구조물)이 심는 측점.
|
||||
* node helper_384_3_planted.mjs <묶음.mjs>
|
||||
* vite ssr 로 묶은 진짜 모듈(helper_384_3_entry.ts) · 표준출력 JSON 한 벌. 짝 = test_384_3_planted_base.py.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const store = () => {
|
||||
const m = new Map();
|
||||
return {
|
||||
getItem: (k) => (m.has(k) ? m.get(k) : null),
|
||||
setItem: (k, v) => m.set(k, String(v)),
|
||||
removeItem: (k) => m.delete(k),
|
||||
key: (i) => [...m.keys()][i] ?? null,
|
||||
get length() {
|
||||
return m.size;
|
||||
},
|
||||
clear: () => m.clear(),
|
||||
};
|
||||
};
|
||||
globalThis.window = globalThis;
|
||||
globalThis.sessionStorage = store();
|
||||
globalThis.localStorage = store();
|
||||
const any = new Proxy(function () {}, {
|
||||
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
|
||||
apply: () => any,
|
||||
construct: () => any,
|
||||
});
|
||||
globalThis.document = any;
|
||||
|
||||
const m = await import(pathToFileURL(process.argv[2]).href);
|
||||
const P = "p386";
|
||||
localStorage.setItem("frd_current_project_id", P);
|
||||
m.usePipeIds(P);
|
||||
|
||||
const st = (chainage_m, kind = "regular") => ({
|
||||
station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`,
|
||||
chainage_m,
|
||||
kind,
|
||||
});
|
||||
const server = [st(0, "bp"), st(20), st(40), st(60, "ep")];
|
||||
const detail = {
|
||||
longitudinal: {
|
||||
stations: server,
|
||||
length_m: 60,
|
||||
samples: [
|
||||
{ chainage_m: 0, elevation_m: 100 },
|
||||
{ chainage_m: 60, elevation_m: 106 },
|
||||
],
|
||||
profile_alignment: {
|
||||
policy: { default_curve_radius_m: 100, station_interval_m: 20 },
|
||||
base_pvi: [
|
||||
{ chainage_m: 0, elevation_m: 100 },
|
||||
{ chainage_m: 60, elevation_m: 106 },
|
||||
],
|
||||
samples: [
|
||||
{ chainage_m: 0, ground_elevation_m: 100 },
|
||||
{ chainage_m: 60, ground_elevation_m: 106 },
|
||||
],
|
||||
stations: server,
|
||||
segments: [],
|
||||
curves: [],
|
||||
},
|
||||
},
|
||||
cross_sections: [],
|
||||
};
|
||||
// 구조물 표 = 실제 정의(마스터 읽기만) · 측점을 심는 구조물 — 켜진 점형 하나 · 꺼진 것 하나(보기와 같이 둘 다 심음)
|
||||
const definition = JSON.parse(
|
||||
readFileSync(new URL("../master_template/구조물정의.json", import.meta.url), "utf8"),
|
||||
);
|
||||
m.useCatalogDefinition(definition);
|
||||
const planting = (item) => {
|
||||
const row = m.catalogRow(item.type_id);
|
||||
return row && row.owner === "structures" && row.station !== "none";
|
||||
};
|
||||
const on = definition.types.find(
|
||||
(item) => item.enabled !== false && item.placement === "point" && planting(item),
|
||||
);
|
||||
const off = definition.types.find((item) => item.enabled === false && planting(item));
|
||||
const types = [on, off].map(({ type_id, group, placement, enabled }) => ({
|
||||
type_id,
|
||||
group,
|
||||
placement,
|
||||
enabled,
|
||||
}));
|
||||
const at = (id, type_id, chainage_m) => ({
|
||||
structure_id: id,
|
||||
type_id,
|
||||
chainage_m,
|
||||
start_m: chainage_m - 3,
|
||||
end_m: chainage_m + 3,
|
||||
options: {},
|
||||
});
|
||||
const ids = (base) => base.stations.map((s) => [s.station_id.split(":")[0], s.chainage_m]);
|
||||
const viewBase = { stations: [{ station_id: "view", chainage_m: 1 }] };
|
||||
const none = m.plantedBase({ base: viewBase, detail }, { pipes: [], structures: [] }, types);
|
||||
const pipe = m.plantedBase(
|
||||
{ base: viewBase, detail },
|
||||
{ pipes: [], structures: [at("b1", on.type_id, 33)] },
|
||||
types,
|
||||
);
|
||||
const both = m.plantedBase(
|
||||
{ base: viewBase, detail },
|
||||
{
|
||||
pipes: [],
|
||||
structures: [at("b1", on.type_id, 33), at("d1", off.type_id, 47)],
|
||||
},
|
||||
types,
|
||||
);
|
||||
const noDetail = m.plantedBase(
|
||||
{ base: viewBase, detail: null },
|
||||
{ pipes: [], structures: [] },
|
||||
types,
|
||||
);
|
||||
process.stdout.write(
|
||||
JSON.stringify({
|
||||
none: ids(none),
|
||||
pipe: ids(pipe),
|
||||
both: ids(both),
|
||||
noDetail: noDetail === viewBase,
|
||||
pvi: both.basePvi.length,
|
||||
kinds: [on.type_id, off.type_id],
|
||||
}),
|
||||
);
|
||||
@@ -126,6 +126,7 @@ const LOCALES = {
|
||||
B06_Profile_Btn_Confirm: ["확정", "Confirm"],
|
||||
Design_Btn_ToCross: ["횡단 보기", "Cross view"],
|
||||
Design_Btn_ToProfile: ["노선 보기", "Route view"],
|
||||
Design_Busy: ["계산 중", "Calculating"], // 「계산 중」 점(386-2)
|
||||
};
|
||||
const STUBS = {
|
||||
"@ui/ui_template_locale": { ui_locales: LOCALES, currentLanguageIndex: 0 },
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
"""PLAN 386-2 sub7 — B05 · B06 한 페이지 자료 줄(`design_page_data.ts`).
|
||||
|
||||
못박는 것
|
||||
- 저장분(구조물 · 관 · 정의 판 확인) = 페이지에서 한 번 — 묶음 · 두 보기 구조물 칸 · 초안 측점이 같은 줄.
|
||||
- 보기 전환 · 다시 보임 = 요청 0(구조물 칸 load 가 직접 안 받음).
|
||||
- 저장 정본이 바뀌면(`invalidateSectionDetail` → `forgetDraftStations`) · 페이지를 떠나면 비움 → 한 번 다시.
|
||||
- 돌려줄 때마다 사본 · 실패는 기억 안 함 · 「계산 중」 점 = 묶음 차례가 떠 있는 동안.
|
||||
|
||||
짝: `helper_384_2_page_data.cjs`(진짜 TS · 서버 흉내).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def r() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_384_2_page_data.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_저장분_한_번(r):
|
||||
assert r["open"] == {"structures": 1, "pipes": 1, "version": 1}
|
||||
assert r["other"] == 2 # 다른 프로젝트는 따로
|
||||
|
||||
|
||||
def test_돌려줄_때마다_사본(r):
|
||||
assert r["copy"] == {"same": False, "after": 1}
|
||||
|
||||
|
||||
def test_비우면_한_번_다시(r):
|
||||
assert r["afterForget"]["structures"] == 3 # p1 · p2 · 비운 뒤 p1 한 번
|
||||
|
||||
|
||||
def test_실패는_기억_안_함(r):
|
||||
assert r["failed"] == "error"
|
||||
assert r["retry"] == 3 # 처음 · 실패 · 다시
|
||||
|
||||
|
||||
def test_계산_중_점(r):
|
||||
assert r["midBusy"] is True
|
||||
assert r["busy"][0] is False and r["busy"][-1] is False # 듣기 시작 = 지금 상태 · 끝나면 끔
|
||||
assert len(r["busy"]) <= 6 # 뗀 뒤 차례는 안 알림
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"rel",
|
||||
[
|
||||
"A00_Common/design/design_pipeline_page.ts",
|
||||
"B05_Profile/B05_Profile_UI_Page_Structures.ts",
|
||||
"B06_Section/B06_Section_UI_Page_Structures_Panel.ts",
|
||||
"B06_Section/B06_Section_Section_Drafts.ts",
|
||||
],
|
||||
)
|
||||
def test_보기_전환_길은_자료_줄만(rel):
|
||||
src = _read(rel)
|
||||
assert "dropStaleStructureTypes(" not in src
|
||||
assert "fetchDetailPipePoints(" not in src
|
||||
assert "pageStructures(" in src
|
||||
# B05 구조물 칸은 [저장] 직전 판번호 · 저장 뒤 목록만 직접 받음
|
||||
if "Page_Structures.ts" not in rel:
|
||||
assert "fetchStructures(" not in src
|
||||
|
||||
|
||||
def test_비우는_자리():
|
||||
drafts = _read("B06_Section/B06_Section_Section_Drafts.ts")
|
||||
page = _read("A00_Common/design/design_page.ts")
|
||||
store = _read("B06_Section/B06_Section_Section_Store.ts")
|
||||
assert "forgetPageData(projectId)" in drafts # 저장 정본 바뀜(invalidate → forgetDraftStations)
|
||||
assert "forgetDraftStations(projectId)" in store
|
||||
assert "forgetPageData(projectId)" in page # 페이지를 떠남
|
||||
assert "onPageBusy(projectId" in page
|
||||
|
||||
|
||||
def test_계산_중_점은_묶음_차례():
|
||||
pipe = _read("A00_Common/design/design_pipeline_page.ts")
|
||||
dock = _read("A00_Common/design/design_dock.ts")
|
||||
assert pipe.count("trackPageBusy(projectId") == 2 # 열 때 start · 조작 feed
|
||||
assert "setBusy" in dock and "Design_Busy" in dock
|
||||
@@ -0,0 +1,91 @@
|
||||
"""PLAN 386-3 sub7 — 묶음 입력 = 가게만(보기 측점 심기 · baseMoved 지움).
|
||||
|
||||
못박는 것
|
||||
- 묶음 planline 저장 기준 = 저장 상세 + 지금 입력(관 · 구조물)이 심는 측점(`plantedBase` · 보기와 같은 셈) —
|
||||
종류 = 정의 전부(꺼진 종류도 심음) · 상세가 없으면 종단도 기준.
|
||||
- 보기가 종단도에 늦게 심어도(종단도 기준이 바뀌어 restart) 묶음은 안 돎 · 열 때 1 · 같은 입력 0.
|
||||
- `baseMoved` 없음 · 페이지(design_shared)가 옵션으로 넘김(묶음은 화면 모듈을 안 끌어옴).
|
||||
|
||||
짝: `helper_384_3_entry.ts` + `helper_384_3_planted.mjs`(진짜 모듈 · vite ssr) · `helper_365_page_pipeline.cjs` ⑬.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
if str(HERE) not in sys.path:
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
from test_365_page_pipeline import r # noqa: E402,F401 — 묶음 시험 한 벌(⑬ 늦게 심은 측점)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def planted(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("b386")
|
||||
build = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_384_3_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
)
|
||||
assert build.returncode == 0, build.stderr
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_384_3_entry.js", bundle)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_384_3_planted.mjs"), str(bundle)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_저장_기준_입력이_심는_측점(planted):
|
||||
assert [c for _, c in planted["none"]] == [0, 20, 40, 60] # 심을 것 없음 = 저장 상세 측점
|
||||
assert ["irregular", 33] in planted["pipe"] # 구조물 자리
|
||||
assert ["irregular", 33] in planted["both"] and ["irregular", 47] in planted[
|
||||
"both"
|
||||
] # 꺼진 종류도
|
||||
assert planted["noDetail"] is True # 상세 없음 = 종단도 기준
|
||||
assert planted["pvi"] == 2
|
||||
|
||||
|
||||
def test_늦게_심어도_안_돎(r):
|
||||
late = r["late"]
|
||||
assert late["openRuns"] == 1 # 열 때 1
|
||||
assert late["lateRuns"] == 0 # 종단도 기준이 바뀌어도(보기가 늦게 심음) 안 돎
|
||||
assert late["same"] == 0 and late["sameRuns"] == 0 # 같은 입력 0
|
||||
assert late["planted"] is not None # planline 이 입력(관)으로 기준을 셈
|
||||
|
||||
|
||||
def test_baseMoved_없음_옵션은_페이지가():
|
||||
pipe = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
||||
assert "baseMoved" not in pipe
|
||||
assert "design_pipeline_base" not in pipe # 묶음은 화면 모듈을 안 끌어옴
|
||||
assert "options.plantedBase?.(savedSource(), at, types)" in pipe
|
||||
assert "plantedBase, //" in shared
|
||||
@@ -0,0 +1,46 @@
|
||||
"""PLAN 386-6 sub7 — [저장] 본문 보기 무관(B05 만 연 길 지움).
|
||||
|
||||
못박는 것
|
||||
- [저장] · [확정] 둘 다 횡단 보기를 (숨긴 채라도) 세움 → 횡단 본문 = 횡단 보기 몫 한 벌(`crossSaveParts`).
|
||||
- 종단 보기 몫엔 횡단 본문이 없음 — 옛 「횡단 보기를 세운 적 없이 [저장]」 캐시 패치 길(saveCachedCrossPatches) 지움.
|
||||
- 횡단 보기를 못 세우면 반쪽 저장 대신 실패 알림.
|
||||
화면(B05 만 연 [저장] 본문 = B06 다녀온 본문)은 PLAN 386-6 검증 줄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
return (ROOT / rel).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _save_body() -> str:
|
||||
shared = _read("A00_Common/design/design_shared.ts")
|
||||
start = shared.index("export async function saveSharedDesign")
|
||||
return shared[start : shared.index("\n}\n", start)]
|
||||
|
||||
|
||||
def test_저장도_횡단_보기를_세움():
|
||||
body = _save_body()
|
||||
assert 'await shared.ensureView("cross");' in body
|
||||
assert 'if (confirm) await shared.ensureView("cross")' not in body
|
||||
assert "cross: crossBody," in body
|
||||
assert "profileSave?.cross" not in body
|
||||
|
||||
|
||||
def test_못_세우면_실패_알림():
|
||||
body = _save_body()
|
||||
assert "if (!crossBody) {" in body
|
||||
assert '"B06_Profile_Save_Failed"' in body and '"B06_Profile_Confirm_Failed"' in body
|
||||
|
||||
|
||||
def test_종단_보기_몫엔_횡단_본문_없음():
|
||||
actions = _read("B05_Profile/B05_Profile_UI_Page_Actions.ts")
|
||||
start = actions.index("export function profileSaveParts(")
|
||||
parts = actions[start:]
|
||||
assert "saveCachedCrossPatches" not in actions
|
||||
assert "\n cross: " not in parts
|
||||
@@ -1057,4 +1057,5 @@ export const ui_locales_b2 = {
|
||||
"계산 불안정 — 입력과 결과가 세 바퀴 안에 안 맞음",
|
||||
"Calculation unstable — inputs and results did not settle in three rounds",
|
||||
],
|
||||
Design_Busy: ["계산 중", "Calculating"],
|
||||
} as const satisfies Record<string, LocaleEntry>;
|
||||
|
||||
Reference in New Issue
Block a user