From 0e9cf938e9fd3a90715e75c4e12388e45028b943 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 11 May 2026 09:59:49 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=80=EB=B6=95=ED=98=95=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EB=8F=99=EC=84=A4=EC=A0=95=20undo=20redo=20=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 50 +++++++++++++++++++ .../roofcover/useRoofShapePassivitySetting.js | 8 ++- src/hooks/useUndoRedo.js | 15 ++---- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 57d859a4..d15242e7 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -265,6 +265,56 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { } }) + // [WALL→ROOF 동기 이동] + // 지붕형상 설정 후 wall 과 roof 는 별개의 폴리곤이고, hip/ridge/baseLine 같은 보조선은 + // 모두 parentId=roof.id 로 묶여 있어 wall 의 directChildren 에 포함되지 않는다. + // wall 만 단독 이동시키면 roof+보조선이 옛 위치에 남아 시각적으로 어긋나므로 + // wall 이동 시 연결된 roof 를 찾아 polygonMoved 로 같은 delta 만큼 이동시킨다. + // (polygonMoved 가 자체적으로 직속 자식 평행이동까지 처리하므로 별도 순회 불필요) + if (this.name === POLYGON_TYPE.WALL) { + const linkedRoof = this.canvas.getObjects().find( + (obj) => obj.name === POLYGON_TYPE.ROOF && obj.wall === this, + ) + if (linkedRoof) { + linkedRoof._preDragLeft = linkedRoof.left + linkedRoof._preDragTop = linkedRoof.top + linkedRoof.set({ + left: (linkedRoof.left ?? 0) + dxModified, + top: (linkedRoof.top ?? 0) + dyModified, + }) + linkedRoof.fire('polygonMoved') + // polygonMoved 가 ROOF 분기에서 _preDragLeft 를 정리하지 않으므로 여기서 정리 + linkedRoof._preDragLeft = null + linkedRoof._preDragTop = null + + // roof 의 attributes.roofId 매칭 객체(예: 일부 baseLine, 텍스트류)는 + // roof.id 와 다른 식별자로 묶여 polygonMoved 의 parentId === roof.id 필터에 + // 잡히지 않을 수 있다. 누락 방지를 위해 추가 평행이동. + const extraKids = this.canvas.getObjects().filter((obj) => { + if (obj === linkedRoof) return false + if (obj.parentId === linkedRoof.id) return false // 이미 polygonMoved 가 처리 + return obj.attributes?.roofId === linkedRoof.id + }) + extraKids.forEach((obj) => { + const next = {} + if (obj.left != null) next.left = obj.left + dxModified + if (obj.top != null) next.top = obj.top + dyModified + if (obj.x1 != null) next.x1 = obj.x1 + dxModified + if (obj.y1 != null) next.y1 = obj.y1 + dyModified + if (obj.x2 != null) next.x2 = obj.x2 + dxModified + if (obj.y2 != null) next.y2 = obj.y2 + dyModified + obj.set(next) + if (obj.startPoint) obj.startPoint = { x: obj.startPoint.x + dxModified, y: obj.startPoint.y + dyModified } + if (obj.endPoint) obj.endPoint = { x: obj.endPoint.x + dxModified, y: obj.endPoint.y + dyModified } + if (obj.minX != null) obj.minX += dxModified + if (obj.maxX != null) obj.maxX += dxModified + if (obj.minY != null) obj.minY += dyModified + if (obj.maxY != null) obj.maxY += dyModified + obj.setCoords?.() + }) + } + } + this.canvas.renderAll() // ROOF 의 경우 polygonMoved 가 같은 일을 다시 하므로, _preDragLeft 는 거기서 정리됨 diff --git a/src/hooks/roofcover/useRoofShapePassivitySetting.js b/src/hooks/roofcover/useRoofShapePassivitySetting.js index 20a31c46..17dd164a 100644 --- a/src/hooks/roofcover/useRoofShapePassivitySetting.js +++ b/src/hooks/roofcover/useRoofShapePassivitySetting.js @@ -42,7 +42,7 @@ export function useRoofShapePassivitySetting(id) { const [isLoading, setIsLoading] = useState(false) const { closePopup } = usePopup() - const { saveSnapshot } = useUndoRedo() + const { saveSnapshot, setDrawing } = useUndoRedo() const buttons = [ { id: 1, name: getMessage('eaves'), type: TYPES.EAVES }, { id: 2, name: getMessage('gable'), type: TYPES.GABLE }, @@ -56,7 +56,13 @@ export function useRoofShapePassivitySetting(id) { closePopup(id) //return } + // 수동설정 popup 진행 중에는 undo/redo/snapshot 차단 + setDrawing(true) setIsLoading(true) + return () => { + // popup 닫힐 때 (handleSave 후 closePopup, 또는 사용자가 그냥 닫기) 잠금 해제 + setDrawing(false) + } }, []) useEffect(() => { diff --git a/src/hooks/useUndoRedo.js b/src/hooks/useUndoRedo.js index cd5ef77b..6ad030fe 100644 --- a/src/hooks/useUndoRedo.js +++ b/src/hooks/useUndoRedo.js @@ -384,10 +384,7 @@ export function useUndoRedo() { const saveSnapshot = useCallback((atomOverrides = null, { force = false } = {}) => { if (!canvas || _isRestoring) return - // 지붕형상 수동설정 메뉴에서는 스냅샷을 쌓지 않는다 (force 시 예외) - if (!force && currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return - - // 외벽선/배치면 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외) + // 외벽선/배치면/지붕형상 수동설정 등 그리기 진행 중에는 스냅샷을 쌓지 않는다 (force 시 예외) if (!force && (_isDrawing || isDrawing)) return // snapshot 전에 mouseLine 제거 @@ -428,10 +425,7 @@ export function useUndoRedo() { }, [setPopup]) const undo = useCallback(() => { - // 지붕형상 수동설정 중에는 undo 차단 - if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return - - // 외벽선/배치면 등 그리기 진행 중에는 undo 차단 + // 외벽선/배치면/지붕형상 수동설정 등 그리기 진행 중에는 undo 차단 if (_isDrawing || isDrawing) return const currentUndoStack = undoStackRef.current @@ -460,10 +454,7 @@ export function useUndoRedo() { }, [canvas, currentMenu, isDrawing, getCanvasSnapshot, getAtomSnapshot, restoreAtomSnapshot, rebuildCanvasAtoms, closeAllPopups]) const redo = useCallback(() => { - // 지붕형상 수동설정 중에는 redo 차단 - if (currentMenu === MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS) return - - // 외벽선/배치면 등 그리기 진행 중에는 redo 차단 + // 외벽선/배치면/지붕형상 수동설정 등 그리기 진행 중에는 redo 차단 if (_isDrawing || isDrawing) return const currentRedoStack = redoStackRef.current