Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_4

This commit is contained in:
2026-10-18 23:18:04 +09:00
20 changed files with 685 additions and 303 deletions
+2
View File
@@ -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). [저장]·[확정] 뒤 비움. */
+219
View File
@@ -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();
},
};
}
+4 -1
View File
@@ -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,
};
@@ -281,6 +283,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;
+21 -7
View File
@@ -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) => {
+27 -51
View File
@@ -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("");
+25 -139
View File
@@ -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 좌측 목록만 하이라이트가 안 붙음). 폼에 아직 아무것도
// 없으면 세션에 남은 선택(카드형 — 부재키 없는 측점)도 같은 규칙으로 세운다.
+2 -3
View File
@@ -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));
+1 -2
View File
@@ -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
+111
View File
@@ -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"
)
+1 -1
View File
@@ -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:
+8 -7
View File
@@ -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_크기_접기_키는_하나():