From b775af191120ecbd62279f41c227f4e4bb76149b Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 26 May 2026 10:31:08 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=93=88=20redo=20undo=20=EC=8B=9C=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=EB=90=98=EC=96=B4=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=EA=B2=83=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useUndoRedo.js | 83 ++++++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/src/hooks/useUndoRedo.js b/src/hooks/useUndoRedo.js index bdce19a6..0472ba1d 100644 --- a/src/hooks/useUndoRedo.js +++ b/src/hooks/useUndoRedo.js @@ -17,7 +17,7 @@ import { import { outerLinePointsState } from '@/store/outerLineAtom' import { selectedMenuState } from '@/store/menuAtom' import { pendingPopupState, popupState } from '@/store/popupAtom' -import { MENU, POLYGON_TYPE, SAVE_KEY } from '@/common/common' +import { POLYGON_TYPE, SAVE_KEY } from '@/common/common' const MAX_HISTORY = 3 @@ -70,13 +70,12 @@ function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAt canvas.loadFromJSON(snapshot.canvasJSON, () => { rebuildCanvasAtoms() - // undo/redo 후 모듈 설치영역이 active 인 채로 남으면 - // 사용자가 다시 클릭하지 않아도 선택된 것처럼 보여 자동 배치 등 부작용 발생. - // 활성 객체가 MODULE_SETUP_SURFACE 이면 선택 해제한다. - const active = canvas.getActiveObject() - if (active && active.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) { - canvas.discardActiveObject() - } + canvas.discardActiveObject() + canvas.getObjects().forEach((obj) => { + if (obj.type === 'QPolygon' && obj.name === POLYGON_TYPE.MODULE) { + obj.set({ stroke: 'black', strokeWidth: 0.3 }) + } + }) canvas.renderAll() restoreAtomSnapshot(snapshot.atomSnapshot) @@ -92,6 +91,7 @@ function _restoreFromJSON(canvas, snapshot, restoreAtomSnapshot, rebuildCanvasAt }, 50) }) } + // 순환 참조만 안전하게 건너뛰는 JSON 직렬화 // WeakSet 방식은 공유 참조(같은 배열이 여러 곳에서 참조)까지 제거하므로 사용 불가 // 대신 조상 경로(ancestors) 스택으로 진짜 순환만 감지 @@ -364,7 +364,9 @@ export function useUndoRedo() { if (polygon.id) { polygon.texts = lengthTextsByParent.get(polygon.id) || [] } - } catch (e) { /* skip */ } + } catch (e) { + /* skip */ + } }) // Group 내부 QPolygon(도머 등)도 동일하게 내부 라인 인스턴스만 재구축한다. @@ -379,7 +381,9 @@ export function useUndoRedo() { if (child.id) { child.texts = lengthTextsByParent.get(child.id) || [] } - } catch (e) { /* skip */ } + } catch (e) { + /* skip */ + } } }) }) @@ -409,40 +413,43 @@ export function useUndoRedo() { } }, [canvas]) - const saveSnapshot = useCallback((atomOverrides = null, { force = false } = {}) => { - if (!canvas || _isRestoring) return + const saveSnapshot = useCallback( + (atomOverrides = null, { force = false } = {}) => { + if (!canvas || _isRestoring) return - // 외벽선/배치면/지붕형상 수동설정 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외) - if (!force && (_isDrawing || isDrawing)) return + // 외벽선/배치면/지붕형상 수동설정 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외) + if (!force && (_isDrawing || isDrawing)) return - // snapshot 전에 mouseLine 제거 - const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') - mouseLines.forEach((obj) => canvas.remove(obj)) + // snapshot 전에 mouseLine 제거 + const mouseLines = canvas.getObjects().filter((obj) => obj.name === 'mouseLine') + mouseLines.forEach((obj) => canvas.remove(obj)) - const canvasJSON = getCanvasSnapshot() - if (!canvasJSON) return + const canvasJSON = getCanvasSnapshot() + if (!canvasJSON) return - const atomSnapshot = getAtomSnapshot() + const atomSnapshot = getAtomSnapshot() - // atomOverrides 로 비동기 state 반영이 안 된 값을 즉시 덮어쓸 수 있다 - if (atomOverrides) { - Object.assign(atomSnapshot, atomOverrides) - } - - const snapshot = { - canvasJSON, - atomSnapshot, - } - - setUndoStack((prev) => { - const newStack = [...prev, snapshot] - if (newStack.length > MAX_HISTORY) { - return newStack.slice(newStack.length - MAX_HISTORY) + // atomOverrides 로 비동기 state 반영이 안 된 값을 즉시 덮어쓸 수 있다 + if (atomOverrides) { + Object.assign(atomSnapshot, atomOverrides) } - return newStack - }) - setRedoStack([]) - }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot]) + + const snapshot = { + canvasJSON, + atomSnapshot, + } + + setUndoStack((prev) => { + const newStack = [...prev, snapshot] + if (newStack.length > MAX_HISTORY) { + return newStack.slice(newStack.length - MAX_HISTORY) + } + return newStack + }) + setRedoStack([]) + }, + [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot], + ) const closeAllPopups = useCallback(() => { setPopup((prev) => ({