Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -249,7 +249,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
};
|
||||
|
||||
/* [직선화]·[쉬프트]·되돌리기·방향키 배선은 `_Panel_Tools` 몫(700줄 한계). */
|
||||
const { tools, history, handleToolPick, setListening } = createPanelTools({
|
||||
const { tools, history, handleToolPick, setListening, disposeTools } = createPanelTools({
|
||||
root,
|
||||
base: () => base,
|
||||
alignment: () => alignment,
|
||||
@@ -262,7 +262,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
moveStation: (station, toChainageM) =>
|
||||
callbacks?.onStructureMove?.(station.chainage_m, toChainageM, station),
|
||||
// 판이 없는 보기는 관 자리를 되돌리기에 태우지 않는다.
|
||||
drainage: drainagePanel ?? { pipeChainages: () => [], setPipeChainages: () => undefined },
|
||||
drainage: drainagePanel ?? { setPipeChainages: () => undefined },
|
||||
// 세션이 정본 — 가게가 편집 초안을 다시 읽음(노선 초안 동안은 노선 0 칸) · 계획고가 그대로면 다시 셈 예약 없음(349-0q)
|
||||
restore: () => void (store.resync() ? rebuild() : draw()),
|
||||
restoreSaved: () => store.restoreSaved(),
|
||||
@@ -666,6 +666,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
|
||||
dispose() {
|
||||
pagePipeline.disposePagePipeline(projectId);
|
||||
setListening(false);
|
||||
disposeTools();
|
||||
drag.dispose();
|
||||
crossPreview.dispose();
|
||||
unbindViewport();
|
||||
|
||||
@@ -333,6 +333,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits());
|
||||
facilityStore.replaceFromResponse(response.pipe_points);
|
||||
if (fordFilled) rememberPipes();
|
||||
if (saved?.response === response) saved.request = JSON.stringify(pipeRequest());
|
||||
// 재계산 뒤에는 선택을 다시 알려야 한다 — 관을 새로 넣은 직후에는 목록이 아직
|
||||
// 비어 폼이 열리지 못했고, 중복 억제 가드에 걸려 두 번째 기회를 잃는다.
|
||||
lastNotifiedPipeChainage = null;
|
||||
@@ -368,6 +369,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
})),
|
||||
);
|
||||
|
||||
/** 마지막 저장분 답 · 그 답을 실은 뒤 요청 몸통 — 초안 없는 걸음으로 되돌릴 때 견줌(472 덧). */
|
||||
let saved: { response: DetailBasinResponse; request: string | null } | null = null;
|
||||
|
||||
/** 지금 화면의 관 목록을 세션 초안에 담는다 — 정본 쓰기는 [저장]·[확정] 몫이다. */
|
||||
function rememberPipes(): void {
|
||||
if (!projectId) return;
|
||||
@@ -380,7 +384,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
const pending = readPendingPipes(projectId as string);
|
||||
return pending !== null // 빈 목록 = 다 지움(관 0개 · 312) — 저장분으로 안 넘어감
|
||||
? computeDetailBasins(projectId as string, pending)
|
||||
: fetchDetailPipePoints(projectId as string);
|
||||
: fetchDetailPipePoints(projectId as string).then(
|
||||
(response) => ((saved = { response, request: null }), response),
|
||||
);
|
||||
};
|
||||
|
||||
/** 세부유역을 다시 나눈다(격자 해석 없음) — 같은 틱의 여러 부름은 한 번(342-3 · 옛 소수 관 옮김).
|
||||
@@ -529,6 +535,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
root,
|
||||
load(nextProjectId: string) {
|
||||
if (projectId === nextProjectId && meta) return;
|
||||
if (projectId !== nextProjectId) saved = null;
|
||||
projectId = nextProjectId;
|
||||
void loadLayers();
|
||||
},
|
||||
@@ -563,6 +570,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
|
||||
selectBasin(picked ? picked.index : null, false);
|
||||
},
|
||||
setPipeChainages(chainages) {
|
||||
// 초안 없는 걸음 = 저장분 — 마지막 저장분 그대로면 끝 · 아니면 조용히 다시 읽음(472 덧)
|
||||
if (chainages === null)
|
||||
return void (JSON.stringify(pipeRequest()) !== saved?.request && run(loadSaved, true));
|
||||
const before = pipeEditor.pipes().map((pipe) => pipe.chainage_m);
|
||||
const next = reconcilePipes(pipeEditor.pipes(), chainages);
|
||||
if (!next) return; // 같은 목록 — 되돌아온 것이므로 여기서 끊는다
|
||||
|
||||
@@ -37,8 +37,9 @@ export interface DrainagePanel {
|
||||
/** 종단 테이블에서 배관 라인을 끌었을 때 — 그 자리로 옮기고 세부유역을 다시 나눈다. */
|
||||
movePipe: (fromChainage: number, toChainage: number) => void;
|
||||
/** 관 목록을 통째로 맞춘다(사이드바 구조물 폼 편집 등 밖에서 바뀐 경우).
|
||||
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다. */
|
||||
setPipeChainages: (chainages: ReadonlyArray<number>) => void;
|
||||
* 현재 목록과 같으면 아무 일도 하지 않는다 — 되먹임 고리를 끊는 지점이다.
|
||||
* null = 초안 없는 걸음(되돌리기) — 저장분으로(472 덧). */
|
||||
setPipeChainages: (chainages: ReadonlyArray<number> | null) => void;
|
||||
/** 종단 테이블 우클릭·통합 목록에서 계곡 통과 시설을 넣을 때.
|
||||
* attributes에 시설 종류·구간·부속 옵션이 담긴다(생략 = 맨 배관). */
|
||||
addPipe: (chainageM: number, attributes?: FacilityAttributes) => void;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { adjustStation } from "./logic/B05_Profile_UI_Profile_Alignment";
|
||||
import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit";
|
||||
import {
|
||||
createProfileHistory,
|
||||
onPageHistory,
|
||||
setPageHistory,
|
||||
type ProfileHistory,
|
||||
} from "./B05_Profile_UI_Profile_History";
|
||||
@@ -63,8 +64,7 @@ export interface PanelToolsContext {
|
||||
moveStation: (station: IrregularStation, toChainageM: number) => void;
|
||||
/** 관 목록 정본 — 되돌리기가 구조물 위치까지 되돌리려면 이력이 이 값을 봐야 한다. */
|
||||
drainage: {
|
||||
pipeChainages: () => number[];
|
||||
setPipeChainages: (chainages: number[]) => void;
|
||||
setPipeChainages: (chainages: number[] | null) => void;
|
||||
};
|
||||
/** 세션 복원 후 화면을 다시 세운다(편집 초안 재적재 포함). */
|
||||
restore: () => void;
|
||||
@@ -83,38 +83,26 @@ export interface PanelTools {
|
||||
handleToolPick: (chainageM: number | null) => boolean;
|
||||
/** 창 키 듣기를 잇고 뗀다 — 종단도가 숨거나 떠나면 뗀다(계획서 24-3a). */
|
||||
setListening: (on: boolean) => void;
|
||||
/** 사진첩 알림을 뗀다 — 종단도를 버릴 때. */
|
||||
disposeTools: () => void;
|
||||
}
|
||||
|
||||
export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
/** 이력이 처음 본 관 목록 — 최초 스냅샷에는 아직 키가 없어 여기로 되돌린다. */
|
||||
let initialPipes: number[] | null = null;
|
||||
|
||||
/** 관 자리는 가게 관 초안(`pipes` · 사진에 듦)이 정본 — 찍기 직전엔 처음 본 목록만 잡아 둔다. */
|
||||
function syncPipes(): void {
|
||||
if (initialPipes === null) initialPipes = ctx.drainage.pipeChainages();
|
||||
}
|
||||
|
||||
/** 스냅샷을 되돌린 뒤 관 위치도 그 시점 초안으로 맞춘다(초안 없는 걸음 = 처음 목록). 같은 목록이면 조용히 끝난다. */
|
||||
/** 스냅샷을 되돌린 뒤 관 위치도 그 시점 초안으로 맞춘다 — 관 자리는 가게 관 초안(`pipes` · 사진에 듦)이
|
||||
* 정본 · 초안 없는 걸음 = 저장분(null · 옛 「처음 본 목록」은 판이 읽기 전 빈 목록이라 관 하나 지움 ↶ 이
|
||||
* 같은 목록으로 멈췄음 · 472 덧). 같은 목록이면 조용히 끝난다. */
|
||||
function restorePipes(): void {
|
||||
const draft = useDesignStore(draftProjectId(), null).get("pipes");
|
||||
const chainages = draft
|
||||
? draft.map((pipe) => Math.round(pipe.chainage_m * 100) / 100)
|
||||
: initialPipes;
|
||||
if (chainages) ctx.drainage.setPipeChainages(chainages);
|
||||
ctx.drainage.setPipeChainages(
|
||||
draft ? draft.map((pipe) => Math.round(pipe.chainage_m * 100) / 100) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// 계획선 편집만 되돌리면 구조물 위치가 어긋난다(사용자 확정 ⑥ — 범위는 B05 조작 전부).
|
||||
/** 찍기 직전(세울 때 · 쉬는 동안 온 값 덮을 때 · 연속 조작 멈출 때) 처음 본 관 목록을 잡는다 —
|
||||
* 그래야 그 목록이 조작 **전** 값으로 잡혀 되돌리기가 시작 자리로 돌아온다(2026-09-04 실측). */
|
||||
const inner = createProfileHistory(
|
||||
() => {
|
||||
ctx.restore();
|
||||
restorePipes();
|
||||
},
|
||||
100,
|
||||
syncPipes,
|
||||
);
|
||||
const history: ProfileHistory = { ...inner, pause: () => (syncPipes(), inner.pause()) };
|
||||
const history = createProfileHistory(() => {
|
||||
ctx.restore();
|
||||
restorePipes();
|
||||
}, 100);
|
||||
setPageHistory(history); // 페이지 사진첩 하나 — 노선 편집기도 이것으로 쌓고 되돌림(346-4)
|
||||
|
||||
// [자유지정] — 적용은 직선화 결과와 같은 길(applyEdits · 이력 · 막기 규칙)(215-1).
|
||||
@@ -173,10 +161,10 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
},
|
||||
canResetToSaved: ctx.canRestoreSaved,
|
||||
// 자유지정 동안 ↶ ↷ = 자유지정 안 한 걸음(367-3) — 큰 이력은 안 건드림.
|
||||
onUndo: () => (tools.mode() === "free" ? (free.undo(), freeView.redraw()) : inner.undo()),
|
||||
onRedo: () => (tools.mode() === "free" ? (free.redo(), freeView.redraw()) : inner.redo()),
|
||||
canUndo: () => (tools.mode() === "free" ? free.canUndo() : inner.canUndo()),
|
||||
canRedo: () => (tools.mode() === "free" ? free.canRedo() : inner.canRedo()),
|
||||
onUndo: () => (tools.mode() === "free" ? (free.undo(), freeView.redraw()) : history.undo()),
|
||||
onRedo: () => (tools.mode() === "free" ? (free.redo(), freeView.redraw()) : history.redo()),
|
||||
canUndo: () => (tools.mode() === "free" ? free.canUndo() : history.canUndo()),
|
||||
canRedo: () => (tools.mode() === "free" ? free.canRedo() : history.canRedo()),
|
||||
hasStraightRuns: () => {
|
||||
const alignment = ctx.alignment();
|
||||
return alignment ? hasStraightRuns(alignment) : false;
|
||||
@@ -400,5 +388,7 @@ export function createPanelTools(ctx: PanelToolsContext): PanelTools {
|
||||
window.addEventListener("keydown", onEscape, true);
|
||||
} else stopHold();
|
||||
};
|
||||
return { tools, history, handleToolPick, setListening };
|
||||
// 삭제 직후 등 다시 그리기 전에도 ↶ ↷ 켜짐을 맞춤(472 덧 — 종단 ↶ 가 꺼진 채 남던 흠)
|
||||
const disposeTools = onPageHistory({ change: () => tools.syncHistory() });
|
||||
return { tools, history, handleToolPick, setListening, disposeTools };
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface ProfileToolsCallbacks {
|
||||
export interface ProfileTools {
|
||||
/** 요약줄 맨 앞에 넣을 도구 묶음. 그릴 때마다 새로 만든다. */
|
||||
render: () => HTMLElement;
|
||||
/** 사진첩이 쌓이거나 되돌린 뒤 ↶ ↷ 켜짐만 맞춤 — 다시 그리기를 안 기다림(472 덧). */
|
||||
syncHistory: () => void;
|
||||
mode: () => ProfileToolMode;
|
||||
/** 그래프에서 측점을 눌렀을 때 — 도구가 삼켰으면 true. */
|
||||
handleStationPick: (chainageM: number) => boolean;
|
||||
@@ -88,6 +90,8 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
let mode: ProfileToolMode = "none";
|
||||
let pending: number | null = null;
|
||||
let runs: StraightRun[] = [];
|
||||
/** 마지막에 그린 ↶ ↷ — 다시 그리기 사이에도 켜짐을 맞춤. */
|
||||
let historyButtons: [HTMLButtonElement, HTMLButtonElement] | null = null;
|
||||
|
||||
function reset(): void {
|
||||
mode = "none";
|
||||
@@ -172,6 +176,11 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
callbacks.onChanged();
|
||||
return true;
|
||||
},
|
||||
syncHistory() {
|
||||
if (!historyButtons) return;
|
||||
historyButtons[0].disabled = !callbacks.canUndo();
|
||||
historyButtons[1].disabled = !callbacks.canRedo();
|
||||
},
|
||||
render() {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.className = "b05-route-profile__tools";
|
||||
@@ -193,6 +202,7 @@ export function createProfileTools(callbacks: ProfileToolsCallbacks): ProfileToo
|
||||
undo.setAttribute("aria-label", "되돌리기");
|
||||
redo.innerHTML = HISTORY_ICON.redo;
|
||||
redo.setAttribute("aria-label", "다시하기");
|
||||
historyButtons = [undo, redo];
|
||||
|
||||
const straighten = toolButton(
|
||||
"직선화",
|
||||
|
||||
@@ -28,7 +28,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
| 조작 | 처음 놓이는 곳 | 브라우저 즉시 셈(모듈) | 서버 뒤 셈(API) | [저장]·[확정] 때 | 사본 | 위치(파일:줄) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 구조물 추가 · 옮김 · 지움 | 보기 메모리(B05 `ownStructures` · B06 `structures`) → 시설 줄 `set` → 세션 `structures` | 시설 줄(알약 · 비정규 측점 · 소단 사본 `berm`) · 묶음 stations → 갈아끼움(정본 표본 유지) · 짝 있는 옮김만 종단 꼭짓점 따라감 · 짝 없는 새 측점 = 꼭짓점 안 세움 · cross 면적 | — | `PUT /route/structures`(초안 그대로) → 횡단 차례 측점 세움 · 면적 Node | 11 | `design_shared.ts:303-322` · `B05_Profile_UI_Page_Structures.ts:98-118` · `design_facility_lane.ts:209-217` · `common_util_structure_station.ts:356` · `B05_Profile_Structures_Router.py:386-405` |
|
||||
| 관 추가 · 옮김 · 지움(B05 배수판) | 배수판 메모리(`facilityStore` · `pipeEditor`) → 세션 `pipes` · `pipe-ids` | 구조물과 같음 · 관 길이 다시 셈 · 일반 측점 자리 관 지움 = 갈아끼움 · 관 지움 ↶ = 그 측점 정본 설계 · 열린 관 폼은 ↶ ↷ 로 옮겨진 관을 따라감(`B05_Profile_UI_Structures_Panel.ts` setPipeFacilities) | 배수유역 `/drainage/detail-basins`(저장 안 함) | `PUT /drainage/pipe-points`(관 차례) · 관 옵션 예약 있으면 같은 파일 한 번 더(관 옵션 차례) · 서버 유역 · 측점 세움 | 14 | `B05_Profile_UI_Drainage_Panel.ts:150-153 · 405-421 · 574-655` · `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B04_PreProcess_Router_Basins.py:538 · 574-633` |
|
||||
| 관 추가 · 옮김 · 지움(B05 배수판) | 배수판 메모리(`facilityStore` · `pipeEditor`) → 세션 `pipes` · `pipe-ids` | 구조물과 같음 · 관 길이 다시 셈 · 일반 측점 자리 관 지움 = 갈아끼움 · 관 지움 ↶ = 그 측점 정본 설계 · 관 초안 없는 걸음으로 ↶ = 저장분(배수판 · 시설 줄 · 알약 · 목록 · 배수유역도 함께 · 472 덧) · 열린 관 폼은 ↶ ↷ 로 옮겨진 관을 따라감(`B05_Profile_UI_Structures_Panel.ts` setPipeFacilities) | 배수유역 `/drainage/detail-basins`(저장 안 함) | `PUT /drainage/pipe-points`(관 차례) · 관 옵션 예약 있으면 같은 파일 한 번 더(관 옵션 차례) · 서버 유역 · 측점 세움 | 14 | `B05_Profile_UI_Drainage_Panel.ts:150-153 · 405-421 · 574-655` · `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B04_PreProcess_Router_Basins.py:538 · 574-633` |
|
||||
| 추가 측점 | 세션 `plus-stations` | 시설 줄 · 묶음 stations · 갈아끼움 · 미리보기 `/section/preview-stations` · 꼭짓점 안 세움 | — | `PUT /section/plus-stations` · 서버 x · y · 측점 세움 | 5 | `B05_Profile_UI_Profile_CrossModal.ts:205-246` · `B05_Profile_Api_PlusStations.ts:44-76 · 128-145` · `B06_Section_Router_Stations.py:428-472` |
|
||||
|
||||
## 종단(계획고)
|
||||
|
||||
@@ -12,7 +12,7 @@ source: ["docs/raw/조사/2026-10-19_470_B05B06_데이터흐름.md", "docs/raw/
|
||||
|
||||
| 조작 | 처음 놓이는 곳 | 브라우저 즉시 셈(모듈) | 서버 뒤 셈(API) | [저장]·[확정] 때 | 사본 | 위치(파일:줄) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| ↶ ↷ | 사진첩 = 세션 draft 칸 글(고름 · 관 id 등 뺌) · 빈 칸 「""」·「{}」·「null」 = 칸 없음 · 「[]」 = 값 | 칸 되씀 → 종단 · 시설 · 노선 다시 먹임 · 횡단 입력 물러섬 = 저장 정본 | — | — | 사진 n | `B05_Profile_UI_Profile_History.ts:50 · 57 · 186 · 226-245` · `design_pipeline_facilities.ts:62-69` · `B05_Profile_UI_RouteEdit_History.ts:174-180` |
|
||||
| ↶ ↷ | 사진첩 = 세션 draft 칸 글(고름 · 관 id 등 뺌) · 빈 칸 「""」·「{}」·「null」 = 칸 없음 · 「[]」 = 값 | 칸 되씀 → 종단 · 시설 · 노선 다시 먹임 · 횡단 입력 물러섬 = 저장 정본 · 관 초안 없는 걸음 = 저장분(배수판이 마지막 저장분 답과 견줘 다르면 다시 읽음) · ↶ ↷ 켜짐 = 쌓임 · 되돌림 알림(`change`)으로 바로 | — | — | 사진 n | `B05_Profile_UI_Profile_History.ts:50 · 57 · 186 · 226-245` · `design_pipeline_facilities.ts:62-69` · `B05_Profile_UI_RouteEdit_History.ts:174-180` · `B05_Profile_UI_Profile_Panel_Tools.ts` restorePipes · `B05_Profile_UI_Drainage_Panel.ts` setPipeChainages |
|
||||
| B05 ↔ B06 전환 | 없음(보기 숨김 · 보임) | 보인 보기가 가게 · 세션 다시 읽음 · 입력 대 묶음 맞춤(최대 3 바퀴) · 시설 줄 · 묶음은 숨은 동안도 셈 | — | — | — | `design_page.ts:209-262` · `design_pipeline_page.ts:559-582` · `design_view_cross.ts:557-565` · `design_view_profile.ts:427` |
|
||||
| 종단 판 접기 | pref `profile-collapsed` | 본문 0 크기 = 파생값(낡음 예약 · 유토 총괄값 · 배분 선반입 · 측점 자료)만 셈 · 그리기만 건너뜀 | — | — | — | `B05_Profile_UI_Profile_Render.ts:131-159 · 211-226` · `B05_Profile_UI_Profile_Drag.ts:43-58` |
|
||||
| 팝아웃 | 없음 — 종단 판 DOM 만 새 창 | 같은 함수 · 같은 가게 | — | — | — | `design_profile_popout.ts:87-132` |
|
||||
|
||||
@@ -55,7 +55,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함
|
||||
| 저장 밖 정본 쓰기 셋 — 넓힘 `/regenerate` · [측점 만들기] · 추천 [적용] | `B06_Section_Router.py:378-388` · `B06_Section_Router_Stations.py:236-296` · `B05_Profile_UI_RouteEdit_Apply.ts:220-245` | 사용자 확정 · 확인창 · 472-6 협의 |
|
||||
| `pipe_points.json` 한 [저장]에 두 번 PUT(관 · 관 옵션) | `B06_Section_Api_Culvert_Options.ts:67-95` | 코드 확인 |
|
||||
| 노선 바뀐 [저장]의 관 옵션 차례 = 옛 노선 열쇠(누가거리 · 관이 따라 옮긴 새 자리와 안 맞으면 안 얹힘) | `design_save.ts:145` · `B06_Section_Router_DesignSave.py` `_patched_points` | 추정 · 옛 길과 같음 |
|
||||
| ↶ ↷ 가 세션을 직접 되씀 — 관 · 구조물 · 추가 측점은 되쓴 뒤 가게 듣는 쪽을 부름(`announceDesignStore` · 472-5) · B06 조정창 = 세션 칸이 주인(사본 없음) · 칸이 바뀐 걸음만 상세 얹기 · 카드 · 면적 다시(472-5 5-D) · 암선 · 암 경사 · 상단측 = 세션 칸이 주인(사본 없음 · 472-5) | `B05_Profile_UI_Profile_History.ts` apply · `design_store.ts` announceDesignStore · `B06_Section_UI_Page_Station_Controls.ts` onRestore · `B06_Section_UI_Page_Persist.ts` readCells | 관 · 구조물 = `test_472_5_shop_owner` · 조정창 = `test_472_5d_adjust_owner` · 암선 · 암 경사 = `test_472_5r_rock_owner` |
|
||||
| ↶ ↷ 가 세션을 직접 되씀 — 관 · 구조물 · 추가 측점은 되쓴 뒤 가게 듣는 쪽을 부름(`announceDesignStore` · 472-5) · B06 조정창 = 세션 칸이 주인(사본 없음) · 칸이 바뀐 걸음만 상세 얹기 · 카드 · 면적 다시(472-5 5-D) · 암선 · 암 경사 · 상단측 = 세션 칸이 주인(사본 없음 · 472-5) | `B05_Profile_UI_Profile_History.ts` apply · `design_store.ts` announceDesignStore · `B06_Section_UI_Page_Station_Controls.ts` onRestore · `B06_Section_UI_Page_Persist.ts` readCells | 관 · 구조물 = `test_472_5_shop_owner` · 관 초안 없는 걸음 = 저장분 · ↶ 켜짐 = `test_472_d_b05_pipe_undo` · 조정창 = `test_472_5d_adjust_owner` · 암선 · 암 경사 = `test_472_5r_rock_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` | 코드 확인 |
|
||||
|
||||
@@ -46,6 +46,7 @@ const STUBS = {
|
||||
canRedo: () => false,
|
||||
}),
|
||||
setPageHistory: () => undefined,
|
||||
onPageHistory: () => () => undefined, // ↶ ↷ 켜짐 맞춤(472 덧) · 이 흠과 무관
|
||||
},
|
||||
"./B05_Profile_UI_Profile_Tools": {
|
||||
createProfileTools: () => ({ mode: () => "none", clear: () => undefined }),
|
||||
@@ -95,7 +96,7 @@ tools.createPanelTools({
|
||||
irregularStations: () => [pipe],
|
||||
stationIdOf: (s) => `irregular:${s.id}`,
|
||||
moveStation: (s, to) => calls.move.push([s.chainage_m, to]),
|
||||
drainage: { pipeChainages: () => [], setPipeChainages: () => undefined },
|
||||
drainage: { setPipeChainages: () => undefined },
|
||||
restore: () => undefined,
|
||||
restoreSaved: () => undefined,
|
||||
canRestoreSaved: () => false,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/* 472 덧 헬퍼 — B05 보기 관 지움 ↶ 가 반쪽만 돌아오던 흠 · 종단 ↶ 단추가 다시 그리기 전까지 꺼져 있던 흠.
|
||||
* 실제 TS(`B05_Profile_UI_Profile_Panel_Tools` · `B05_Profile_UI_Profile_History` · 가게)를 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌림 · 배수 판은 흉내(받은 관 목록만 적음) · 결과 = 표준출력 JSON. 짝 = `test_472_d_b05_pipe_undo.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
const listeners = {};
|
||||
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(),
|
||||
};
|
||||
};
|
||||
globalThis.sessionStorage = storage();
|
||||
globalThis.localStorage = storage();
|
||||
globalThis.window = {
|
||||
sessionStorage: globalThis.sessionStorage,
|
||||
localStorage: globalThis.localStorage,
|
||||
addEventListener: (type, fn) => (listeners[type] ??= new Set()).add(fn),
|
||||
removeEventListener: (type, fn) => listeners[type]?.delete(fn),
|
||||
setTimeout: () => 0,
|
||||
clearTimeout: () => undefined,
|
||||
};
|
||||
globalThis.document = { addEventListener: () => undefined, querySelector: () => null };
|
||||
|
||||
// 도구줄 · 자유지정 그림은 흉내 — ↶ ↷ 켜짐 맞춤 부름 수만 셈
|
||||
let synced = 0;
|
||||
const STUBS = {
|
||||
"./B05_Profile_UI_Profile_Tools": {
|
||||
createProfileTools: () => ({
|
||||
mode: () => "none",
|
||||
clear: () => undefined,
|
||||
syncHistory: () => void (synced += 1),
|
||||
}),
|
||||
},
|
||||
"../A00_Common/design/logic/design_profile_free": {
|
||||
createFreeDraw: () => ({ key: () => false, hint: () => "" }),
|
||||
},
|
||||
"../A00_Common/design/design_profile_free_view": {
|
||||
createFreeView: () => ({ mount: () => undefined, redraw: () => undefined }),
|
||||
},
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
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 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) =>
|
||||
(CATALOG.isCatalogRequest(request) ? CATALOG : null) ??
|
||||
STUBS[request] ??
|
||||
(request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const store = loadTs(path.join(ROOT, "A00_Common", "design", "design_store.ts"));
|
||||
const tools = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Panel_Tools.ts"));
|
||||
|
||||
const P = "p472d";
|
||||
localStorage.setItem("frd_current_project_id", P);
|
||||
store.pinDraftProject(P);
|
||||
const shop = store.useDesignStore(P, null);
|
||||
|
||||
// 배수 판 흉내 — 판이 저장분을 읽기 전(빈 목록)에 사진첩이 섬 · 관 하나(351) 프로젝트
|
||||
let shown = [];
|
||||
const given = [];
|
||||
const panel = tools.createPanelTools({
|
||||
root: { isConnected: true },
|
||||
base: () => ({}),
|
||||
alignment: () => null,
|
||||
edits: () => ({}),
|
||||
savedRadii: () => ({}),
|
||||
applyEdits: () => undefined,
|
||||
selectedStationId: () => null,
|
||||
irregularStations: () => [],
|
||||
stationIdOf: (s) => s.id,
|
||||
moveStation: () => undefined,
|
||||
drainage: {
|
||||
pipeChainages: () => shown, // 옛 「처음 본 목록」 길이 부름
|
||||
setPipeChainages: (chainages) => given.push(chainages),
|
||||
},
|
||||
restore: () => undefined,
|
||||
restoreSaved: () => undefined,
|
||||
canRestoreSaved: () => false,
|
||||
refresh: () => undefined,
|
||||
});
|
||||
shown = [351]; // 판이 저장분을 읽음
|
||||
const touch = () => listeners.keydown?.forEach((fn) => fn({ key: "a", target: null }));
|
||||
|
||||
// 종단 우클릭 [배관 삭제] — 판이 관을 빼고 가게 관 초안을 빈 목록으로 씀
|
||||
touch();
|
||||
const syncedBefore = synced;
|
||||
shown = [];
|
||||
shop.set("pipes", []);
|
||||
panel.history.record();
|
||||
const afterDelete = { canUndo: panel.history.canUndo(), synced: synced - syncedBefore };
|
||||
|
||||
panel.history.undo();
|
||||
const undo = { given: given.at(-1), draft: shop.get("pipes") ?? null };
|
||||
panel.history.redo();
|
||||
const redo = { given: given.at(-1), draft: shop.get("pipes") ?? null };
|
||||
panel.history.undo();
|
||||
const undo2 = { given: given.at(-1) };
|
||||
|
||||
process.stdout.write(JSON.stringify({ afterDelete, undo, redo, undo2 }));
|
||||
@@ -135,8 +135,8 @@ def test_적용_막히면_열린_채(out):
|
||||
|
||||
def test_되돌리기_배선_자유지정_먼저():
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : inner.undo()' in panel
|
||||
assert 'tools.mode() === "free" ? free.canUndo() : inner.canUndo()' in panel
|
||||
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : history.undo()' in panel
|
||||
assert 'tools.mode() === "free" ? free.canUndo() : history.canUndo()' in panel
|
||||
keys = panel[panel.index('isCtrlKey(event, "z") || isCtrlKey(event, "y")') :]
|
||||
free_at = keys.index('if (tools.mode() === "free") {')
|
||||
assert (
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""PLAN 472 덧 (요구 272) — B05 보기 관 지움 ↶ 가 반쪽만 돌아옴 · 배수유역도도 안 돌아옴.
|
||||
|
||||
까닭: ↶ 가 관 초안 없는 걸음(저장분)으로 가면 종단 사진첩이 「처음 본 관 목록」을 배수 판에
|
||||
줬는데, 그 목록은 판이 저장분을 읽기 전에 잡혀 빈 목록 — 관 하나 프로젝트에서 지운 뒤 판 목록
|
||||
(빈 목록)과 같아 판이 「같은 목록」으로 멈춤 → 배수 판 · 시설 줄 · 알약 · 목록은 관 없음 ·
|
||||
묶음은 저장분 관(측점 57) · 누가토량 −17,385.4. B06 보기 ↶ 도 같은 길(배수유역도 관 · 세부유역
|
||||
빠짐). 가게 구독은 빈 초안(null)을 안 받음. 또 종단 ↶ 단추는 다시 그릴 때만 켜짐을 맞춰 삭제
|
||||
직후 꺼져 있었음.
|
||||
고침: 초안 없는 걸음 = null(저장분) → 판이 마지막 저장분 답과 견줘 다르면 조용히 다시 읽음 ·
|
||||
사진첩 쌓임 · 되돌림 알림에 ↶ ↷ 켜짐 맞춤. TS 는 `helper_472_d_b05_pipe_undo.cjs` 가 Node 로 돌림.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_d_b05_pipe_undo.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
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" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_472_d_b05_pipe_undo.cjs")],
|
||||
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 out["undo"] == {"given": None, "draft": None}
|
||||
assert out["undo2"]["given"] is None
|
||||
|
||||
|
||||
def test_다시하기는_그_걸음_초안(out) -> None:
|
||||
assert out["redo"] == {"given": [], "draft": []}
|
||||
|
||||
|
||||
def test_삭제_직후_되돌리기_단추_켜짐(out) -> None:
|
||||
"""옛 코드 = 종단 도구줄은 다시 그릴 때만 ↶ 켜짐을 맞춤(맞춤 0 번 · 빨강)."""
|
||||
assert out["afterDelete"]["canUndo"] is True
|
||||
assert out["afterDelete"]["synced"] >= 1
|
||||
|
||||
|
||||
def test_배수_판_저장분_견줌() -> None:
|
||||
panel = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts")
|
||||
# 저장분 답을 받은 때만 견줄 몸통을 적음 · 초안 없는 걸음은 다르면 조용히 다시 읽음
|
||||
assert "(response) => ((saved = { response, request: null }), response)" in panel
|
||||
assert (
|
||||
"if (saved?.response === response) saved.request = JSON.stringify(pipeRequest());" in panel
|
||||
)
|
||||
assert (
|
||||
"return void (JSON.stringify(pipeRequest()) !== saved?.request && run(loadSaved, true));"
|
||||
in panel
|
||||
)
|
||||
assert "if (projectId !== nextProjectId) saved = null;" in panel
|
||||
tools = _read("B05_Profile", "B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert "initialPipes" not in tools
|
||||
profile = _read("A00_Common", "design", "design_profile.ts")
|
||||
assert "disposeTools();" in profile
|
||||
Reference in New Issue
Block a user