From 80720fef2a3e39375966499913e9af9529d1e7a3 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Thu, 7 May 2026 10:53:51 +0900 Subject: [PATCH] =?UTF-8?q?=EC=99=B8=EB=B2=BD=EC=84=A0,=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=EB=A9=B4=20Redo=20undo=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/MenuDepth01.jsx | 5 ++ src/hooks/roofcover/useOuterLineWall.js | 15 +++++- src/hooks/surface/usePlacementShapeDrawing.js | 9 ++-- src/hooks/useUndoRedo.js | 54 +++++++++++++++++-- src/store/canvasAtom.js | 7 +++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/components/floor-plan/MenuDepth01.jsx b/src/components/floor-plan/MenuDepth01.jsx index d6bb2477..9e02df75 100644 --- a/src/components/floor-plan/MenuDepth01.jsx +++ b/src/components/floor-plan/MenuDepth01.jsx @@ -9,6 +9,7 @@ import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import { subMenusState } from '@/store/menuAtom' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { commonUtilsState } from '@/store/commonUtilsAtom' +import { isUndoRedoRestoring } from '@/hooks/useUndoRedo' export default function MenuDepth01() { const canvas = useRecoilValue(canvasState) @@ -29,6 +30,10 @@ export default function MenuDepth01() { } useEffect(() => { + // undo/redo 복원 중에는 메뉴 변경에 따른 팝업 자동 오픈을 건너뛴다. + // 그렇지 않으면 그리기 팝업이 다시 열리며 setDrawing(true) 가 호출되어 + // redo 버튼이 비활성화되는 문제가 발생한다. + if (isUndoRedoRestoring()) return handleMenu(selectedMenu) canvas?.discardActiveObject() }, [currentMenu]) diff --git a/src/hooks/roofcover/useOuterLineWall.js b/src/hooks/roofcover/useOuterLineWall.js index 44f53475..9fcd56a2 100644 --- a/src/hooks/roofcover/useOuterLineWall.js +++ b/src/hooks/roofcover/useOuterLineWall.js @@ -63,7 +63,7 @@ export function useOuterLineWall(id, propertiesId) { const { tempGridMode } = useTempGrid() const { addPolygonByLines } = usePolygon() const { handleSelectableObjects } = useObject() - const { saveSnapshot } = useUndoRedo() + const { saveSnapshot, setDrawing } = useUndoRedo() const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState) const adsorptionPointAddMode = useRecoilValue(adsorptionPointAddModeState) @@ -121,6 +121,15 @@ export function useOuterLineWall(id, propertiesId) { } }, [verticalHorizontalMode, points, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, interval, tempGridMode]) + // 외벽선 그리기 진행 중에는 undo/redo/스냅샷을 차단한다. + // 그리기 전 스냅샷은 첫 클릭(mouseDown) 에서 force 로 저장한다 (기존 동작 유지). + useEffect(() => { + setDrawing(true) + return () => { + setDrawing(false) + } + }, []) + useEffect(() => { arrow1Ref.current = arrow1 }, [arrow1]) @@ -158,7 +167,9 @@ export function useOuterLineWall(id, propertiesId) { pointer = { x: Big(pointer.x).toNumber(), y: Big(pointer.y).toNumber() } if (points.length === 0) { - saveSnapshot() + // 그리기 진행 중에는 스냅샷이 차단되므로 force 로 저장한다. + // (처음 한 번만 저장되고 이후 추가 클릭은 points.length > 0 분기로 빠지므로 중복 저장 안 됨) + saveSnapshot(null, { force: true }) setPoints((prev) => [...prev, pointer]) } else { const lastPoint = points[points.length - 1] diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index 67ff35f9..d23def24 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -51,7 +51,7 @@ export function usePlacementShapeDrawing(id) { const { setSurfaceShapePattern } = useRoofFn() const { changeSurfaceLineType } = useSurfaceShapeBatch({}) const { handleSelectableObjects } = useObject() - const { saveSnapshot, popLastSnapshot } = useUndoRedo() + const { saveSnapshot, popLastSnapshot, setDrawing } = useUndoRedo() const { swalFire } = useSwal() const { getMessage } = useMessage() @@ -98,12 +98,15 @@ export function usePlacementShapeDrawing(id) { }, [verticalHorizontalMode, points, adsorptionRange, adsorptionPointMode]) useEffect(() => { - // 배치면 그리기 시작 시 스냅샷 저장 (undo 하면 그리기 전 상태로 복원) - saveSnapshot() + // 배치면 그리기 시작 시 스냅샷 저장 (undo 하면 그리기 전 상태로 복원). + // 그리기 진행 중에는 undo/redo/snapshot 을 차단하므로 force 로 저장한다. + setDrawing(true) + saveSnapshot(null, { force: true }) setPoints([]) setType(OUTER_LINE_TYPE.OUTER_LINE) return () => { + setDrawing(false) const placementShapeDrawingStartPoint = canvas.getObjects().find((obj) => obj.name === 'placementShapeDrawingStartPoint') canvas.remove(placementShapeDrawingStartPoint) diff --git a/src/hooks/useUndoRedo.js b/src/hooks/useUndoRedo.js index 2047dc7e..1567985b 100644 --- a/src/hooks/useUndoRedo.js +++ b/src/hooks/useUndoRedo.js @@ -11,6 +11,7 @@ import { moduleSetupSurfaceState, redoStackState, roofPolygonArrayState, + undoRedoDrawingState, undoStackState, } from '@/store/canvasAtom' import { outerLinePointsState } from '@/store/outerLineAtom' @@ -25,6 +26,10 @@ const MAX_HISTORY = 3 let _isRestoring = false let _restoreTimer = null +// 외벽선/배치면 등 그리기 진행 중에는 undo/redo/snapshot 을 차단한다. +// (확정 시점부터 history 가 동작해야 하므로) +let _isDrawing = false + /** * 외부 훅/모듈에서 현재 undo/redo 복원 중인지 확인할 때 사용 */ @@ -32,6 +37,21 @@ export function isUndoRedoRestoring() { return _isRestoring } +/** + * 외벽선/배치면 등 그리기 진행 상태를 외부에서 설정한다. + * 그리기가 시작될 때 true, 확정 또는 취소 시 false 로 설정한다. + */ +export function setUndoRedoDrawing(value) { + _isDrawing = !!value +} + +/** + * 외부 훅/모듈에서 현재 그리기 진행 중인지 확인할 때 사용 + */ +export function isUndoRedoDrawing() { + return _isDrawing +} + // loadFromJSON 공통 처리 - 이벤트 일시 중단 후 복원 function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) { // 진행 중인 복원 타이머가 있으면 취소 @@ -98,6 +118,7 @@ export function useUndoRedo() { const canvas = useRecoilValue(canvasState) const [undoStack, setUndoStack] = useRecoilState(undoStackState) const [redoStack, setRedoStack] = useRecoilState(redoStackState) + const [isDrawing, setIsDrawing] = useRecoilState(undoRedoDrawingState) const [, setModuleSetupSurface] = useRecoilState(moduleSetupSurfaceState) const [moduleIsSetup, setModuleIsSetup] = useRecoilState(moduleIsSetupState) @@ -375,6 +396,9 @@ export function useUndoRedo() { // 지붕형상 수동설정 메뉴에서는 스냅샷을 쌓지 않는다 (force 시 예외) if (!force && currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return + // 외벽선/배치면 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외) + if (!force && (_isDrawing || isDrawing)) return + // snapshot 전에 mouseLine 제거 const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') mouseLines.forEach((obj) => canvas.remove(obj)) @@ -402,7 +426,7 @@ export function useUndoRedo() { return newStack }) setRedoStack([]) - }, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot]) + }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot]) const closeAllPopups = useCallback(() => { setPopup((prev) => ({ @@ -416,6 +440,9 @@ export function useUndoRedo() { // 지붕형상 수동설정 중에는 undo 차단 if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return + // 외벽선/배치면 등 그리기 진행 중에는 undo 차단 + if (_isDrawing || isDrawing) return + const currentUndoStack = undoStackRef.current if (!canvas || currentUndoStack.length === 0 || _isRestoring) return @@ -439,12 +466,15 @@ export function useUndoRedo() { setRedoStack((prev) => [...prev, currentSnapshot]) _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) - }, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) + }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) const redo = useCallback(() => { // 지붕형상 수동설정 중에는 redo 차단 if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return + // 외벽선/배치면 등 그리기 진행 중에는 redo 차단 + if (_isDrawing || isDrawing) return + const currentRedoStack = redoStackRef.current if (!canvas || currentRedoStack.length === 0 || _isRestoring) return @@ -467,7 +497,7 @@ export function useUndoRedo() { setUndoStack((prev) => [...prev, currentSnapshot]) _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAtoms, setCanvasZoom) - }, [canvas, currentMenu, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) + }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) const clearStacks = useCallback(() => { setUndoStack([]) @@ -479,13 +509,27 @@ export function useUndoRedo() { setUndoStack((prev) => (prev.length > 0 ? prev.slice(0, -1) : prev)) }, []) + /** + * 외벽선/배치면 등 그리기 진행 중에는 undo/redo 를 차단한다. + * 마운트 시 true, 언마운트 시 false 로 설정한다. + */ + const setDrawing = useCallback( + (value) => { + const v = !!value + setIsDrawing(v) + setUndoRedoDrawing(v) + }, + [setIsDrawing], + ) + return { saveSnapshot, undo, redo, clearStacks, popLastSnapshot, - canUndo: undoStack.length > 0, - canRedo: redoStack.length > 0, + setDrawing, + canUndo: undoStack.length > 0 && !isDrawing, + canRedo: redoStack.length > 0 && !isDrawing, } } diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index c87db0d1..2949de7e 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -421,3 +421,10 @@ export const redoStackState = atom({ default: [], dangerouslyAllowMutability: true, }) + +// 외벽선/배치면 등 그리기 진행 중 여부. +// true 동안에는 undo/redo 가 비활성화되고 새 스냅샷이 쌓이지 않는다. +export const undoRedoDrawingState = atom({ + key: 'undoRedoDrawingState', + default: false, +})