fix(472-5 5-A 값 주인 하나): 관 · 구조물 주인 = 설계 가게 칸 — 화면 사본은 가게 알림으로 맞춤 · 되돌리기도 알림

- 되돌리기 · 다시하기(세션 직접 되씀) 뒤 announceDesignStore — 관 · 구조물 · 추가 측점 듣는 쪽을 되돌린 값으로
  (옛: 종단 목록 · 횡단 판 · 배수 편집기가 옛 값을 쥐고 다음 조작이 되돌림을 덮음)
- 종단 보기 — 가게 관 → 배수 편집기 adoptPipes(숨어도 · 제 쓰기 메아리 빼고 · 옵션만 바뀐 것도) · 가게 구조물 → takeStructures(보일 때)
- 횡단 보기 — 판이 마지막으로 읽거나 쓴 값과 다르면(옵션만도) 다시 실음(seen · foreign) · 제 쓰기는 쓰기 전에 적음
- 종단 고름 · 알약 관 = 시설 줄(가게 초안 자리 · 노선 따라감 뒤도) — 옛 알림 자리 안 봄
- 시험 test_472_5_shop_owner(옛 코드 실패 확인) · test_188_1 글 한 줄(까닭: 고름 관 원천 바뀜) · 위키 values · rules

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 09:53:26 +09:00
co-authored by Claude Opus 5.5
parent b43c4f1060
commit 086ead9d6a
15 changed files with 316 additions and 22 deletions
+11
View File
@@ -241,6 +241,17 @@ const listeners = new Map<string, Set<(value: unknown) => void>>();
* 노선을 모르면 `routeId = null` — 노선 칸(`alignmentDraft` · `sectionDetail`)은 비어 보이고
* 쓰기는 버려진다. 프로젝트를 모르면 프로젝트 칸이 그렇다.
*/
/** 가게를 안 거친 세션 되씀(되돌리기 · 다시하기) 뒤 — 프로젝트 목록 칸을 듣는 쪽을 지금 값으로 부름
* (472-5 · 관 · 구조물 사본이 옛 값으로 남아 다음 조작이 되돌림을 덮던 흠). */
export function announceDesignStore(projectId: string): void {
const store = useDesignStore(projectId, null);
(["structures", "pipes", "plusStations"] as const).forEach((key) => {
const id = slotOf(key, projectId, null).id;
const value = store.get(key);
if (id) listeners.get(`${key}|${id}`)?.forEach((fn) => fn(value));
});
}
export function useDesignStore(projectId: string | null, routeId: number | null): DesignStore {
return {
get(key) {
+6
View File
@@ -18,6 +18,7 @@ import { stateKey, writeByKey } from "../b_page_state";
import { showToast } from "@ui/ui_template_elements";
import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations";
import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
import { useDesignStore } from "./design_store";
import { pipeIdAt } from "./design_pipe_ids";
import { pageRouteId, pageSectionContext } from "./design_page_data";
import {
@@ -575,6 +576,11 @@ export async function mountCrossView(
void refreshDetailForStructures();
}),
);
// 가게 관 · 구조물 초안을 남이 고침(되돌리기 · 종단 보기 · 저장 뒤 · 옵션만도) — 보일 때 목록 · 폼 다시(472-5)
const shop = useDesignStore(projectId, null);
const onShop = (): void => void (host.visible() && structuresPanel.foreign() && reloadPanel());
host.onDispose(shop.subscribe("structures", onShop));
host.onDispose(shop.subscribe("pipes", onShop));
host.onDispose(followSwaps(projectId, () => currentRouteId, host, refreshDetailForStructures));
host.onDispose(() => {
window.clearTimeout(pipeReloadTimer);
+12 -2
View File
@@ -116,6 +116,8 @@ export async function mountProfileView(
/** 배수유역도가 마지막으로 알린 관 목록 — 숨어 있다 다시 보일 때 폼 · 3D 에 다시 싣는다. */
let lastPipes: PipeReport | null = shared.lastPipes ?? null;
const designStore = useDesignStore(activeProjectId, null);
/** 화면 관 = 시설 줄(알림을 가게 초안 자리로 · 노선 따라감 뒤도) — 고름 · 알약이 옛 알림 자리를 안 봄(472-5). */
const shownPipes = (): PipeReport => (lastPipes ? (shared.lane.pipes() as PipeReport) : []);
/** 화면 관 목록 = 시설 줄 한 벌(462 · 가게 관 초안 측점 · 관 세션 id 장부 · 알약 · 측점선 · 흙덮기는 그쪽이
* 숨어도 셈) — 이 보기는 보일 때 좌측 목록만 맞춤. */
const applyPipes = (): void => {
@@ -144,7 +146,7 @@ export async function mountProfileView(
const selectGraphAt = (id: string | null, chainageM: number | null): void => {
const structurePick = !!readSelection(activeProjectId)?.structureId;
const sections = structurePick ? [] : (state.detail?.cross_sections ?? []);
const target = graphTargetAt(id, chainageM, lastPipes ?? [], sections);
const target = graphTargetAt(id, chainageM, shownPipes(), sections);
if ("pipeChainageM" in target)
return profilePanel.setSelectedStructure(pipeIdAt(target.pipeChainageM, activeProjectId));
profilePanel.setSelectedStation(target.stationId);
@@ -396,7 +398,7 @@ export async function mountProfileView(
host,
{
irregular: () => bridge.irregularStations(),
pipes: () => lastPipes ?? [],
pipes: shownPipes,
sections: () => state.detail?.cross_sections ?? [],
},
);
@@ -436,6 +438,14 @@ export async function mountProfileView(
// 추가 측점(368-2) — 초안 · 저장분이 바뀌면 시설 줄이 측점 계획을 다시 맞춤(종단 측점선 · 3D)
setPlusStations(() => plusStationsOf(activeProjectId)); // 저장분이 늦게 와도 한 번 다시 맞춤
void loadSavedPlusStations(activeProjectId).then(() => bridge.resyncStations());
// 가게 관 · 구조물 초안이 다른 길(횡단 보기 · 되돌리기 · 저장 뒤)로 바뀜 — 배수 편집기는 숨어도 받음(다음
// 요청이 옛 값을 되쓰지 않게) · 목록은 보일 때(숨으면 다시 보일 때 load) · 제 쓰기는 같은 값이라 그냥 지남(472-5).
host.onDispose(
designStore.subscribe("pipes", (next) => next && profilePanel.drainage.adoptPipes(next)),
);
host.onDispose(
designStore.subscribe("structures", (next) => host.visible() && bridge.takeStructures(next)),
);
// 한 페이지 두 보기 — 숨김 · 보임 · 떠남(계획서 24-3a · 24-3b).
bindProfileViewLife(host, {
projectId: activeProjectId,
@@ -58,6 +58,7 @@ import {
basinIndexOfPipe,
pipeMarkerColor,
adoptDraftPipes,
adoptEditorPipes,
followEditorPipes,
reconcilePipes,
WIDTH_KEY,
@@ -537,6 +538,12 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
},
pipeChainages: () => pipeEditor.chainages(),
followPipes: (pipes) => followEditorPipes(pipeEditor, facilityStore, pipes),
adoptPipes(pipes) {
if (!adoptEditorPipes(pipeEditor, facilityStore, pipeRequest(), pipes)) return;
callbacks.onPipesChanged?.(buildPipeReports(pipeEditor.pipes(), basins, facilityStore));
syncPipeSelection(false);
scheduleDraw();
},
movePipe(fromChainage, toChainage) {
const index = pipeEditor
.pipes()
@@ -10,7 +10,11 @@
import type { GroupItems, SummaryItem } from "../common_util/common_util_summary_item";
import type { RoutePoint } from "./B05_Profile_Api_Fetch";
import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
import type { PipeFacility, PipeSource } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type {
DetailPipeInput,
PipeFacility,
PipeSource,
} from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans";
import type { StageFn } from "../A00_Common/design/logic/design_pipeline_types";
@@ -28,6 +32,8 @@ export interface DrainagePanel {
pipeChainages: () => number[];
/** 노선 초안 follow 가 다시 매긴 관 자리를 받음(짝 = 관 id · 재계산 없음 · 410). */
followPipes: (pipes: ReadonlyArray<{ id?: string | null; chainage_m: number }>) => void;
/** 가게 관 초안을 그대로 받음(다른 보기 · 되돌리기가 고침 · 472-5) — 제 쓰기와 같으면 아무것도 안 함 · 재계산 없음. */
adoptPipes: (pipes: ReadonlyArray<DetailPipeInput>) => void;
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
movePipe: (fromChainage: number, toChainage: number) => void;
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
+31 -1
View File
@@ -20,7 +20,7 @@ import {
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import { normalizeStrength, rampColor } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
import type { MapContextMenu, MapContextMenuItem } from "@ui/ui_template_context_menu";
import type { DetailBasin } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { DetailBasin, DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { PipeEditor, PipePoint } from "./B05_Profile_UI_Drainage_Pipes";
import type { FacilityStore } from "./B05_Profile_UI_Drainage_Facility";
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
@@ -333,6 +333,36 @@ export function mountDrainageToggles(
/** 노선 초안 follow 가 다시 매긴 관 자리(가게 관 초안)를 편집기에 얹음(410) — 짝 = 관 id(막 놓아 초안에 id
* 없는 관끼리는 누가거리 차례 · 수가 같을 때만) · 시설 속성 · 고름도 따라감 · 재계산 · 초안 쓰기 없음(가게가
* 이미 그 값). 편집기가 옛 노선 누가거리로 초안을 덮지 않게. */
/** 가게 관 초안을 편집기 · 시설 보관소에 그대로 싣는다 — 다른 길(횡단 보기 · 되돌리기 · 저장 뒤 rebase)이
* 초안을 고친 때(472-5 · 안 실으면 다음 요청이 옛 값을 초안에 되씀). `current` = 지금 요청 몸통 —
* 같으면 제 쓰기 메아리라 아무것도 안 함(false) · 고른 관은 같은 id(없으면 같은 자리)로 따라감. */
export function adoptEditorPipes(
editor: PipeEditor,
facilities: FacilityStore,
current: ReadonlyArray<DetailPipeInput>,
next: ReadonlyArray<DetailPipeInput>,
): boolean {
if (JSON.stringify(current) === JSON.stringify(next)) return false;
const picked = editor.pipes()[editor.selected() ?? -1];
facilities.replaceFromResponse(next);
editor.setPipes(
next.map((pipe) => ({
chainage_m: pipe.chainage_m,
reason: pipe.source,
...(pipe.id ? { id: pipe.id } : {}),
})),
);
if (picked) {
const at = editor
.pipes()
.findIndex((pipe) =>
picked.id ? pipe.id === picked.id : Math.abs(pipe.chainage_m - picked.chainage_m) < 0.005,
);
if (at >= 0) editor.select(at);
}
return true;
}
export function followEditorPipes(
editor: PipeEditor,
facilities: FacilityStore,
@@ -28,7 +28,7 @@
* ========================================================================== */
import { projectStateKeys, readByKey, writeByKey } from "../A00_Common/b_page_state";
import { draftProjectId } from "../A00_Common/design/design_store";
import { announceDesignStore, draftProjectId } from "../A00_Common/design/design_store";
/** 초안 통이지만 조작이 아닌 칸 · 다른 페이지 칸 — 사진 밖. */
const NOT_EDIT =
@@ -213,6 +213,7 @@ export function createProfileHistory(
try {
onRestore();
hooks.forEach((entry) => entry.restore?.(kind));
announceDesignStore(projectId); // 가게 듣는 쪽(관 · 구조물 사본)도 되돌린 값으로(472-5)
// 되살리며 다시 쓴 같은 값(정리된 꼴)을 이 걸음으로 — 다음 찍기가 헛걸음을 안 만듦.
stack[cursor] = takeSnapshot(projectId, stack[cursor]);
} finally {
@@ -130,6 +130,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
};
// 폼 한 벌(계획서 24-3b) — 횡단 보기가 보일 때 폼 알림을 받는 동작만 건다. 상세 옵션 칸은
// 한 벌이 보기에 따라 그린다(횡단 보기에서만).
/** 가게 관 · 구조물 초안 중 이 판이 마지막으로 읽거나 쓴 값(JSON) — 다르면 남(되돌리기 · 종단 보기 ·
* 저장 뒤)이 고친 것이라 다시 실음(472-5 · 옵션만 바뀌어도). */
let seen = { structures: "", pipes: "" };
const section = sharedStructures(deps.shared);
bindDesignView(deps.shared, "cross", {
structures: {
@@ -137,6 +140,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
structures = withLocalIds(next);
section.setStructures(structures);
pushMarks();
seen.structures = JSON.stringify(structures); // 제 쓰기 — 듣는 쪽이 남 고침과 가름(472-5)
if (deps.projectId) writePendingStructures(deps.projectId, structures);
// 고른 구조물을 다른 측점으로 옮기면 횡단 카드도 따라감(53-1)
const picked = readSelection(deps.projectId);
@@ -317,6 +321,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
deps.shared.lane.setStored(storedStructures);
storedPipes = pipeResponse.pipe_points;
structures = readPendingStructures(projectId) ?? stored.structures;
seen = {
structures: JSON.stringify(readPendingStructures(projectId)),
pipes: JSON.stringify(readPendingPipes(projectId)),
};
const note = staleDefinitionNotice(projectId, stored.definition_version); // 322-2
if (note) showToast(note);
section.setStructures(structures);
@@ -411,19 +419,18 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
*/
function persistPipes(): void {
if (!deps.projectId) return;
writePendingPipes(
deps.projectId,
pipeFacilities.map((pipe) => ({
...(pipe.id ? { id: pipe.id } : {}), // B05 초안과 같은 꼴(445 H2)
chainage_m: pipe.chainage_m,
source: pipe.source ?? "user",
facility: pipe.facility,
...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}),
...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}),
...(pipe.options ? { options: pipe.options } : {}),
...linkFields(pipe),
})),
);
const next = pipeFacilities.map((pipe) => ({
...(pipe.id ? { id: pipe.id } : {}), // B05 초안과 같은 꼴(445 H2)
chainage_m: pipe.chainage_m,
source: pipe.source ?? "user",
facility: pipe.facility,
...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}),
...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}),
...(pipe.options ? { options: pipe.options } : {}),
...linkFields(pipe),
}));
seen.pipes = JSON.stringify(next); // 제 쓰기(472-5)
writePendingPipes(deps.projectId, next);
deps.onPipesWritten?.();
}
@@ -493,6 +500,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
root: section.root,
listRoot: section.listRoot,
load,
foreign: () =>
!!deps.projectId &&
(JSON.stringify(readPendingStructures(deps.projectId)) !== seen.structures ||
JSON.stringify(readPendingPipes(deps.projectId)) !== seen.pipes),
listStale: () => {
if (!deps.projectId) return false;
const shopStructures = readPendingStructures(deps.projectId) ?? storedStructures;
@@ -55,6 +55,8 @@ export interface B06StructuresPanel {
load: () => Promise<void>;
/** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */
listStale: () => boolean;
/** 가게 초안이 이 판이 마지막으로 읽거나 쓴 값과 다른가(옵션만 바뀐 것도 · 472-5). */
foreign: () => boolean;
/** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인
* 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */
overrideOptions: (chainageM: number, patch: Record<string, string | number>) => void;
+2 -1
View File
@@ -29,6 +29,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
| 자동저장(디바운스 PUT) 없음 · 화면 잠금 = [저장] · [확정] · [초기화] · 추천 [적용]만 | `design_save.ts:249` | — |
| 측점 열쇠 = `common_util_station_key` 한 벌 · 노선 바뀜 = 관 · 구조물 좌표(x · y) 따라감 · 누가거리 다시 매김 | `common_util_station_key.ts:32` · `common_util_route_follow.ts:291` | `test_344_3_station_key_mirror` · `test_410_follow_xy` |
| B05 · B06 캐시 = 한 벌 · [저장] 한 번에 함께 나감 | `design_shared.ts:395-448` · `design_save.ts:237` | — |
| 관 · 구조물 주인 = 가게 칸 하나 — 화면 사본은 가게 알림으로 맞춤(제 쓰기는 같은 값 · 남 고침은 다시 실음) | `design_view_profile.ts` 가게 구독 · `design_view_cross.ts` onShop · `B06_Section_UI_Page_Structures_Panel.ts` seen | `test_472_5_shop_owner` |
| 정본 자리 = 값을 낳은 주인 — 노선 시설 = `pipe_points.json` · `structures.json` · 횡단 값 = 횡단 패치 | `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B05_Profile_Structures_Router.py:386-405` · `B06_Section_Section_Store.ts:449` | — |
## 바꿀 때 할 일
@@ -51,7 +52,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
| [저장] 9 차례 · 차례마다 따로 커밋 — 실패 = 앞 차례만 저장 | `design_save.ts:253-299` | 472-6 |
| `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 |
| 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠 | `design_save.ts:187-188` | 추정 |
| ↶ ↷ 가 세션을 직접 되씀 — B06 조정 메모리 맵 · B05 상단측 맵이 옛 값일 수 있음 | `design_store.ts:231-235` | 추정 |
| ↶ ↷ 가 세션을 직접 되씀 — 관 · 구조물 · 추가 측점은 되쓴 뒤 가게 듣는 쪽을 부름(`announceDesignStore` · 472-5) · B06 조정 메모리 맵 · B05 상단측 맵은 아직 옛 값일 수 있음 | `B05_Profile_UI_Profile_History.ts` apply · `design_store.ts` announceDesignStore | 관 · 구조물 = `test_472_5_shop_owner` · 나머지 추정 |
| 스냅 켬 · 간격 = 되돌리기 밖 | `B05_Profile_UI_RouteEdit_SnapStep.ts:74-78` | 추정 |
| `flushDesignStore` 부름 없음 — 멈춘 뒤 1 s 안 새로고침 = 가게 세션 사본 잃음(다시 셈) | `design_store.ts:179` | 코드 확인 |
| 「[]」 — 되돌리기 = 값 · 떠남 확인 = 빈 칸 | `B05_Profile_UI_Profile_History.ts:50` · `design_leave.ts:41-42` | 코드 확인 |
+2 -2
View File
@@ -24,8 +24,8 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
| 횡단 설계 결과(설계선) | DB `cross_sections.design` | 상세 cache(살아 있는 객체 · 제자리 고침) · 세션 `section-detail`(서버 사본만) · canon · 묶음 cross · `pipelineDetail` | TS `common_util_cross_design.ts:267` ↔ 파이썬 `B06_Section_Engine_Design.py:195` · `test_b06_cross_design_mirror` | 다시 셈마다 cache 제자리 · 입력 물러섬 = 저장 정본(`B06_Section_Cross_Refresh.ts:402-408`) · 페이지 수명 · [저장]·[확정]·[초기화] 뒤 비움 |
| 측점 면적 | DB 면적 — 표준 측점 = 파이썬 · 구조물 측점 = Node | cache 면적 · 묶음 cross | TS `common_util_cross_design_areas.ts` ↔ 파이썬 `B06_Section_Engine_Areas.py` · 구조물 = `B06_Section_Structure_Layouts.ts`(Node 한 벌) | [저장] 때 서버가 브라우저 면적 버리고 다시 셈 |
| 누가토량 | 종단 파일 `mass_haul` | 그릴 때 셈 · 배지 · `_derived/haul` · `B05_Profile_Api_HaulPlan.ts:28` kept | TS `common_util_mass_haul.ts:426` 한 벌(브라우저 · Node) | 그릴 때(접혀도 셈) · [저장] |
| 구조물 | `structures.json` | `B05_Profile_UI_Page_Structures.ts:78` · `B06_Section_UI_Page_Structures_Panel.ts:175` · 시설 줄 `design_facility_lane.ts:97` · savedStructures · `design_page_data.ts:25` · 세션 `structures` · `structure-type-cache` · cache | 따라감 TS `common_util_route_follow.ts:291` ↔ 파이썬 `common_util_route_follow.py:120` · `test_410_follow_xy` | 시설 줄 알림 · 숨은 보기는 보일 때 |
| 관 | `pipe_points.json` | 배수판 둘(`B05_Profile_UI_Drainage_Panel.ts:123 · 142`) · `B06_Section_UI_Page_Structures_Panel.ts:176` · 시설 줄 `:101` · `design_view_profile.ts:116` · savedPipes · `design_pipe_ids.ts:24` · 세션 `pipes` · `pipe-ids` · `culvertopt` · `kindreset` · `pipe-option-queue` · cache culvert · 유역 pkl | 관 길이 TS `design_structure_pipe_calc.ts` · `B06_Section_Structure_Layouts.ts:95`(Node 한 벌) · `test_b06_pipe_length` | 시설 줄 알림 · 배수 다시 나눔 응답 · [저장] 뒤 다시 받음 |
| 구조물 | `structures.json` | `B05_Profile_UI_Page_Structures.ts:78` · `B06_Section_UI_Page_Structures_Panel.ts:175` · 시설 줄 `design_facility_lane.ts:97` · savedStructures · `design_page_data.ts:25` · 세션 `structures` · `structure-type-cache` · cache | 따라감 TS `common_util_route_follow.ts:291` ↔ 파이썬 `common_util_route_follow.py:120` · `test_410_follow_xy` | 주인 = 가게 `structures`(세션) · 사본은 화면 사본 — 가게가 바뀌면(되돌리기 포함 · `announceDesignStore`) 종단 목록 `takeStructures`(보일 때) · 횡단 판 `foreign()` → 다시 실음 · 숨은 보기는 보일 때 load |
| 관 | `pipe_points.json` | 배수판 둘(`B05_Profile_UI_Drainage_Panel.ts:123 · 142`) · `B06_Section_UI_Page_Structures_Panel.ts:176` · 시설 줄 `:101` · `design_view_profile.ts:116` · savedPipes · `design_pipe_ids.ts:24` · 세션 `pipes` · `pipe-ids` · `culvertopt` · `kindreset` · `pipe-option-queue` · cache culvert · 유역 pkl | 관 길이 TS `design_structure_pipe_calc.ts` · `B06_Section_Structure_Layouts.ts:95`(Node 한 벌) · `test_b06_pipe_length` | 주인 = 가게 `pipes` · 배수 편집기는 가게가 바뀌면 숨어도 `adoptPipes`(제 쓰기 메아리 빼고) · 횡단 판 `foreign()` → 다시 실음 · 고름 · 알약 관 = 시설 줄(`shownPipes`) · 배수 다시 나눔 응답 · [저장] 뒤 다시 받음 |
| 추가 측점 | `plus_stations.json` | `B05_Profile_Api_PlusStations.ts:14 · 74 · 76` · 세션 `plus-stations` | 따라감 TS `followPlusDraft` ↔ 파이썬 `B05_Profile_Plus_Stations.py:82` · `test_459_route_seat_mirror` | 노선 초안 follow 뒤 · [저장] |
| 소단 구간 · 깊이 | 구간 = `structures.json` · 깊이 = DB 패치 | 세션 `berm`(시설 줄만 씀) · `bermdepth` · cache `design.berm` · 초안 노선 `bermAt`(`design_shared.ts:248`) | TS `B06_Section_UI_Page_Persist.ts:157` bermSpansFromStructures ↔ 파이썬 `B06_Section_Engine_Structures_Wall.py:124` load_berm_spans · `test_272_berm_source` | 구조물 바뀜 → 시설 줄 `syncBerm`(같으면 안 씀) → cross 한 번 · 사본 없으면 저장분 |
| 페이지 설정 | DB 단계 2 설정 · 종단 정책 | ★ 가게 `pageSettings`(세션 `pagesettings`) — 폼 · 종단도 기울기 한도 · 시작 측점이 듣고 최소고 강제는 바로 읽음 | 정책 파이썬 기준표 ↔ TS `B05_Profile_UI_Panel_Fields.ts` · `test_442_2_grade_limit_mirror` | 열 때 두 보기가 얹음 · [저장] 설정 차례 |
+93
View File
@@ -0,0 +1,93 @@
/* 472-5 헬퍼 — node helper_472_5_shop.mjs <묶음.mjs> · 표준출력 JSON.
* ① 되돌리기 · 다시하기가 가게 듣는 쪽(관 · 구조물 사본)을 부르는가 ② 배수 편집기가 남이 고친 관 초안을 받는가. */
import { pathToFileURL } from "node:url";
const storage = () => {
const m = new Map();
return {
getItem: (k) => (m.has(k) ? m.get(k) : null),
setItem: (k, v) => m.set(k, String(v)),
removeItem: (k) => m.delete(k),
key: (i) => [...m.keys()][i] ?? null,
get length() {
return m.size;
},
clear: () => m.clear(),
};
};
const any = new Proxy(function () {}, {
get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any),
apply: () => any,
construct: () => any,
});
globalThis.window = { sessionStorage: storage(), localStorage: storage() }; // 손길 듣기 없음 = 열 때 규칙 꺼짐
globalThis.sessionStorage = window.sessionStorage;
globalThis.localStorage = window.localStorage;
globalThis.document = any;
const m = await import(pathToFileURL(process.argv[2]).href);
const P = "p472five";
localStorage.setItem("frd_current_project_id", P);
m.pinDraftProject(P);
const store = m.useDesignStore(P, null);
const A = [{ structure_id: "a", type_id: "berm", placement: "point", chainage_m: 20, options: {} }];
const AB = [
...A,
{ structure_id: "b", type_id: "berm", placement: "point", chainage_m: 40, options: {} },
];
const pipes1 = [
{ id: "k1", chainage_m: 100, source: "user", facility: "pipe", options: { diameter_mm: 800 } },
];
const pipes2 = [
{ id: "k1", chainage_m: 100, source: "user", facility: "pipe", options: { diameter_mm: 1000 } },
];
store.set("structures", A);
store.set("pipes", pipes1);
const history = m.createProfileHistory(() => undefined, 100);
store.set("structures", AB);
store.set("pipes", pipes2);
history.record();
const heard = { structures: [], pipes: [] };
store.subscribe("structures", (v) => heard.structures.push(v));
store.subscribe("pipes", (v) => heard.pipes.push(v));
history.undo();
const afterUndo = {
structures: heard.structures.at(-1) ?? null,
pipes: heard.pipes.at(-1) ?? null,
};
history.redo();
const afterRedo = {
structures: heard.structures.at(-1) ?? null,
pipes: heard.pipes.at(-1) ?? null,
};
// ② 배수 편집기 — 같은 몸통 = 메아리(안 받음) · 옵션만 바뀜 = 받음 · 고른 관 유지
const editor = m.createPipeEditor(() => undefined);
const facilities = m.createFacilityStore();
editor.setPipes([
{ chainage_m: 100, reason: "user", id: "k1" },
{ chainage_m: 140, reason: "user", id: "k2" },
]);
facilities.replaceFromResponse(pipes1);
editor.select(0);
const body = () =>
facilities.attach(
editor.pipes().map((p) => ({ chainage_m: p.chainage_m, source: p.reason, id: p.id })),
);
const echo = m.adoptEditorPipes(editor, facilities, body(), body());
const next = [
{ id: "k2", chainage_m: 140, source: "user", facility: "pipe" },
{ id: "k1", chainage_m: 100, source: "user", facility: "pipe", options: { diameter_mm: 1200 } },
];
const adopted = m.adoptEditorPipes(editor, facilities, body(), next);
const picked = editor.pipes()[editor.selected() ?? -1]?.id ?? null;
console.log(
JSON.stringify({
afterUndo,
afterRedo,
echo,
adopted,
picked,
options: facilities.get(100)?.options ?? null,
}),
);
@@ -0,0 +1,6 @@
/* 472-5 헬퍼 — 되돌리기 · 가게 알림 · 배수 편집기 받기(vite ssr) · test_472_5_shop_owner.py 가 부름. */
export { createProfileHistory } from "../../B05_Profile/B05_Profile_UI_Profile_History";
export { pinDraftProject, useDesignStore } from "../../A00_Common/design/design_store";
export { adoptEditorPipes } from "../../B05_Profile/B05_Profile_UI_Drainage_Parts";
export { createPipeEditor } from "../../B05_Profile/B05_Profile_UI_Drainage_Pipes";
export { createFacilityStore } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility";
+2 -1
View File
@@ -101,5 +101,6 @@ def test_종단_보기_배선() -> None:
page = _read("A00_Common", "design", "design_view_profile.ts")
assert "regularChainage:" in page
assert "remember: (chainageM) => rememberPick(chainageM, null)" in page
assert "graphTargetAt(id, chainageM, lastPipes ?? [], sections)" in page
# 472-5 — 관 = 시설 줄(가게 초안 자리 · 노선 따라감 뒤도) · 옛 알림 자리 안 봄
assert "graphTargetAt(id, chainageM, shownPipes(), sections)" in page
assert "const sections = structurePick ? [] :" in page
+109
View File
@@ -0,0 +1,109 @@
"""PLAN 472-5 5-A (요구 273) — 관 · 구조물 값 주인 하나(설계 가게) · 사본 갈림 막음.
못박는 것
- 되돌리기 · 다시하기(세션 직접 되씀)가 가게 듣는 쪽을 되돌린 값으로 부름 — 옛 코드는 안 불러
종단 목록 · 횡단 판 · 배수 편집기가 옛 값을 쥐고 다음 조작이 되돌림을 덮었음(옛 코드 실패).
- 배수 편집기 받기 — 제 쓰기 메아리는 안 받음 · 남이 고친 옵션 · 차례도 받음 · 고른 관(id) 유지.
- 배선 — 종단 보기 = 관(숨어도) · 구조물(보일 때) 구독 · 횡단 보기 = 남 고침(옵션만도) 다시 실음 ·
횡단 판은 쓰기 전에 제 값을 적음(메아리 가름).
./venv/Scripts/python.exe -m pytest resources/tester/test_472_5_shop_owner.py -q
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
reason="프론트엔드 의존성 미설치",
)
def _read(*parts: str) -> str:
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def out(tmp_path_factory) -> dict:
work = tmp_path_factory.mktemp("s4725")
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
"build",
"--configLoader",
"runner",
"--ssr",
"../resources/tester/helper_472_5_shop_entry.ts",
"--outDir",
str(work),
"--logLevel",
"error",
],
cwd=ROOT,
check=True,
capture_output=True,
timeout=300,
)
bundle = work / "bundle.mjs"
shutil.copy(work / "helper_472_5_shop_entry.js", bundle)
proc = subprocess.run( # noqa: S603
["node", str(HERE / "helper_472_5_shop.mjs"), str(bundle)],
cwd=ROOT,
capture_output=True,
text=True,
encoding="utf-8",
timeout=120,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_되돌리기가_가게_듣는_쪽을_부름(out) -> None:
assert [s["structure_id"] for s in out["afterUndo"]["structures"]] == ["a"]
assert out["afterUndo"]["pipes"][0]["options"] == {"diameter_mm": 800}
assert [s["structure_id"] for s in out["afterRedo"]["structures"]] == ["a", "b"]
assert out["afterRedo"]["pipes"][0]["options"] == {"diameter_mm": 1000}
def test_배수_편집기_남_고침만_받음(out) -> None:
assert out["echo"] is False
assert out["adopted"] is True
assert out["options"] == {"diameter_mm": 1200}
assert out["picked"] == "k1"
def test_배선() -> None:
profile = _read("A00_Common", "design", "design_view_profile.ts")
assert (
'designStore.subscribe("pipes", (next) => next && profilePanel.drainage.adoptPipes(next))'
in profile
)
assert (
'designStore.subscribe("structures", (next) => host.visible() && '
"bridge.takeStructures(next))" in profile
)
cross = _read("A00_Common", "design", "design_view_cross.ts")
assert "structuresPanel.foreign() && reloadPanel()" in cross
assert (
'shop.subscribe("pipes", onShop)' in cross
and 'shop.subscribe("structures", onShop)' in cross
)
panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts")
assert panel.index("seen.pipes = JSON.stringify(next);") < panel.index(
"writePendingPipes(deps.projectId, next);"
)
assert panel.index("seen.structures = JSON.stringify(structures);") < panel.index(
"writePendingStructures(deps.projectId, structures);"
)
history = _read("B05_Profile", "B05_Profile_UI_Profile_History.ts")
assert "announceDesignStore(projectId);" in history