Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -142,6 +142,8 @@ export const STATE_REGISTRY = {
|
||||
},
|
||||
/** 관 세션 id 장부(323-5 · `design_pipe_ids`) — `{next, ids: [{id, at}]}` · 저장 안 함. */
|
||||
"pipe-ids": { bucket: "draft", scope: "project" },
|
||||
/** 횡단 보기가 서기 전 종단 보기 폼이 고친 관 옵션(세월교 · BOX) `[[누가거리, 옵션]]` — 횡단 보기가 서면 캐시에 얹고 비움(462). */
|
||||
"pipe-option-queue": { bucket: "draft", scope: "project" },
|
||||
/** 설계 가게(`design/design_store`)의 고른 측점 · 구조물 — `{chainageM, structureId}`. */
|
||||
selection: { bucket: "draft", scope: "project" },
|
||||
/** 구조물 패널 종류별 캐시 — 고른 구조물 하나 × 종류마다 옛 값(54-32). [저장]·[확정] 뒤 비움. */
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_facility_lane.ts
|
||||
* B05 · B06 한 페이지 **시설 줄 한 벌**(요구 271-5 · 462) — 구조물 + 관 → 알약 · 비정규 측점 · 흙덮기 관 ·
|
||||
* 관 id 장부 맞춤 · 평면 마커 원천.
|
||||
*
|
||||
* 페이지가 세움 — 보기가 숨어도 · 아직 안 섰어도 늘 셈(순수 계산 · 서버 GET 없음). 종단도에 알약 · 측점 ·
|
||||
* 흙덮기 관을 쓰는 자리는 여기 하나 · 보기는 읽기만(`marks` · `stations` · `pipes`) · 바뀜은 `onChange`.
|
||||
* 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게)
|
||||
* 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도
|
||||
* 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d)
|
||||
* ========================================================================== */
|
||||
|
||||
import { useDesignStore } from "./design_store";
|
||||
import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids";
|
||||
import { plannedIrregular } from "./design_structure_status";
|
||||
import { planStations } from "../../common_util/common_util_structure_station";
|
||||
import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow";
|
||||
import { followedStation } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
import { followSnapped } from "../../B05_Profile/B05_Profile_UI_Structures_Snap";
|
||||
import {
|
||||
pipesToStructureMarks,
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||
import type { IrregularStation } from "../../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import type { MinCoverPipe } from "../../B05_Profile/B05_Profile_UI_Profile_MinCover";
|
||||
|
||||
/** 관 한 건 — 배수유역도 알림 · B06 관 목록 어느 쪽이든(`id` = 관 고정 id · 새 관은 없음). */
|
||||
export interface LanePipe {
|
||||
chainage_m: number;
|
||||
id?: string | null;
|
||||
facility: string;
|
||||
options?: Record<string, string | number> | null;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
}
|
||||
|
||||
/** 시설 줄이 쓰는 종단도 몫. */
|
||||
export interface LaneProfile {
|
||||
setStructureTypes: (types: StructureType[]) => void;
|
||||
setStructures: (next: StructureInstance[]) => void;
|
||||
setIrregularStations: (stations: IrregularStation[]) => void;
|
||||
setMinCoverPipes: (pipes: ReadonlyArray<MinCoverPipe>) => void;
|
||||
resetStationEdit: (chainageM: number, moveTo?: number) => void;
|
||||
}
|
||||
|
||||
export interface FacilityLane {
|
||||
/** 종류 표 · 저장분 구조물(보기가 받은 것 — 같은 페이지 자료 줄). */
|
||||
setTypes: (types: StructureType[]) => void;
|
||||
setStored: (structures: StructureInstance[]) => void;
|
||||
/** 구조물 · 관 목록이 바뀜 — `carryEdits` = 사용자 고침(계획고 편집 옮김) · 불러옴은 false. */
|
||||
set: (
|
||||
next: { structures?: StructureInstance[]; pipes?: ReadonlyArray<LanePipe> },
|
||||
carryEdits?: boolean,
|
||||
) => void;
|
||||
/** 노선을 놓아 계산 묶음이 가게 관 · 구조물을 다시 매김(되돌림 포함) — 편집 안 옮김. */
|
||||
follow: () => void;
|
||||
setIntervalM: (intervalM: number) => void;
|
||||
/** [초기선 복원] — 관 투영 · 측점만 비움(관 정본은 배수유역 몫). */
|
||||
clearProjected: () => void;
|
||||
/** 다시 셈(편집 안 옮김) · 종단도에 다시 씀 — 종단도가 막 섰을 때 · 다시 그렸을 때(추가 측점이 늦게 옴). */
|
||||
paint: () => void;
|
||||
pipes: () => LanePipe[];
|
||||
structures: () => StructureInstance[];
|
||||
types: () => StructureType[];
|
||||
/** 알약 = 구조물 + 관(가상 구조물 · 관 세션 id) — 평면 마커도 이것. */
|
||||
marks: () => StructureInstance[];
|
||||
stations: () => IrregularStation[];
|
||||
intervalM: () => number;
|
||||
onChange: (fn: () => void) => () => void;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
const run = (fn: () => void): void => {
|
||||
try {
|
||||
fn();
|
||||
} catch (error) {
|
||||
console.error("[facility_lane]", error);
|
||||
}
|
||||
};
|
||||
|
||||
export function createFacilityLane(
|
||||
projectId: string,
|
||||
profile: () => LaneProfile | null,
|
||||
): FacilityLane {
|
||||
const store = useDesignStore(projectId, null);
|
||||
let types: StructureType[] = [];
|
||||
let stored: StructureInstance[] = [];
|
||||
let structures: StructureInstance[] = [];
|
||||
/** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */
|
||||
let reported: LanePipe[] | null = null;
|
||||
let pipes: LanePipe[] = [];
|
||||
let sessionIds = new Map<string, string>(); // 관 고정 id → 세션 id(알약 id)
|
||||
let stations: IrregularStation[] = [];
|
||||
let intervalM = 20;
|
||||
let painted: { to: LaneProfile; stations: string } | null = null;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
/** 화면 관 목록 = 알림을 가게 관 초안 측점으로(노선 따라 다시 매김 · 349-0d) · 관 세션 id 도 그 자리로 —
|
||||
* 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 · 되돌려도 그 관) · id 없는 관(막 놓은 관)은 자리로. */
|
||||
const shownPipes = (list: ReadonlyArray<LanePipe>): LanePipe[] => {
|
||||
const shown = followPipeReports(
|
||||
list.map((pipe) => ({ ...pipe, id: pipe.id ?? undefined })),
|
||||
store.get("pipes"),
|
||||
).sort((x, y) => x.chainage_m - y.chainage_m);
|
||||
placePipeIds(
|
||||
shown.flatMap((pipe) => {
|
||||
const sid = pipe.id && sessionIds.get(pipe.id);
|
||||
return sid ? [[sid, pipe.chainage_m] as const] : [];
|
||||
}),
|
||||
projectId,
|
||||
);
|
||||
sessionIds = new Map(
|
||||
shown.flatMap((pipe) =>
|
||||
pipe.id ? [[pipe.id, pipeIdAt(pipe.chainage_m, projectId)] as const] : [],
|
||||
),
|
||||
);
|
||||
prunePipeIds(
|
||||
shown.map((pipe) => pipe.chainage_m),
|
||||
projectId,
|
||||
); // 헛 id 정리(411-E)
|
||||
return shown;
|
||||
};
|
||||
|
||||
/** 옛 측점에 남은 계획고 편집(유령 변화점)을 옮기거나 지움 — 옮김 = 0.006m 안 새 측점 · 같은 id ·
|
||||
* 사라진 것 하나와 새것 하나(옮긴 관 · 213-2) · 없으면 맞춘 자리의 새 측점(234-1 · 272) · 나머지는 지움. */
|
||||
const carry = (next: IrregularStation[]): void => {
|
||||
const to = profile();
|
||||
if (!to) return;
|
||||
const nextKeys = new Set(next.map((station) => station.chainage_m.toFixed(3)));
|
||||
const oldKeys = new Set(stations.map((station) => station.chainage_m.toFixed(3)));
|
||||
const gone = stations.filter((station) => !nextKeys.has(station.chainage_m.toFixed(3)));
|
||||
const born = next.filter((station) => !oldKeys.has(station.chainage_m.toFixed(3)));
|
||||
gone.forEach((station) => {
|
||||
const same = followedStation(station, gone, born) ?? followSnapped(station, born);
|
||||
to.resetStationEdit(station.chainage_m, same?.chainage_m);
|
||||
});
|
||||
};
|
||||
|
||||
const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)];
|
||||
|
||||
function paint(force = false): void {
|
||||
const to = profile();
|
||||
if (!to) return;
|
||||
to.setStructureTypes([...types]);
|
||||
to.setStructures(marks());
|
||||
const signature = JSON.stringify(stations);
|
||||
// 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림
|
||||
if (force || painted?.to !== to || painted.stations !== signature)
|
||||
to.setIrregularStations(stations);
|
||||
painted = { to, stations: signature };
|
||||
to.setMinCoverPipes(pipes as MinCoverPipe[]);
|
||||
}
|
||||
|
||||
/** 다시 셈 — 종류 표를 받기 전이면 측점은 미룸(받으면 다시 맞춤). */
|
||||
function refresh(carryEdits: boolean, force = false): void {
|
||||
pipes = reported ? shownPipes(reported) : [];
|
||||
if (types.length) {
|
||||
const next = plannedIrregular(
|
||||
planStations(withPipeIds(pipes, projectId), structures, types),
|
||||
intervalM,
|
||||
);
|
||||
if (carryEdits) carry(next);
|
||||
stations = next;
|
||||
}
|
||||
paint(force);
|
||||
listeners.forEach(run);
|
||||
}
|
||||
|
||||
// 추가 측점(368-2) — 초안 · 저장분이 바뀌면 측점 계획을 다시 맞춤
|
||||
const stopPlus = store.subscribe("plusStations", () => refresh(true));
|
||||
|
||||
return {
|
||||
setTypes(next) {
|
||||
types = next;
|
||||
refresh(false);
|
||||
},
|
||||
setStored(next) {
|
||||
stored = next;
|
||||
},
|
||||
set(next, carryEdits = true) {
|
||||
if (next.structures) structures = next.structures;
|
||||
if (next.pipes) reported = [...next.pipes];
|
||||
refresh(carryEdits);
|
||||
},
|
||||
follow() {
|
||||
structures = store.get("structures") ?? stored;
|
||||
refresh(false);
|
||||
},
|
||||
setIntervalM(next) {
|
||||
if (next > 0 && next !== intervalM) {
|
||||
intervalM = next;
|
||||
refresh(false);
|
||||
}
|
||||
},
|
||||
clearProjected() {
|
||||
reported = [];
|
||||
pipes = [];
|
||||
stations = [];
|
||||
paint(true);
|
||||
listeners.forEach(run);
|
||||
},
|
||||
paint: () => refresh(false, true),
|
||||
pipes: () => pipes,
|
||||
structures: () => structures,
|
||||
types: () => types,
|
||||
marks,
|
||||
stations: () => stations,
|
||||
intervalM: () => intervalM,
|
||||
onChange(fn) {
|
||||
listeners.add(fn);
|
||||
return () => void listeners.delete(fn);
|
||||
},
|
||||
dispose() {
|
||||
stopPlus();
|
||||
listeners.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import type { DesignView } from "./design_dock";
|
||||
import { saveSharedDesign, type DesignPageShared } from "./design_shared";
|
||||
import { guardDesignLeave } from "./design_leave";
|
||||
import { usePipeIds } from "./design_pipe_ids";
|
||||
import { createFacilityLane } from "./design_facility_lane";
|
||||
import { pinDraftProject } from "./design_store";
|
||||
import { pipelineDetail, reconcilePagePipeline } from "./design_pipeline_page";
|
||||
import { forgetPageData, onPageBusy } from "./design_page_data";
|
||||
@@ -149,7 +150,8 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
panel: null,
|
||||
profile: null,
|
||||
structures: null,
|
||||
pipeOptionQueue: [],
|
||||
// 시설 줄 한 벌(462) — 보기가 숨어도 · 아직 안 섰어도 듣고 셈 · 종단도는 서면 받음
|
||||
lane: createFacilityLane(projectId, () => shared.profile),
|
||||
revealLeft: () => layout?.setOptionsOpen(true),
|
||||
ensureView: (view) => (slots.get(view) ?? mountView(view, false)).ready,
|
||||
};
|
||||
@@ -262,8 +264,10 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
void reconcilePagePipeline(projectId); // 들어올 때 입력(세션 · 가게) 대 묶음 맞춤(365-2)
|
||||
} else void mountView(view, true).ready.then(() => reconcilePagePipeline(projectId)); // 첫 마운트 뒤도(365-5)
|
||||
// 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다.
|
||||
const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } };
|
||||
probe.__designPage ??= { view, switchMs: [] };
|
||||
const probe = window as unknown as {
|
||||
__designPage?: { view: string; switchMs: number[]; lane?: unknown };
|
||||
};
|
||||
probe.__designPage ??= { view, switchMs: [], lane: shared.lane }; // 시설 줄(462) 알약 · 측점
|
||||
probe.__designPage.view = view;
|
||||
probe.__designPage.switchMs.push(Math.round(performance.now() - started));
|
||||
};
|
||||
@@ -281,6 +285,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
slots.forEach((slot) => slot.dispose());
|
||||
slots.clear();
|
||||
shared.profile?.dispose();
|
||||
shared.lane.dispose();
|
||||
stopBusy();
|
||||
forgetPageData(projectId, true); // 다시 열면 저장분 · 노선 id 새로(386-2)
|
||||
delete (window as unknown as { __designPage?: unknown }).__designPage;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* 한 벌이 받은 사용자 조작(종단도 측점 고름 · 구조물 폼 · 우클릭)은 **지금 보이는 보기**가
|
||||
* `bindDesignView` 로 건 동작으로 간다. 값이 바뀐 알림(관 목록 · 횡단 재계산)은 숨은 보기도
|
||||
* 받는다 — 다시 보일 때 같은 값을 들고 있게.
|
||||
* 시설 줄(알약 · 비정규 측점 · 관 id 장부)은 페이지 한 벌(`design_facility_lane`) — 관 알림 · 노선 따라감을 여기서 넘김.
|
||||
* [저장] · [확정]은 두 보기 몫을 모아 저장 한 줄기(`saveDesign`) 한 번으로 보낸다. 종단 보기의
|
||||
* [확정]은 횡단 보기를 숨긴 채 세워 횡단 자료를 같은 페이지에서 얻는다(`ensureView`).
|
||||
* ========================================================================== */
|
||||
@@ -15,6 +16,7 @@ import { showToast } from "@ui/ui_template_elements";
|
||||
import { splitSidebarActions } from "@ui/ui_template_overlay";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { goToWorkflowStage, WORKFLOW_STEP_ROUTES } from "../b_workflow_nav";
|
||||
import { readState, writeState } from "../b_page_state";
|
||||
import { currentRoute, navigateTo, remountRoute } from "../router";
|
||||
import { clearRouteLatestCache } from "../../B05_Profile/B05_Profile_Api_Fetch";
|
||||
import { fetchCurrentRouteId } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
@@ -29,6 +31,7 @@ import { saveDesign, type DesignSaveStep } from "./design_save";
|
||||
import { markDesignClean } from "./design_leave";
|
||||
import { resetDesign } from "./design_reset";
|
||||
import type { DesignView } from "./design_dock";
|
||||
import type { FacilityLane } from "./design_facility_lane";
|
||||
import { routeDraftBridge, type PagePipelineOptions } from "./design_pipeline_page";
|
||||
import { plantedBase } from "./design_pipeline_base";
|
||||
import { isUserPick } from "./design_user_pick";
|
||||
@@ -107,8 +110,8 @@ export interface DesignPageShared {
|
||||
structures: StructuresSection | null;
|
||||
/** 배수유역도가 마지막으로 알린 관 목록 — 뒤늦게 서는 보기가 처음부터 같은 관 목록을 쥔다(24-7a). */
|
||||
lastPipes?: Parameters<NonNullable<RouteProfilePanelCallbacks["onPipesChanged"]>>[0];
|
||||
/** 횡단 보기가 서기 전에 종단 보기가 고친 관 옵션 — 횡단 보기가 서면 캐시에 얹는다. */
|
||||
pipeOptionQueue: Array<[number, Record<string, number | string>]>;
|
||||
/** 시설 줄 한 벌(462) — 알약 · 비정규 측점 · 흙덮기 관 · 관 id 장부 · 평면 마커 원천. 보기는 읽기만. */
|
||||
lane: FacilityLane;
|
||||
/** 접힌 좌측 패널을 편다(바깥에서 고른 것이 폼에 실릴 때). */
|
||||
revealLeft: () => void;
|
||||
/** 그 보기를 (숨긴 채라도) 세우고 자료가 설 때까지 기다린다. */
|
||||
@@ -129,7 +132,7 @@ export function bindDesignView(
|
||||
/**
|
||||
* 종단 보기 폼에서 고친 관 옵션을 횡단 캐시에도 얹는다 — 세월교 · BOX 폼 값은 횡단 스펙(캐시)이
|
||||
* 정본이라, 안 얹으면 한 벌 폼이 횡단 보기로 가는 순간 옛 값으로 돌아간다. 횡단 보기가 아직
|
||||
* 안 섰으면 담아 두었다가 설 때 얹는다.
|
||||
* 안 섰으면 세션 칸에 담아 두었다가 설 때 얹는다(새로고침해도 남음 · 462).
|
||||
*/
|
||||
export function forwardPipeOptions(
|
||||
shared: DesignPageShared,
|
||||
@@ -138,17 +141,23 @@ export function forwardPipeOptions(
|
||||
): void {
|
||||
if (!patch || !Object.keys(patch).length) return;
|
||||
const apply = shared.bindings.cross?.onPipeOptions;
|
||||
if (apply) apply(chainageM, patch);
|
||||
else shared.pipeOptionQueue.push([chainageM, patch]);
|
||||
if (apply) return apply(chainageM, patch);
|
||||
const queue = readState<PipeOptionQueue>("pipe-option-queue", shared.projectId) ?? [];
|
||||
writeState("pipe-option-queue", [...queue, [chainageM, patch]], shared.projectId);
|
||||
}
|
||||
|
||||
type PipeOptionQueue = Array<[number, Record<string, number | string>]>;
|
||||
|
||||
/** 횡단 보기가 캐시 창구를 건다 — 담아 둔 옵션을 먼저 얹는다. */
|
||||
export function bindPipeOptions(
|
||||
shared: DesignPageShared,
|
||||
apply: (chainageM: number, patch: Record<string, number | string>) => void,
|
||||
): void {
|
||||
bindDesignView(shared, "cross", { onPipeOptions: apply });
|
||||
shared.pipeOptionQueue.splice(0).forEach(([chainageM, patch]) => apply(chainageM, patch));
|
||||
const queue = readState<PipeOptionQueue>("pipe-option-queue", shared.projectId);
|
||||
if (!queue?.length) return;
|
||||
writeState("pipe-option-queue", null, shared.projectId);
|
||||
queue.forEach(([chainageM, patch]) => apply(chainageM, patch));
|
||||
}
|
||||
|
||||
/** 지금 보기의 종단도 알림으로 보낸다. */
|
||||
@@ -194,6 +203,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile {
|
||||
callbacks: {
|
||||
onPipesChanged: (pipes) => {
|
||||
shared.lastPipes = pipes;
|
||||
shared.lane.set({ pipes }); // 시설 줄 먼저 — 보기 알림은 맞춘 값을 읽음
|
||||
toAll(shared, "onPipesChanged")(pipes);
|
||||
},
|
||||
onCrossDesignsUpdated: (updated) => crossDesignsUpdated(shared, updated),
|
||||
@@ -214,6 +224,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile {
|
||||
attachProfilePopout(profile, shared.profileSlot, popoutButton);
|
||||
attachSubPanelPopouts(profile);
|
||||
shared.profile = profile;
|
||||
shared.lane.paint(); // 종단도가 서기 전 시설 줄이 먼저 셌으면 그 값
|
||||
if (shared.projectId) startPagePipeline(shared, profile);
|
||||
return profile;
|
||||
}
|
||||
@@ -227,7 +238,10 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
routeDraftBridge(projectId, profile, {
|
||||
stationIntervalM: SECTION_STATION_INTERVAL_M,
|
||||
onCorridorStale: () => each((hooks) => hooks.onCorridorStale?.()),
|
||||
onFacilitiesFollowed: (far) => each((hooks) => hooks.onFacilitiesFollowed?.(far)),
|
||||
onFacilitiesFollowed: (far) => {
|
||||
shared.lane.follow(); // 가게 관 · 구조물 = 따라간 자리(편집은 묶음이 옮김)
|
||||
each((hooks) => hooks.onFacilitiesFollowed?.(far));
|
||||
},
|
||||
onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"),
|
||||
// 초안 소단 = 지금 구조물(따라간 자리)의 소단 구간(365-5 · 저장 노선은 다시 셈 창구가 세션 사본으로)
|
||||
bermAt: (structures) => {
|
||||
|
||||
@@ -66,14 +66,13 @@ import "../../B05_Profile/B05_Profile_UI_Style.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Table.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Drainage.css";
|
||||
import "../../B05_Profile/B05_Profile_UI_Style_Structures.css";
|
||||
import { pipeIdAt, placePipeIds, prunePipeIds } from "./design_pipe_ids";
|
||||
import { pipeIdAt } from "./design_pipe_ids";
|
||||
import { useDesignStore } from "./design_store";
|
||||
import { setPlusStations } from "../../common_util/common_util_structure_station";
|
||||
import {
|
||||
loadSavedPlusStations,
|
||||
plusStationsOf,
|
||||
} from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||
import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow";
|
||||
import { STRUCTURE_ROUTE_DRIFT_MAX_M } from "../../common_util/common_util_route_follow";
|
||||
import {
|
||||
DEFAULT_ROAD_WIDTHS,
|
||||
@@ -84,7 +83,10 @@ import {
|
||||
} from "../../B05_Profile/B05_Profile_UI_Page_Helpers";
|
||||
import { createProfileSections, type ProfileViewState } from "./design_view_profile_sections";
|
||||
import { createProfile3d } from "./design_view_profile_3d";
|
||||
import { setPlanMarkSource } from "../../B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks";
|
||||
import {
|
||||
planMarksChanged,
|
||||
setPlanMarkSource,
|
||||
} from "../../B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks";
|
||||
|
||||
type PipeReport = Parameters<NonNullable<RouteProfilePanelCallbacks["onPipesChanged"]>>[0];
|
||||
|
||||
@@ -113,38 +115,10 @@ export async function mountProfileView(
|
||||
/** 배수유역도가 마지막으로 알린 관 목록 — 숨어 있다 다시 보일 때 폼 · 3D 에 다시 싣는다. */
|
||||
let lastPipes: PipeReport | null = shared.lastPipes ?? null;
|
||||
const designStore = useDesignStore(activeProjectId, null);
|
||||
/** 화면 관 목록 = 배수유역도 알림을 가게 관 초안 측점으로(노선 따라 다시 매김 · 349-0d) · 관 세션 id 도
|
||||
* 그 자리로 — 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 · 되돌려도 그 관) · id 없는 관(막 놓은 관)은 자리로. */
|
||||
let sessionIds = new Map<string, string>(); // 관 고정 id → 세션 id(알약 id)
|
||||
const shownPipes = (pipes: PipeReport): PipeReport => {
|
||||
const shown = followPipeReports(pipes, designStore.get("pipes")).sort(
|
||||
(x, y) => x.chainage_m - y.chainage_m,
|
||||
);
|
||||
placePipeIds(
|
||||
shown.flatMap((pipe) => {
|
||||
const sid = pipe.id && sessionIds.get(pipe.id);
|
||||
return sid ? [[sid, pipe.chainage_m] as const] : [];
|
||||
}),
|
||||
activeProjectId,
|
||||
);
|
||||
sessionIds = new Map(
|
||||
shown.flatMap((pipe) =>
|
||||
pipe.id ? [[pipe.id, pipeIdAt(pipe.chainage_m, activeProjectId)] as const] : [],
|
||||
),
|
||||
);
|
||||
prunePipeIds(
|
||||
shown.map((pipe) => pipe.chainage_m),
|
||||
activeProjectId,
|
||||
); // 헛 id 정리(411-E)
|
||||
return shown;
|
||||
};
|
||||
const applyPipes = (reported: PipeReport, carryEdits = true): void => {
|
||||
const pipes = shownPipes(reported);
|
||||
// 횡단배수는 계획선을 그 지점에 물리는 시설이라 그래프 세로 점선·계획고 틸팅
|
||||
// 버튼이 필요하다(2026-08-17 사용자 지시). 알약 레인은 종류·선택을 맡는다.
|
||||
bridge.setPipes(pipes, carryEdits);
|
||||
panel.structures.setPipeFacilities(pipes.map(pipeToFacilityItem));
|
||||
profilePanel.setMinCoverPipes(pipes);
|
||||
/** 화면 관 목록 = 시설 줄 한 벌(462 · 가게 관 초안 측점 · 관 세션 id 장부 · 알약 · 측점선 · 흙덮기는 그쪽이
|
||||
* 숨어도 셈) — 이 보기는 보일 때 좌측 목록만 맞춤. */
|
||||
const applyPipes = (): void => {
|
||||
panel.structures.setPipeFacilities((shared.lane.pipes() as PipeReport).map(pipeToFacilityItem));
|
||||
snapOld();
|
||||
restorePick(); // 관 목록이 실린 뒤라야 폼이 그 시설을 찾는다.
|
||||
};
|
||||
@@ -159,9 +133,9 @@ export async function mountProfileView(
|
||||
const followed = designStore.get("pipes");
|
||||
if (followed) profilePanel.drainage.followPipes(followed);
|
||||
else if (profilePanel.drainage.pipeChainages().length) profilePanel.drainage.reload();
|
||||
// 숨어도 관 세션 id 장부는 따라감 — 평면 마커 누름이 초안 자리를 고름(440-3) · 그리기는 보일 때
|
||||
if (!host.visible()) return void (lastPipes && shownPipes(lastPipes));
|
||||
if (lastPipes) applyPipes(lastPipes, false);
|
||||
// 관 세션 id 장부 · 측점선 · 알약은 시설 줄이 따라감(숨어도 · 462) — 이 보기는 보일 때 목록만
|
||||
if (!host.visible()) return;
|
||||
if (lastPipes) applyPipes();
|
||||
bridge.takeStructures(designStore.get("structures"));
|
||||
};
|
||||
/** 측점선 강조 — 측점선이 없는 관 시설(물넘이포장 · 비정규 측점 아님)은 알약을 id 로 켬(53-2) ·
|
||||
@@ -214,8 +188,7 @@ export async function mountProfileView(
|
||||
// 관 지점이다. 숨어 있을 때는 담아만 두고 다시 보일 때 싣는다(폼은 한 벌이라 횡단 보기 몫).
|
||||
onPipesChanged: (pipes) => {
|
||||
lastPipes = pipes;
|
||||
if (host.visible()) applyPipes(pipes);
|
||||
else shownPipes(pipes); // 숨어도 관 id 장부만(440-3)
|
||||
if (host.visible()) applyPipes(); // 관 id 장부 · 측점은 시설 줄이 먼저 맞춤(462)
|
||||
},
|
||||
onStructureMove: (from, to, station) => {
|
||||
if (isPipeStation(station)) profilePanel.drainage.movePipe(from, to);
|
||||
@@ -389,15 +362,21 @@ export async function mountProfileView(
|
||||
const bridge = createStructuresBridge({
|
||||
projectId: activeProjectId,
|
||||
panel: () => panel,
|
||||
profilePanel: () => profilePanel,
|
||||
renderStationLines: () => sections.redrawStationLines(),
|
||||
lane: shared.lane,
|
||||
isRestoring: () => state.restoring,
|
||||
});
|
||||
// 평면 구조물 측점 마커(399) — 알약과 같은 목록 · 고름 · 옮김
|
||||
// 시설 줄이 바뀜(어느 보기 고침 · 노선 따라감 · 숨은 동안도) — 평면 마커 · 보일 때 3D 측점선
|
||||
host.onDispose(
|
||||
shared.lane.onChange(() => {
|
||||
planMarksChanged(activeProjectId);
|
||||
if (host.visible()) sections.redrawStationLines();
|
||||
}),
|
||||
);
|
||||
// 평면 구조물 측점 마커(399) — 알약과 같은 목록(시설 줄) · 고름 · 옮김
|
||||
host.onDispose(
|
||||
setPlanMarkSource(activeProjectId, {
|
||||
marks: bridge.marks,
|
||||
types: bridge.types,
|
||||
marks: shared.lane.marks,
|
||||
types: shared.lane.types,
|
||||
intervalM: () => panel.values().stationInterval || 20,
|
||||
select: onStructureSelect,
|
||||
move: onStructureMarkMove,
|
||||
@@ -447,10 +426,7 @@ export async function mountProfileView(
|
||||
// 탭이 다시 보일 때 정의 판이 바뀌었으면 종류 · 패널 다시 읽음(322-0)
|
||||
const reloadIfShown = (): void => void (host.visible() && !state.restoring && bridge.load());
|
||||
host.onDispose(onStaleStructureTypes(reloadIfShown));
|
||||
// 추가 측점(368-2) — 초안 · 저장분이 바뀌면 측점 계획을 다시 맞춤(종단 측점선 · 3D)
|
||||
host.onDispose(
|
||||
useDesignStore(activeProjectId, null).subscribe("plusStations", () => bridge.resyncStations()),
|
||||
);
|
||||
// 추가 측점(368-2) — 초안 · 저장분이 바뀌면 시설 줄이 측점 계획을 다시 맞춤(종단 측점선 · 3D)
|
||||
setPlusStations(() => plusStationsOf(activeProjectId)); // 저장분이 늦게 와도 한 번 다시 맞춤
|
||||
void loadSavedPlusStations(activeProjectId).then(() => bridge.resyncStations());
|
||||
// 한 페이지 두 보기 — 숨김 · 보임 · 떠남(계획서 24-3a · 24-3b).
|
||||
@@ -468,7 +444,7 @@ export async function mountProfileView(
|
||||
reload: async () => {
|
||||
sections.flushSwap(); // 숨은 동안 측점 목록이 바뀌었으면 종단도 · 3D 측점선 다시(346-2)
|
||||
await bridge.load();
|
||||
if (lastPipes) applyPipes(lastPipes);
|
||||
if (lastPipes) applyPipes();
|
||||
},
|
||||
syncPick: () => {
|
||||
if (readStructurePick(activeProjectId)) {
|
||||
@@ -571,7 +547,7 @@ export async function mountProfileView(
|
||||
if (latestResponse.route && routeId !== null) await restoreSections(routeId);
|
||||
// 횡단 보기가 먼저 서서 배수유역도가 관 목록을 이미 알렸다면 그것을 먼저 싣는다 — 빈 목록으로
|
||||
// 시작하면 아래 구조물 로딩이 관 대조로 관을 지우고 초안 옵션이 자동 배치로 되돌아간다(24-7a).
|
||||
if (lastPipes) applyPipes(lastPipes);
|
||||
if (lastPipes) applyPipes();
|
||||
// 구조물 타입 레지스트리·정본 — 노선이 없어도 목록은 보여 준다(추가는 노선 이후).
|
||||
await bridge.load();
|
||||
advanceLoading("");
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
* B05_Profile_UI_Page_Structures.ts
|
||||
* 구조물·계곡 통과 시설을 화면 세 곳(사이드 목록·종단 그래프·3D)에 맞추는 다리.
|
||||
*
|
||||
* 화면 본체(B05_Profile_UI_Page)가 700줄 한계에 닿아 분리했다. 두 정본을 섞어
|
||||
* 쓰는 자리라 상태를 여기로 옮겨 왔다 — 구조물 정본(structures.json)의 목록·판번호와,
|
||||
* 관 지점 정본(pipe_points.json)에서 투영한 측점·알약이 그것이다. 본체는 이 객체의
|
||||
* 메서드만 부르고 목록은 `irregularStations()`로 읽는다.
|
||||
* 화면 본체(B05_Profile_UI_Page)가 700줄 한계에 닿아 분리했다. 여기 몫 = 구조물 정본(structures.json)의
|
||||
* 목록 · 종류 · 판번호 · 저장. 알약 · 비정규 측점 · 관 투영은 페이지 시설 줄 한 벌(`design_facility_lane` · 462)이
|
||||
* 셈 — 이 다리는 목록을 넘기고 `irregularStations()` · `marks()` 는 시설 줄을 읽음.
|
||||
*
|
||||
* 저장 규칙은 종전 그대로다 — 사이드 목록이 바뀌면 곧바로 서버 정본에 저장하고,
|
||||
* 판번호가 밀리면(다른 창이 먼저 저장) 최신본을 받아 화면을 맞추고 사용자에게 알린다.
|
||||
@@ -13,7 +12,6 @@
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { mountViewerStructureMenu } from "./B05_Profile_UI_Viewer_Menu";
|
||||
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
||||
import {
|
||||
fetchStructures,
|
||||
fetchStructureTypes,
|
||||
@@ -26,77 +24,24 @@ import {
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "./B05_Profile_Api_Structures";
|
||||
import type { IrregularStation } from "./B05_Profile_UI_IrregularStations";
|
||||
import type { createRoutePanel } from "./B05_Profile_UI_Panel";
|
||||
import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel";
|
||||
import { followedStation } from "./logic/B05_Profile_UI_Profile_Alignment";
|
||||
import { followSnapped } from "./B05_Profile_UI_Structures_Snap";
|
||||
import { plantsStation } from "./B05_Profile_UI_Profile_Structures";
|
||||
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 { pipeChainageOf } from "../A00_Common/design/design_pipe_ids";
|
||||
import { pageStructures, pageTypesChecked } from "../A00_Common/design/design_page_data";
|
||||
import { planMarksChanged } from "./B05_Profile_UI_RouteEdit_StructureMarks";
|
||||
|
||||
/** 관 지점 한 건 — 유효직경·시설 종류·부속 옵션까지 그대로 받는다. */
|
||||
export interface PipeProjection {
|
||||
chainage_m: number;
|
||||
effective_diameter_mm: number | null;
|
||||
facility: PipeFacility;
|
||||
options?: Record<string, string | number>;
|
||||
/** 독립 기슭막이 구간 — 시작 · 끝도 측점을 심는다(272 · 서버와 같은 규칙). */
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
}
|
||||
import type { FacilityLane } from "../A00_Common/design/design_facility_lane";
|
||||
|
||||
export interface StructuresBridgeDeps {
|
||||
projectId: string;
|
||||
/** 사이드 패널 — 생성 순서상 나중에 만들어지므로 게터로 받는다. */
|
||||
panel: () => ReturnType<typeof createRoutePanel>;
|
||||
profilePanel: () => ReturnType<typeof createRouteProfilePanel>;
|
||||
/** 3D 측점 세로선 다시 그리기 — 상세가 아직 없으면 본체가 알아서 건너뛴다. */
|
||||
renderStationLines: () => void;
|
||||
/** 페이지 시설 줄 한 벌(462) — 알약 · 비정규 측점은 여기서 셈. */
|
||||
lane: FacilityLane;
|
||||
/** 복원 중에는 서버로 저장하지 않는다(되살린 값을 그대로 되쓰지 않기 위함). */
|
||||
isRestoring: () => boolean;
|
||||
}
|
||||
|
||||
export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
/** 비정규 측점(그래프 세로선·3D 라벨) — 측점 계획 한 벌(관 · 측점 심는 구조물 · 322-3). */
|
||||
let irregularStations: IrregularStation[] = [];
|
||||
|
||||
/** [초기선 복원] 등에서 그래프의 배관 투영만 지운다 — 관 정본은 건드리지 않는다. */
|
||||
function clearProjectedStations(): void {
|
||||
planPipes = [];
|
||||
irregularStations = [];
|
||||
deps.renderStationLines();
|
||||
deps.profilePanel().setIrregularStations([]);
|
||||
}
|
||||
|
||||
/** 비정규 측점 목록 변경 → 3D·그래프·테이블에 반영(프론트 프리뷰, 백엔드 미전송).
|
||||
* `carryEdits` false = 노선이 움직여 따라간 측점(계획고 편집은 계산 묶음 follow 가 옮김 · 349-0d). */
|
||||
function applyIrregularStations(stations: IrregularStation[], carryEdits = true): void {
|
||||
// 위치가 바뀌거나 삭제된 비정규 측점의 옛 chainage에 남은 계획고 편집(유령 변화점)을 옮기거나 지운다.
|
||||
// 옮김 = 0.006m 안 새 측점(서버 0.01m 반올림) · 같은 id · 사라진 것 하나와 새것 하나(옮긴 관 · 213-2)
|
||||
// — 세션 초안도 새 키로 바뀌어 옛 키가 다시 안 나감. 나머지는 지움.
|
||||
const nextKeys = new Set(stations.map((station) => station.chainage_m.toFixed(3)));
|
||||
const oldKeys = new Set(irregularStations.map((station) => station.chainage_m.toFixed(3)));
|
||||
const gone = irregularStations.filter(
|
||||
(station) => !nextKeys.has(station.chainage_m.toFixed(3)),
|
||||
);
|
||||
const born = stations.filter((station) => !oldKeys.has(station.chainage_m.toFixed(3)));
|
||||
if (carryEdits)
|
||||
gone.forEach((station) => {
|
||||
// 없으면 맞춘 자리의 새 측점 — 옛 소수 자동 맞춤이 여럿을 한꺼번에 옮겨도 편집이 따라감(234-1 · 272)
|
||||
const same = followedStation(station, gone, born) ?? followSnapped(station, born);
|
||||
deps.profilePanel().resetStationEdit(station.chainage_m, same?.chainage_m);
|
||||
});
|
||||
irregularStations = stations;
|
||||
deps.renderStationLines();
|
||||
deps.profilePanel().setIrregularStations(stations);
|
||||
// 배수유역도로 되먹이지 않음(322-3) — 관 고침 · 지움은 폼 콜백이 배수 패널로 바로 감(관 정본이 원천)
|
||||
}
|
||||
|
||||
/* ── 구조물 정본(structures.json) ────────────────────────────────────
|
||||
* 목록이 바뀌면 **세션 초안**에 담고(`writePending`), 정본에는 [저장]·[확정]에서만
|
||||
* 쓴다(`saveStructuresIfDirty`). 판번호가 밀리면(다른 창이 먼저 저장) 최신본을
|
||||
@@ -106,29 +51,11 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
let structureRevision = 0;
|
||||
let structureSaving: Promise<void> = Promise.resolve();
|
||||
|
||||
/* ── 횡단배수(A군) 측점 세로선 ──────────────────────────────────────────
|
||||
* 횡단배수 시설은 계획선을 그 지점에 물리므로 그래프에 세로 점선과 계획고 틸팅
|
||||
* 버튼(▲▼)이 있어야 한다(2026-08-17 사용자 지시). 대상은 관 정본(배수관·BOX암거·
|
||||
* 물넘이·세월교)에 더해 수동 A군(노출형 횡단수로·개거)까지다. */
|
||||
/** 측점 계획 입력 — 관(배수 패널이 알려 줌) · 종류 표(정의 기본값 · 카드 이름). */
|
||||
let planPipes: PipeProjection[] = [];
|
||||
let planTypes: StructureType[] = [];
|
||||
let structureTypeMap = new Map<
|
||||
string,
|
||||
{ type_id: string; group: string; name: string; placement: string; managed_by: string | null }
|
||||
>();
|
||||
|
||||
/** 그래프 · 3D 측점 = 측점 계획 한 벌(322-3 · 카드 · B06 종단 · 서버와 같은 목록 · 같은 m 하나 · 관이 먼저).
|
||||
* 종류 표를 받기 전이면 미룸(받으면 다시 맞춤). */
|
||||
function syncCrossDrainStations(carryEdits = true): void {
|
||||
if (!planTypes.length) return;
|
||||
const interval = deps.panel().values().stationInterval || 20;
|
||||
applyIrregularStations(
|
||||
plannedIrregular(planStations(withPipeIds(planPipes), ownStructures, planTypes), interval),
|
||||
carryEdits,
|
||||
);
|
||||
}
|
||||
|
||||
/** 알약 id의 누가거리 — 구조물이면 정본에서, 관이면 id 에서 되읽는다(없으면 null).
|
||||
* 3D·사이드 목록과 마찬가지로 **종단 알약 선택도 B06 으로 이어져야** 해서 둔다
|
||||
* (2026-09-06: 알약만 `structure-pick` 을 안 써 B06 이 못 잡았음). */
|
||||
@@ -147,41 +74,10 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
return structureId ? pipeChainageOf(structureId) : null; // 관 세션 id 장부(323-5)
|
||||
}
|
||||
|
||||
/** 그래프 알약 레인에 올릴 목록 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합친다.
|
||||
* 자동 배수관도 세로 점선이 아니라 같은 알약으로 나온다(2026-08-17 사용자 지시 1). */
|
||||
/** 구조물 정본 목록(미저장분 우선) — 알약 레인 · 측점 계획은 시설 줄이 이것 + 관으로 셈. */
|
||||
let ownStructures: StructureInstance[] = [];
|
||||
/** 서버 정본 목록 — 가게 초안이 비면(노선 초안을 버림) 이것으로. */
|
||||
let storedStructures: StructureInstance[] = [];
|
||||
let pipeMarks: StructureInstance[] = [];
|
||||
|
||||
function syncGraphStructures(): void {
|
||||
deps.profilePanel().setStructures([...ownStructures, ...pipeMarks]);
|
||||
planMarksChanged(deps.projectId); // 평면 구조물 마커 = 알약과 같은 목록(399)
|
||||
}
|
||||
|
||||
/** 관 지점을 알약 레인용 가상 구조물로 만든다(정본은 pipe_points, 저장하지 않는다). */
|
||||
function pipesToMarks(
|
||||
pipes: Array<{
|
||||
chainage_m: number;
|
||||
facility: PipeFacility;
|
||||
options?: Record<string, string | number>;
|
||||
}>,
|
||||
): StructureInstance[] {
|
||||
return pipes.map((pipe) => ({
|
||||
structure_id: pipeIdAt(pipe.chainage_m), // 관 세션 id(323-5)
|
||||
type_id: pipe.facility,
|
||||
placement: "point",
|
||||
chainage_m: pipe.chainage_m,
|
||||
start_m: null,
|
||||
end_m: null,
|
||||
options: pipe.options ?? {},
|
||||
memo: "",
|
||||
placement_source: "automatic",
|
||||
status: "draft",
|
||||
revision: 0,
|
||||
geometry: null,
|
||||
}));
|
||||
}
|
||||
|
||||
/* ── 미저장 조작분 세션 보관(2026-08-29 — CLAUDE.md 5장) ──────────────
|
||||
* 조작은 화면과 세션에만 남기고 영구저장은 [저장]·[확정]에서만 한다. 세션에 두는
|
||||
@@ -201,8 +97,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
|
||||
function applyStructures(next: StructureInstance[]): void {
|
||||
ownStructures = withLocalIds(next);
|
||||
syncGraphStructures();
|
||||
syncCrossDrainStations();
|
||||
deps.lane.set({ structures: ownStructures });
|
||||
// 복원 중 호출은 사용자 조작이 아니다 — 미저장 표시를 남기지 않는다.
|
||||
if (!deps.isRestoring()) {
|
||||
deps.panel().structures.setStructures(ownStructures);
|
||||
@@ -230,8 +125,8 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
structureRevision = stored.revision;
|
||||
deps.panel().structures.setStructures(stored.structures);
|
||||
ownStructures = storedStructures = stored.structures;
|
||||
syncGraphStructures();
|
||||
syncCrossDrainStations();
|
||||
deps.lane.setStored(storedStructures);
|
||||
deps.lane.set({ structures: ownStructures }, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -284,8 +179,7 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
pageStructures(deps.projectId),
|
||||
]);
|
||||
deps.panel().structures.setTypes(allTypes, listPick("b05")); // 표 목록 both(322-1)
|
||||
deps.profilePanel().setStructureTypes(allTypes);
|
||||
planTypes = allTypes;
|
||||
deps.lane.setTypes(allTypes);
|
||||
// A군 판정에 쓸 타입 정보 — 횡단배수만 그래프 세로선·틸팅 대상이다.
|
||||
structureTypeMap = new Map(
|
||||
allTypes.map((type) => [
|
||||
@@ -301,14 +195,14 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
);
|
||||
structureRevision = stored.revision;
|
||||
storedStructures = stored.structures;
|
||||
deps.lane.setStored(storedStructures);
|
||||
const note = staleDefinitionNotice(deps.projectId, stored.definition_version); // 322-2
|
||||
if (note) showToast(note);
|
||||
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(2026-08-29).
|
||||
const pending = readPending();
|
||||
ownStructures = pending ?? stored.structures;
|
||||
deps.panel().structures.setStructures(ownStructures);
|
||||
syncGraphStructures();
|
||||
syncCrossDrainStations();
|
||||
deps.lane.set({ structures: ownStructures }, false);
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.",
|
||||
@@ -335,43 +229,35 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) {
|
||||
onPick: (chainage, typeId) => deps.panel().structures.addAt(chainage, typeId),
|
||||
});
|
||||
},
|
||||
/** 측점 계획을 다시 맞춤 — 추가 측점이 바뀜(368-2). */
|
||||
resyncStations: () => syncCrossDrainStations(),
|
||||
/** 알약 레인 목록 · 종류 — 평면 구조물 마커가 같은 것을 씀(399). */
|
||||
marks: (): StructureInstance[] => [...ownStructures, ...pipeMarks],
|
||||
types: (): StructureType[] => planTypes,
|
||||
/** 그래프·3D가 쓰는 현재 비정규 측점 목록(물넘이포장 제외 — 알약 레인 몫). */
|
||||
irregularStations: () => irregularStations,
|
||||
/** 측점 계획을 다시 맞춤 — 추가 측점 저장분이 늦게 옴(368-2). */
|
||||
resyncStations: () => deps.lane.set({}),
|
||||
/** 알약 레인 목록 · 종류(시설 줄) — 평면 구조물 마커가 같은 것을 씀(399). */
|
||||
marks: (): StructureInstance[] => deps.lane.marks(),
|
||||
types: (): StructureType[] => deps.lane.types(),
|
||||
/** 그래프·3D가 쓰는 현재 비정규 측점 목록(시설 줄 · 물넘이포장 제외 — 알약 레인 몫). */
|
||||
irregularStations: () => deps.lane.stations(),
|
||||
/** 구조물 종류의 군(A~G · 모르면 undefined) — 측점 고름이 덮어 열 구조물을 가름(216-1). */
|
||||
typeGroup: (typeId: string) => structureTypeMap.get(typeId)?.group,
|
||||
/** 관 지점 목록이 바뀜 — 측점 투영과 알약 레인을 한 번에 맞춘다(`carryEdits` = 계획고 편집 옮김). */
|
||||
setPipes(pipes: PipeProjection[], carryEdits = true): void {
|
||||
planPipes = [...pipes];
|
||||
syncCrossDrainStations(carryEdits);
|
||||
pipeMarks = pipesToMarks(pipes);
|
||||
syncGraphStructures();
|
||||
},
|
||||
/** [초기선 복원] — 그래프의 배관 투영만 지운다(관 정본은 배수유역이 맡는다). */
|
||||
clearProjectedStations,
|
||||
clearProjectedStations: () => deps.lane.clearProjected(),
|
||||
/** 알약 식별자에서 관 누가거리를 되읽는다(관이 아니면 null). */
|
||||
pipeMarkChainage,
|
||||
/** 측점 없는 관 알약(물넘이포장)인가 — 고름 값에 알약 id 를 남길지 가름(요구 178). */
|
||||
stationlessPipeMark: (structureId: string | null): boolean => {
|
||||
const mark = pipeMarks.find((entry) => entry.structure_id === structureId);
|
||||
if (pipeMarkChainage(structureId) === null) return false; // 관 알약만
|
||||
const mark = deps.lane.marks().find((entry) => entry.structure_id === structureId);
|
||||
return !!mark && !plantsStation(mark, []);
|
||||
},
|
||||
/** 알약 식별자의 누가거리(구조물·관 공통). B06 으로 넘길 선택값을 만들 때 쓴다. */
|
||||
markChainage,
|
||||
/** 사이드 목록이 바뀜 — 화면을 맞추고 서버 정본에 저장한다. */
|
||||
applyStructures,
|
||||
/** 가게 구조물 초안을 계산 묶음이 다시 씀(노선 따라 측점 다시 매김 · 되돌림 · 349-0d) — 화면만 맞춤. */
|
||||
/** 가게 구조물 초안을 계산 묶음이 다시 씀(노선 따라 측점 다시 매김 · 되돌림 · 349-0d) — 목록만 맞춤(측점은 시설 줄). */
|
||||
takeStructures(next: StructureInstance[] | null): void {
|
||||
const list = next ?? storedStructures;
|
||||
if (JSON.stringify(list) === JSON.stringify(ownStructures)) return;
|
||||
ownStructures = list;
|
||||
deps.panel().structures.setStructures(ownStructures);
|
||||
syncGraphStructures();
|
||||
syncCrossDrainStations(false);
|
||||
},
|
||||
saveStructuresIfDirty,
|
||||
/** 서버 정본을 다시 받아 화면을 그 상태로 맞춘다. */
|
||||
|
||||
@@ -205,13 +205,16 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
? inside.reduce((best, at) => (Math.abs(at - anchor) < Math.abs(best - anchor) ? at : best))
|
||||
: anchor;
|
||||
};
|
||||
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */
|
||||
const pushMarks = (): void =>
|
||||
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). 알약 · 측점선 · 관 id 장부 = 페이지
|
||||
* 시설 줄 한 벌(462 · 숨은 B05 평면 마커도 따라감) · `carryEdits` = 사용자 고침(불러옴은 false). */
|
||||
const pushMarks = (carryEdits = true): void => {
|
||||
deps.shared.lane.set({ structures, pipes: pipeFacilities }, carryEdits);
|
||||
deps.onMarks?.(
|
||||
[...structures, ...pipesToStructureMarks(pipeFacilities)],
|
||||
markTypes,
|
||||
pipeFacilities,
|
||||
);
|
||||
};
|
||||
// 폼 한 벌(계획서 24-3b) — 횡단 보기가 보일 때 폼 알림을 받는 동작만 건다. 상세 옵션 칸은
|
||||
// 한 벌이 보기에 따라 그린다(횡단 보기에서만).
|
||||
const section = sharedStructures(deps.shared);
|
||||
@@ -396,8 +399,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
]);
|
||||
section.setTypes(types, listPick("b06")); // 표 목록 both + b06(322-1)
|
||||
markTypes = types;
|
||||
deps.shared.lane.setTypes(types);
|
||||
// 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙).
|
||||
storedStructures = stored.structures;
|
||||
deps.shared.lane.setStored(storedStructures);
|
||||
storedPipes = pipeResponse.pipe_points;
|
||||
structures = readPendingStructures(projectId) ?? stored.structures;
|
||||
const note = staleDefinitionNotice(projectId, stored.definition_version); // 322-2
|
||||
@@ -438,7 +443,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
if (!pendingPipes) snapOldPipes(projectId);
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
// 종단 알약 레인 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합쳐 B05 와 같은 표기로.
|
||||
pushMarks();
|
||||
pushMarks(false);
|
||||
// 목록이 늦게 도착하면 그 사이에 고른 시설은 강조될 자리가 없었다 — 다시 세운다
|
||||
// (2026-09-04 사용자 보고: B06 좌측 목록만 하이라이트가 안 붙음). 폼에 아직 아무것도
|
||||
// 없으면 세션에 남은 선택(카드형 — 부재키 없는 측점)도 같은 규칙으로 세운다.
|
||||
|
||||
@@ -670,11 +670,10 @@ export function createSectionView(
|
||||
setStructureEdit(edit) {
|
||||
structureEdit = edit;
|
||||
},
|
||||
setStructureMarks(structures, types, pipes) {
|
||||
setStructureMarks(structures, types) {
|
||||
spanMarks = structures;
|
||||
spanTypes = types;
|
||||
syncSpanHighlights();
|
||||
profile.setMarks(structures, types, pipes);
|
||||
syncSpanHighlights(); // 종단 알약 · 측점선은 시설 줄 한 벌(462)
|
||||
// 검증용 훅 — 알약 레인이 무엇을 받았는지 화면 밖에서 수치로 본다
|
||||
// (`__corridorBuild` 등과 같은 용도).
|
||||
(window as unknown as { __b06Marks?: unknown }).__b06Marks = {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
*
|
||||
* 옛 위쪽 붙박이 종단(자체 그래프 · 유토곡선 없음 · 흙덮기 막기 없음)을 갈음한다.
|
||||
* 도구(▲▼ · 되돌리기 · 직선화 · 옮기기 · 확대 · 표 · 유토곡선 · 범례 · 뱃지)와 막기 규칙
|
||||
* (최소 흙덮기 · 기울기 상한)은 종단도 한 벌이 맡고, 여기서는 이음만 한다.
|
||||
* (최소 흙덮기 · 기울기 상한)은 종단도 한 벌이 맡고, 여기서는 이음만 한다. 알약 · 측점선 · 흙덮기 관은
|
||||
* 페이지 시설 줄 한 벌(`shared.lane` · 462)이 종단도에 씀 — 여기는 읽기만.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DesignProfile } from "../A00_Common/design/design_profile";
|
||||
@@ -32,24 +33,13 @@ import type {
|
||||
HaulEquipmentLimit,
|
||||
SectionDetailResponse,
|
||||
} from "./B06_Section_Api_Fetch";
|
||||
import {
|
||||
structureAnchorM,
|
||||
type StructureInstance,
|
||||
type StructureType,
|
||||
} from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import {
|
||||
irregularStationId,
|
||||
isPipeStation,
|
||||
type IrregularStation,
|
||||
} from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import { structureAnchorM } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||
import { irregularStationId, isPipeStation } from "../B05_Profile/B05_Profile_UI_IrregularStations";
|
||||
import {
|
||||
isPipeMark,
|
||||
moveMarkById,
|
||||
type SectionStructureEdit,
|
||||
} from "./B06_Section_UI_Section_View_Menu";
|
||||
import { planStations, type PlanPipe } from "../common_util/common_util_structure_station";
|
||||
import { plannedIrregular } from "../A00_Common/design/design_structure_status";
|
||||
import { withPipeIds } from "../A00_Common/design/design_pipe_ids";
|
||||
|
||||
/** 관 · 측점을 같은 자리로 보는 거리(m) — 계획서 24-1 `PIPE_MATCH_M` 과 같은 값. */
|
||||
const MATCH_M = PIPE_MATCH_M;
|
||||
@@ -79,11 +69,6 @@ export interface SectionProfile {
|
||||
haulLimits?: HaulEquipmentLimit[];
|
||||
naturalSpoilMinSlope?: number;
|
||||
}) => void;
|
||||
setMarks: (
|
||||
structures: ReadonlyArray<StructureInstance>,
|
||||
types: ReadonlyArray<StructureType>,
|
||||
pipes?: ReadonlyArray<PlanPipe>,
|
||||
) => void;
|
||||
/** 카드가 바뀌었다 — 유토곡선 · 뱃지만 다시 싣는다(편집 초안은 그대로). */
|
||||
redraw: () => void;
|
||||
/** 카드를 골랐다 — 종단도 선택 표시가 따라간다. */
|
||||
@@ -92,26 +77,6 @@ export interface SectionProfile {
|
||||
selectMark: (structureId: string | null) => void;
|
||||
}
|
||||
|
||||
/** 종단 측점선 = 측점 계획 한 벌(322-3 · 관 · 측점 심는 구조물 모두 · 종단 보기 · 카드와 같은 목록).
|
||||
* 관 구간(독립 기슭막이 시작 · 끝)은 받은 관 목록에서 · 없으면 알약에서 기준만. */
|
||||
function planMarkStations(
|
||||
marks: ReadonlyArray<StructureInstance>,
|
||||
pipes: ReadonlyArray<PlanPipe> | null,
|
||||
types: ReadonlyArray<StructureType>,
|
||||
intervalM: number,
|
||||
): IrregularStation[] {
|
||||
const pipeList =
|
||||
pipes ??
|
||||
marks.filter(isPipeMark).map((mark) => ({
|
||||
chainage_m: structureAnchorM(mark),
|
||||
facility: mark.type_id,
|
||||
options: mark.options,
|
||||
}));
|
||||
const structures = marks.filter((mark) => !isPipeMark(mark));
|
||||
// 덮는 구조물 id = 관 세션 id(323-5 · 알약 id 와 같음)
|
||||
return plannedIrregular(planStations(withPipeIds(pipeList), structures, types), intervalM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배수유역도에 노선 평면선을 싣는다 — 종단 보기가 두고 간 최신 노선 캐시를 먼저 쓰고,
|
||||
* 없으면 한 번 읽는다(읽기만 · 계산 없음).
|
||||
@@ -143,13 +108,10 @@ export function createSectionProfile(
|
||||
): SectionProfile {
|
||||
let profile: DesignProfile | null = null;
|
||||
let detail: SectionDetailResponse | null = null;
|
||||
let marks: ReadonlyArray<StructureInstance> = [];
|
||||
let markPipes: ReadonlyArray<PlanPipe> | null = null;
|
||||
/** 알약 · 비정규 측점 = 시설 줄 한 벌(462 · 읽기만). */
|
||||
const lane = shared.lane;
|
||||
/** 마지막으로 세운 알약 — 카드 고름이 측점선을 맞출 때 지킬지 가름(53-1). */
|
||||
let markedId: string | null = null;
|
||||
let types: ReadonlyArray<StructureType> = [];
|
||||
let stations: IrregularStation[] = [];
|
||||
let intervalM = 20;
|
||||
/** 마지막으로 받은 관 목록 지문(자리 · 종류 · 관경) — 바뀔 때만 카드를 다시 얹음. */
|
||||
let pipeSignature: string | null = null;
|
||||
|
||||
@@ -166,7 +128,7 @@ export function createSectionProfile(
|
||||
/** 종단도 측점 id → 카드 id. 관 측점(`irregular:`)은 누가거리로 카드를 찾는다. */
|
||||
const cardOf = (stationId: string | null): string | null => {
|
||||
if (stationId === null) return null;
|
||||
const pipe = stations.find((entry) => irregularStationId(entry.id) === stationId);
|
||||
const pipe = lane.stations().find((entry) => irregularStationId(entry.id) === stationId);
|
||||
if (pipe) return nearestCard(pipe.chainage_m)?.station_id ?? null;
|
||||
return stationId;
|
||||
};
|
||||
@@ -180,22 +142,6 @@ export function createSectionProfile(
|
||||
{ from: "map" },
|
||||
);
|
||||
|
||||
const syncMarks = (): void => {
|
||||
if (!profile) return;
|
||||
profile.setStructureTypes([...types]);
|
||||
profile.setStructures([...marks]);
|
||||
stations = planMarkStations(marks, markPipes, types, intervalM);
|
||||
profile.setIrregularStations(stations);
|
||||
// 판(배수유역도)이 없는 보기 — 관 목록을 직접 넘겨 최소 흙덮기를 판정한다.
|
||||
profile.setMinCoverPipes(
|
||||
marks.filter(isPipeMark).map((mark) => ({
|
||||
chainage_m: structureAnchorM(mark),
|
||||
facility: mark.type_id as never,
|
||||
options: mark.options,
|
||||
})),
|
||||
);
|
||||
};
|
||||
|
||||
/** 한 벌 종단도에 횡단 보기 알림을 건다 — 횡단 보기가 보일 때만 이쪽으로 온다. */
|
||||
const build = (): DesignProfile => {
|
||||
bindDesignView(shared, "cross", {
|
||||
@@ -245,7 +191,7 @@ export function createSectionProfile(
|
||||
selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });
|
||||
return;
|
||||
}
|
||||
const hit = marks.find((entry) => entry.structure_id === structureId);
|
||||
const hit = lane.marks().find((entry) => entry.structure_id === structureId);
|
||||
if (!hit) return;
|
||||
const edit = host.edit();
|
||||
if (!isPipeMark(hit) && hit.structure_id && edit) {
|
||||
@@ -253,7 +199,7 @@ export function createSectionProfile(
|
||||
return;
|
||||
}
|
||||
// 측점 없는 관 = 카드 안 고름 · 스크롤 없음 · 그 관 폼 · 알약만(요구 178 · 307)
|
||||
if (edit?.selectPipe && !plantsStation(hit, types)) {
|
||||
if (edit?.selectPipe && !plantsStation(hit, lane.types())) {
|
||||
edit.selectPipe(structureAnchorM(hit));
|
||||
return;
|
||||
}
|
||||
@@ -266,7 +212,7 @@ export function createSectionProfile(
|
||||
const edit = host.edit();
|
||||
if (!edit) return;
|
||||
moveMarkById(structureId, toChainageM, {
|
||||
structures: marks,
|
||||
structures: lane.marks(),
|
||||
stations: detail?.longitudinal.stations ?? [],
|
||||
edit,
|
||||
});
|
||||
@@ -287,10 +233,9 @@ export function createSectionProfile(
|
||||
const fresh = !shared.profile;
|
||||
profile = build();
|
||||
if (fresh) loadRoute(id, profile);
|
||||
syncMarks();
|
||||
}
|
||||
detail = input.detail;
|
||||
intervalM = input.stationInterval;
|
||||
lane.setIntervalM(input.stationInterval);
|
||||
const routeId = Number(route);
|
||||
profile.render(
|
||||
input.detail,
|
||||
@@ -304,13 +249,7 @@ export function createSectionProfile(
|
||||
naturalSpoilMinSlope: input.naturalSpoilMinSlope,
|
||||
});
|
||||
}
|
||||
syncMarks();
|
||||
},
|
||||
setMarks(nextMarks, nextTypes, nextPipes) {
|
||||
marks = nextMarks;
|
||||
types = nextTypes;
|
||||
markPipes = nextPipes ?? null;
|
||||
syncMarks();
|
||||
lane.paint(); // 다시 그린 종단도에 시설 줄(추가 측점이 늦게 왔으면 다시 셈)
|
||||
},
|
||||
redraw() {
|
||||
profile?.redraw();
|
||||
@@ -325,12 +264,14 @@ export function createSectionProfile(
|
||||
profile.setSelectedStation(null);
|
||||
return;
|
||||
}
|
||||
const pipe = stations.find(
|
||||
(entry) => Math.abs(entry.chainage_m - section.chainage_m) < MATCH_M,
|
||||
);
|
||||
const pipe = lane
|
||||
.stations()
|
||||
.find((entry) => Math.abs(entry.chainage_m - section.chainage_m) < MATCH_M);
|
||||
profile.setSelectedStation(pipe ? irregularStationId(pipe.id) : section.station_id);
|
||||
// 일반 측점은 측점선 짝 알약이 없어 꺼짐 — 고른 구조물이 이 측점을 덮으면 그 알약을 지킴(53-1)
|
||||
const marked = markedId ? marks.find((entry) => entry.structure_id === markedId) : undefined;
|
||||
const marked = markedId
|
||||
? lane.marks().find((entry) => entry.structure_id === markedId)
|
||||
: undefined;
|
||||
if (!pipe && marked && coversChainage(marked, section.chainage_m))
|
||||
profile.setSelectedStructure(markedId, true);
|
||||
},
|
||||
|
||||
@@ -312,7 +312,7 @@ async function build(list) {
|
||||
profile.line = null;
|
||||
panel = panelModule.createB06StructuresPanel({
|
||||
projectId: PID,
|
||||
shared: { view: () => "cross" },
|
||||
shared: { view: () => "cross", lane: { set() {}, setTypes() {}, setStored() {} } }, // 시설 줄(462)
|
||||
detail: () => ({ cross_sections: CARDS }),
|
||||
stationInterval: () => 20,
|
||||
focusChainage: (at, revealOnly) => {
|
||||
|
||||
@@ -309,7 +309,7 @@ async function build(list, keep = false) {
|
||||
profile.pill = null;
|
||||
panel = panelModule.createB06StructuresPanel({
|
||||
projectId: PID,
|
||||
shared: { view: () => "cross" },
|
||||
shared: { view: () => "cross", lane: { set() {}, setTypes() {}, setStored() {} } }, // 시설 줄(462)
|
||||
detail: () => ({ cross_sections: CARDS }),
|
||||
stationInterval: () => 20,
|
||||
focusChainage: (at, revealOnly) => focusStationAt(at, revealOnly),
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
/* 462 B05 · B06 한 페이지 시설 줄 한 벌(요구 271-5) — 진짜 `design_facility_lane` · 관 id 장부 · 측점 계획 ·
|
||||
* 관 따라감 · 가게를 Node 로 돌려 JSON 한 줄(`test_271_facility_lane.py` 가 판정). 종단도는 가짜(받은 것 기록).
|
||||
* node helper_271_facility_lane.cjs
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
const CATALOG = require(path.join(__dirname, "helper_322_catalog_module.cjs"));
|
||||
|
||||
function memoryStorage() {
|
||||
const map = new Map();
|
||||
return {
|
||||
getItem: (k) => (map.has(k) ? map.get(k) : null),
|
||||
setItem: (k, v) => map.set(k, String(v)),
|
||||
removeItem: (k) => map.delete(k),
|
||||
key: (i) => [...map.keys()][i] ?? null,
|
||||
get length() {
|
||||
return map.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
|
||||
globalThis.sessionStorage = globalThis.window.sessionStorage;
|
||||
globalThis.localStorage = globalThis.window.localStorage;
|
||||
|
||||
const anyModule = (extra = {}) =>
|
||||
new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) });
|
||||
const STUBS = {
|
||||
"config_frontend.ts": anyModule({
|
||||
CURRENT_PROJECT_ID_KEY: "frd_current_project_id",
|
||||
API_BASE_URL: "",
|
||||
API_TIMEOUT_MS: 1000,
|
||||
PIPE_TYPES: [],
|
||||
}),
|
||||
"ui_template_elements.ts": anyModule(),
|
||||
"B04_PreProcess_Api_Fetch.ts": anyModule(),
|
||||
};
|
||||
|
||||
const cache = new Map();
|
||||
const resolve = (base) =>
|
||||
[base, `${base}.ts`, path.join(base, "index.ts")].find(
|
||||
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
|
||||
) ?? `${base}.ts`;
|
||||
function loadTs(full) {
|
||||
const stub = STUBS[path.basename(full)];
|
||||
if (stub) return stub;
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2020,
|
||||
esModuleInterop: true,
|
||||
},
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const near = (request) => {
|
||||
if (CATALOG.isCatalogRequest(request)) return CATALOG;
|
||||
if (request.endsWith(".json"))
|
||||
return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8"));
|
||||
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
|
||||
for (const [prefix, dir] of Object.entries(alias))
|
||||
if (request.startsWith(prefix))
|
||||
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
|
||||
return loadTs(resolve(path.join(path.dirname(full), request)));
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const design = path.join(ROOT, "A00_Common", "design");
|
||||
const { createFacilityLane } = loadTs(path.join(design, "design_facility_lane.ts"));
|
||||
const ids = loadTs(path.join(design, "design_pipe_ids.ts"));
|
||||
const { useDesignStore } = loadTs(path.join(design, "design_store.ts"));
|
||||
const { readState } = loadTs(path.join(ROOT, "A00_Common", "b_page_state.ts"));
|
||||
const TYPES = JSON.parse(
|
||||
fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"),
|
||||
).types;
|
||||
|
||||
/** 가짜 종단도 — 받은 것 · 편집 옮김 기록. */
|
||||
function fakeProfile() {
|
||||
const got = {
|
||||
structures: null,
|
||||
stations: null,
|
||||
minCover: null,
|
||||
types: 0,
|
||||
resets: [],
|
||||
stationWrites: 0,
|
||||
};
|
||||
return {
|
||||
got,
|
||||
setStructureTypes: (types) => (got.types = types.length),
|
||||
setStructures: (next) => (got.structures = next),
|
||||
setIrregularStations: (stations) => ((got.stations = stations), (got.stationWrites += 1)),
|
||||
setMinCoverPipes: (pipes) => (got.minCover = pipes.map((p) => p.chainage_m)),
|
||||
resetStationEdit: (from, to) => got.resets.push([from, to ?? null]),
|
||||
};
|
||||
}
|
||||
const book = (projectId) => readState("pipe-ids", projectId) ?? { next: 1, ids: [] };
|
||||
const wall = (id, at) => ({
|
||||
structure_id: id,
|
||||
type_id: "retaining_wall",
|
||||
placement: "interval",
|
||||
chainage_m: at,
|
||||
start_m: at - 5,
|
||||
end_m: at + 5,
|
||||
options: {},
|
||||
});
|
||||
const markView = (lane) => lane.marks().map((m) => [m.structure_id, m.type_id, m.chainage_m]);
|
||||
const stationView = (lane) => lane.stations().map((s) => [s.chainage_m, s.id]);
|
||||
|
||||
const out = {};
|
||||
|
||||
// ① R3 뒤집기 — B06 만 연 채 351 관 → 360 옮김 → 버림 → 카드가 351 관 id 를 물음 → 다시 실음
|
||||
{
|
||||
const P = "P271-r3";
|
||||
ids.usePipeIds(P);
|
||||
const profile = fakeProfile();
|
||||
const lane = createFacilityLane(P, () => profile);
|
||||
lane.setTypes(TYPES);
|
||||
lane.set({ structures: [], pipes: [{ id: "X", chainage_m: 351, facility: "pipe" }] }, false);
|
||||
const first = ids.pipeIdAt(351, P);
|
||||
ids.followMovedPipe(P, 351, 360); // B06 movePipeTo
|
||||
lane.set({ pipes: [{ id: "X", chainage_m: 360, facility: "pipe" }] });
|
||||
const moved = ids.pipeIdAt(360, P);
|
||||
const ghost = ids.pipeIdAt(351, P); // 버린 뒤 다시 받기 전 카드가 물음(헛 id)
|
||||
lane.set({ pipes: [{ id: "X", chainage_m: 351, facility: "pipe" }] }, false); // 목록 다시 실음
|
||||
out.r3 = {
|
||||
first,
|
||||
moved,
|
||||
ghostBeforeReload: ghost !== first,
|
||||
after: book(P).ids,
|
||||
afterId: ids.pipeIdAt(351, P),
|
||||
marks: markView(lane),
|
||||
};
|
||||
}
|
||||
|
||||
// ② 같은 조작 — B05 먼저 듣는 중(listener 처음부터) 대 B06 먼저(끝에 B05 가 읽음)
|
||||
function scenario(P, listenFromStart) {
|
||||
ids.usePipeIds(P);
|
||||
const profile = fakeProfile();
|
||||
const lane = createFacilityLane(P, () => profile);
|
||||
const heard = [];
|
||||
const listen = () => lane.onChange(() => heard.push(stationView(lane).length));
|
||||
if (listenFromStart) listen();
|
||||
lane.setTypes(TYPES);
|
||||
lane.setStored([wall("w1", 100)]);
|
||||
lane.set(
|
||||
{
|
||||
structures: [wall("w1", 100)],
|
||||
pipes: [
|
||||
{ id: "A", chainage_m: 60, facility: "pipe" },
|
||||
{ id: "B", chainage_m: 200, facility: "pipe" },
|
||||
],
|
||||
},
|
||||
false,
|
||||
);
|
||||
// B06 폼 — 구조물 옮김 · 관 하나 넣음(새 관 id 없음) · 관 옮김
|
||||
lane.set({ structures: [wall("w1", 140)] });
|
||||
lane.set({
|
||||
pipes: [
|
||||
{ id: "A", chainage_m: 60, facility: "pipe" },
|
||||
{ chainage_m: 120, facility: "pipe" },
|
||||
{ id: "B", chainage_m: 200, facility: "pipe" },
|
||||
],
|
||||
});
|
||||
ids.followMovedPipe(P, 200, 230);
|
||||
lane.set({
|
||||
pipes: [
|
||||
{ id: "A", chainage_m: 60, facility: "pipe" },
|
||||
{ chainage_m: 120, facility: "pipe" },
|
||||
{ id: "B", chainage_m: 230, facility: "pipe" },
|
||||
],
|
||||
});
|
||||
// 배수유역도가 뒤늦게 같은 목록을 알림(B05 판)
|
||||
lane.set({
|
||||
pipes: [
|
||||
{ id: "A", chainage_m: 60, facility: "pipe" },
|
||||
{ chainage_m: 120, facility: "pipe" },
|
||||
{ id: "B", chainage_m: 230, facility: "pipe" },
|
||||
],
|
||||
});
|
||||
if (!listenFromStart) listen();
|
||||
const ledger = book(P).ids;
|
||||
const markIds = lane.marks().map((m) => m.structure_id);
|
||||
return {
|
||||
marks: markView(lane).map(([id, type, at]) => [id.startsWith("pipe-") ? "pipe" : id, type, at]),
|
||||
stations: stationView(lane).map(([at]) => at),
|
||||
minCover: profile.got.minCover,
|
||||
profileMarks: profile.got.structures.length === lane.marks().length,
|
||||
profileStations: JSON.stringify(profile.got.stations) === JSON.stringify(lane.stations()),
|
||||
ghostIds: ledger.filter((entry) => !markIds.includes(entry.id)).length,
|
||||
resets: profile.got.resets,
|
||||
heardB05: listenFromStart ? heard.length : null,
|
||||
};
|
||||
}
|
||||
out.b05First = scenario("P271-b05", true);
|
||||
out.b06First = scenario("P271-b06", false);
|
||||
|
||||
// ③ 노선 따라감(가게 씀 + follow) = 편집 안 옮김 · 다시 실음(같은 목록) = 안 옮김 · 측점 다시 안 씀
|
||||
{
|
||||
const P = "P271-follow";
|
||||
ids.usePipeIds(P);
|
||||
const profile = fakeProfile();
|
||||
const lane = createFacilityLane(P, () => profile);
|
||||
const store = useDesignStore(P, null);
|
||||
lane.setTypes(TYPES);
|
||||
lane.set({ structures: [], pipes: [{ id: "A", chainage_m: 80, facility: "pipe" }] }, false);
|
||||
store.set("pipes", [{ id: "A", chainage_m: 95, source: "user", facility: "pipe" }]);
|
||||
store.set("structures", [wall("w9", 150)]);
|
||||
lane.follow();
|
||||
const followed = {
|
||||
stations: stationView(lane).map(([at]) => at),
|
||||
resets: [...profile.got.resets],
|
||||
};
|
||||
const writes = profile.got.stationWrites;
|
||||
lane.set({ structures: [wall("w9", 150)] }, false); // 숨었던 B05 가 다시 보이며 같은 목록을 실음
|
||||
const reshown = { resets: profile.got.resets.length, writes: profile.got.stationWrites - writes };
|
||||
lane.set({ structures: [wall("w9", 170)] }); // 사용자 옮김 = 옮김
|
||||
out.follow = { followed, reshown, userMove: profile.got.resets };
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(out));
|
||||
@@ -57,8 +57,7 @@ async function run() {
|
||||
const bridge = bridgeMod.createStructuresBridge({
|
||||
projectId: "p1",
|
||||
panel: () => ({ structures: quiet, values: () => ({}) }),
|
||||
profilePanel: () => quiet,
|
||||
renderStationLines: () => undefined,
|
||||
lane: quiet, // 시설 줄 한 벌(462)
|
||||
isRestoring: () => false,
|
||||
});
|
||||
out.conflict = await settle(bridge.saveStructuresIfDirty());
|
||||
|
||||
@@ -180,8 +180,7 @@ def test_move_sites_follow_edits():
|
||||
b06 = (PROJECT_ROOT / "B06_Section/B06_Section_UI_Page_Structures_Panel.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
b05 = (PROJECT_ROOT / "B05_Profile/B05_Profile_UI_Page_Structures.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
# B05 측점 갱신 = 페이지 시설 줄 한 벌(462 · 어느 보기 고침이든)
|
||||
lane = (PROJECT_ROOT / "A00_Common/design/design_facility_lane.ts").read_text(encoding="utf-8")
|
||||
assert "deps.shared.profile?.resetStationEdit(hit.chainage_m, to)" in b06
|
||||
assert "followedStation(station, gone, born)" in b05
|
||||
assert "followedStation(station, gone, born)" in lane
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
"""462 B05 · B06 한 페이지 — 시설 줄 페이지 한 벌(요구 271-5 · 분석 4장 (A) · (E)).
|
||||
|
||||
① R3 뒤집기 — B06 만 연 채 351 관 → 360 → 버림 → 카드가 351 관 id 를 물어 헛 id → 목록 다시 실음 =
|
||||
장부 한 줄(처음 id · 351) · 옛 코드 = {s1@360 헛 · s2@351}(장부 맞춤이 B05 보기에만)
|
||||
② 같은 조작 — B05 먼저 듣는 중 = B06 먼저(끝에 B05 가 읽음) · 알약 · 측점 · 흙덮기 관 · 종단도 받은 것 같음 · 헛 id 0
|
||||
③ 노선 따라감 = 편집 안 옮김 · 숨었던 보기가 같은 목록을 다시 실음 = 안 옮김 · 측점 다시 안 씀 · 사용자 옮김 = 옮김
|
||||
④ 이음 — 관 알림 · 노선 따라감 · B06 목록이 페이지 한 벌로 · 보기는 읽기만 · B05 → B06 관 옵션 = 세션 칸
|
||||
TS 셈은 `helper_271_facility_lane.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_271_facility_lane.py -q
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_271_facility_lane.cjs")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def src(*parts: str) -> str:
|
||||
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def got() -> dict:
|
||||
run = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HELPER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
)
|
||||
assert run.returncode == 0, run.stderr
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_B06_만_열어도_장부_헛_id_없음(got) -> None:
|
||||
r3 = got["r3"]
|
||||
assert r3["first"] == r3["moved"] == "pipe-s1" # 옮겨도 그 id
|
||||
assert r3["ghostBeforeReload"] is True # 버린 사이 카드가 지은 헛 id(재현)
|
||||
assert r3["after"] == [{"id": "pipe-s1", "at": 351}] # 다시 실으면 처음 id 한 줄
|
||||
assert r3["afterId"] == "pipe-s1"
|
||||
assert r3["marks"] == [["pipe-s1", "pipe", 351]]
|
||||
|
||||
|
||||
def test_B06_먼저_B05_먼저_같음(got) -> None:
|
||||
first, late = got["b05First"], got["b06First"]
|
||||
for key in ("marks", "stations", "minCover", "resets"):
|
||||
assert first[key] == late[key], key
|
||||
assert first["stations"] == [60, 120, 135, 140, 145, 230]
|
||||
assert first["minCover"] == [60, 120, 230]
|
||||
for one in (first, late):
|
||||
assert one["profileMarks"] is True and one["profileStations"] is True
|
||||
assert one["ghostIds"] == 0
|
||||
assert first["heardB05"] == 6 # 숨은 보기도 바뀔 때마다 들음
|
||||
|
||||
|
||||
def test_노선_따라감은_편집_안_옮김(got) -> None:
|
||||
follow = got["follow"]
|
||||
assert follow["followed"] == {"stations": [95, 145, 150, 155], "resets": []}
|
||||
assert follow["reshown"] == {"resets": 0, "writes": 0}
|
||||
assert follow["userMove"] == [[145, 165], [150, 170], [155, 175]]
|
||||
|
||||
|
||||
def test_이음_페이지_한_벌() -> None:
|
||||
shared = src("A00_Common", "design", "design_shared.ts")
|
||||
changed = shared[shared.index("onPipesChanged: (pipes) => {") :]
|
||||
changed = changed[: changed.index("},")]
|
||||
assert changed.index("shared.lane.set({ pipes });") < changed.index("toAll(")
|
||||
assert "shared.lane.follow();" in shared[shared.index("onFacilitiesFollowed: (far)") :][:200]
|
||||
assert "shared.lane.paint();" in shared # 종단도가 막 섰을 때
|
||||
page = src("A00_Common", "design", "design_page.ts")
|
||||
assert "lane: createFacilityLane(projectId, () => shared.profile)," in page
|
||||
assert "shared.lane.dispose();" in page
|
||||
# 종단도에 알약 · 측점 · 흙덮기 관을 쓰는 자리 = 시설 줄 하나
|
||||
for parts in (
|
||||
("A00_Common", "design", "design_view_profile.ts"),
|
||||
("B05_Profile", "B05_Profile_UI_Page_Structures.ts"),
|
||||
("B06_Section", "B06_Section_UI_Section_View_Profile.ts"),
|
||||
):
|
||||
text = src(*parts)
|
||||
for call in (
|
||||
"setIrregularStations(",
|
||||
"setMinCoverPipes(",
|
||||
"placePipeIds(",
|
||||
"prunePipeIds(",
|
||||
):
|
||||
assert call not in text, (parts[-1], call)
|
||||
assert "planStations(" not in text, parts[-1]
|
||||
panel = src("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
push = panel[panel.index("const pushMarks") :]
|
||||
assert "deps.shared.lane.set({ structures, pipes: pipeFacilities }, carryEdits);" in push[:600]
|
||||
view = src("A00_Common", "design", "design_view_profile.ts")
|
||||
assert "marks: shared.lane.marks," in view and "types: shared.lane.types," in view
|
||||
|
||||
|
||||
def test_관_옵션_넘김은_세션_칸() -> None:
|
||||
shared = src("A00_Common", "design", "design_shared.ts")
|
||||
assert "pipeOptionQueue" not in shared
|
||||
assert 'writeState("pipe-option-queue"' in shared
|
||||
assert '"pipe-option-queue": { bucket: "draft", scope: "project" },' in src(
|
||||
"A00_Common", "b_page_state.ts"
|
||||
)
|
||||
@@ -80,7 +80,7 @@ def test_B06_알약_풂과_측점_없는_관():
|
||||
view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
|
||||
assert "if (structureId === null) {" in view
|
||||
assert "selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });" in view
|
||||
assert "if (edit?.selectPipe && !plantsStation(hit, types)) {" in view
|
||||
assert "if (edit?.selectPipe && !plantsStation(hit, lane.types())) {" in view
|
||||
cross = _read("A00_Common", "design", "design_view_cross.ts")
|
||||
assert "selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM)," in cross
|
||||
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
|
||||
|
||||
@@ -44,10 +44,12 @@ def test_마커_목록은_알약_목록(got) -> None:
|
||||
assert "const structures = laneStructures(options.structures);" in lane
|
||||
marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts")
|
||||
assert "laneStructures(source.marks())" in marks
|
||||
bridge = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts")
|
||||
assert "setStructures([...ownStructures, ...pipeMarks]);" in bridge
|
||||
assert "marks: (): StructureInstance[] => [...ownStructures, ...pipeMarks]," in bridge
|
||||
assert "planMarksChanged(deps.projectId);" in bridge
|
||||
# 알약 · 평면 마커 = 시설 줄 한 벌(462) 같은 목록 · 바뀌면 평면 다시 그림
|
||||
lane = _read("A00_Common", "design", "design_facility_lane.ts")
|
||||
assert "to.setStructures(marks());" in lane
|
||||
view = _read("A00_Common", "design", "design_view_profile.ts")
|
||||
assert "marks: shared.lane.marks," in view
|
||||
assert "planMarksChanged(activeProjectId);" in view
|
||||
|
||||
|
||||
def test_고름_정보_창_법선(got) -> None:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
이 탭에서 고친 사용자 칸만 · [저장] patch 바탕도 서버 값 · 세션 = 서버 정본 그대로
|
||||
② 같은 판 · 서버 실패 = 세션 그대로(갈아끼움 · 알림 없음)
|
||||
③ 노선 초안이 옛 상세를 쥔 채 서버 판이 바뀜 — 초안을 버리면 새 판
|
||||
④ 숨은 종단 보기도 관 세션 id 장부를 돌림(평면 마커 누름 = 초안 자리) · 그리기는 보일 때
|
||||
④ 숨은 종단 보기도 관 세션 id 장부를 돌림(평면 마커 누름 = 초안 자리 · 장부 = 시설 줄 한 벌 462) · 그리기는 보일 때
|
||||
TS 셈은 `helper_451_session_canon.cjs`.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_451_session_canon.py -q
|
||||
@@ -63,9 +63,10 @@ def test_초안_hold_는_버리면_새_판(got) -> None:
|
||||
|
||||
|
||||
def test_숨은_종단_보기도_관_id_장부() -> None:
|
||||
view = (ROOT / "A00_Common/design/design_view_profile.ts").read_text(encoding="utf-8")
|
||||
follow = view[view.index("const followFacilities") : view.index("const selectGraphAt")]
|
||||
assert "if (!host.visible()) return void (lastPipes && shownPipes(lastPipes));" in follow
|
||||
changed = view[view.index("onPipesChanged: (pipes) => {") :]
|
||||
changed = changed[: changed.index("},")]
|
||||
assert "else shownPipes(pipes);" in changed
|
||||
# 장부 맞춤 = 페이지 시설 줄 한 벌(462) — 관 알림 · 노선 따라감 모두 보기 숨김과 무관
|
||||
shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8")
|
||||
changed = shared[shared.index("onPipesChanged: (pipes) => {") :]
|
||||
assert "shared.lane.set({ pipes });" in changed[: changed.index("},")]
|
||||
assert "shared.lane.follow();" in shared
|
||||
lane = (ROOT / "A00_Common/design/design_facility_lane.ts").read_text(encoding="utf-8")
|
||||
assert "placePipeIds(" in lane and "prunePipeIds(" in lane
|
||||
|
||||
@@ -46,6 +46,6 @@ def test_draft_pipe_options_are_replayed_onto_the_cache():
|
||||
shared = _read("A00_Common", "design", "design_shared.ts")
|
||||
assert "shared.lastPipes = pipes" in shared
|
||||
profile = _read("A00_Common", "design", "design_view_profile.ts")
|
||||
assert profile.index("if (lastPipes) applyPipes(lastPipes)") < profile.index(
|
||||
assert profile.index("if (lastPipes) applyPipes();") < profile.index(
|
||||
"await bridge.load();\n advanceLoading"
|
||||
)
|
||||
|
||||
@@ -62,7 +62,8 @@ def test_막기_규칙은_종단도_한_곳():
|
||||
assert profile.count("blocksMinCover(") == 1
|
||||
# 판이 없는 보기도 관 목록을 넘겨 최소 흙덮기를 판정한다.
|
||||
assert "setMinCoverPipes(" in profile
|
||||
assert "setMinCoverPipes(" in _read("B06_Section/B06_Section_UI_Section_View_Profile.ts")
|
||||
# 두 보기 = 시설 줄 한 벌이 넘김(462)
|
||||
assert "setMinCoverPipes(" in _read("A00_Common/design/design_facility_lane.ts")
|
||||
|
||||
|
||||
def test_크기_접기_키는_하나():
|
||||
|
||||
Reference in New Issue
Block a user