From 4378712b39efc80c6b7be03a9961ed7829bfdc87 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 2 Jun 2026 13:40:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=88=98=EB=8F=99=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A0=84=EB=B6=80=20=EC=95=88=EB=90=98=EB=A9=B4=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=EC=99=84=EB=A3=8C=20=EB=B6=88=EA=B0=80=20=EC=88=98?= =?UTF-8?q?=EB=8F=99=EC=84=A4=EC=A0=95=20=EC=99=84=EB=A3=8C=20=EC=8B=9C=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7=20=EB=B3=B4=EC=A1=B0=EC=84=A0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=99=84=EB=A3=8C=20=EC=8B=9C=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=EC=83=B7=20=EA=B0=9C=EA=B5=AC=20=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=EC=8B=9C=20undo=202=ED=9A=8C=20=ED=95=84=EC=9A=94=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/object/SizeSetting.jsx | 4 +-- src/hooks/object/useObjectBatch.js | 10 +++++--- src/hooks/roofcover/useAuxiliaryDrawing.js | 25 +++++++++++++++++++ .../roofcover/useRoofShapePassivitySetting.js | 21 ++++++++++++---- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/components/floor-plan/modal/object/SizeSetting.jsx b/src/components/floor-plan/modal/object/SizeSetting.jsx index 00fb8e34..da7d818d 100644 --- a/src/components/floor-plan/modal/object/SizeSetting.jsx +++ b/src/components/floor-plan/modal/object/SizeSetting.jsx @@ -16,7 +16,7 @@ import { useSwal } from '@/hooks/useSwal' export default function SizeSetting(props) { const contextPopupPosition = useRecoilValue(contextPopupPositionState) const [settingTarget, setSettingTarget] = useState(props.side || 1) - const { id, pos = contextPopupPosition, target } = props + const { id, pos = contextPopupPosition, target, initialPlacement = false } = props const { getMessage } = useMessage() const { closePopup } = usePopup() const { resizeObjectBatch } = useObjectBatch({}) @@ -58,7 +58,7 @@ export default function SizeSetting(props) { } if (target.name === BATCH_TYPE.OPENING || target.name === BATCH_TYPE.SHADOW) { - resizeObjectBatch(settingTarget, target, width, height, id) + resizeObjectBatch(settingTarget, target, width, height, id, initialPlacement) } else if (target.name === POLYGON_TYPE.ROOF) { resizeSurfaceShapeBatch(settingTarget, target, width, height, id) } diff --git a/src/hooks/object/useObjectBatch.js b/src/hooks/object/useObjectBatch.js index 5b204406..0a295e5d 100644 --- a/src/hooks/object/useObjectBatch.js +++ b/src/hooks/object/useObjectBatch.js @@ -209,7 +209,9 @@ export function useObjectBatch({ isHidden, setIsHidden }) { initEvent() closeAll() - addPopup(popupId, 1, ) + // 초기 배치 흐름에서 연 사이즈 설정: 배치 시작(applyOpeningAndShadow)이 이미 "오브젝트 없음" 스냅샷을 + // 찍어뒀으므로, 이 사이즈 확정은 중복 스냅샷을 만들지 않게 한다(undo 한 번에 제거되도록). + addPopup(popupId, 1, ) } }) } else if (selectedType === INPUT_TYPE.DIMENSION) { @@ -1305,8 +1307,10 @@ export function useObjectBatch({ isHidden, setIsHidden }) { return { leftPoints, rightPoints, groupPoints } } - const resizeObjectBatch = (side, target, width, height, popupId) => { - saveSnapshot() + const resizeObjectBatch = (side, target, width, height, popupId, skipSnapshot = false) => { + // 초기 배치(applyOpeningAndShadow)에서 열린 사이즈 설정이면 배치 시작 스냅샷과 중복되므로 건너뛴다. + // 컨텍스트 메뉴 "사이즈 수정"으로 기존 오브젝트를 수정할 땐 skipSnapshot=false 라 별개 undo 로 남는다. + if (!skipSnapshot) saveSnapshot() const objectWidth = target.width const objectHeight = target.height diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index 17a1d70a..0a0102e4 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -10,6 +10,7 @@ import { fabric } from 'fabric' import { useAdsorptionPoint } from '@/hooks/useAdsorptionPoint' import { useSwal } from '@/hooks/useSwal' import { usePopup } from '@/hooks/usePopup' +import { useUndoRedo } from '@/hooks/useUndoRedo' import { calculateAngle, isSamePoint } from '@/util/qpolygon-utils' import { POLYGON_TYPE } from '@/common/common' import { useMessage } from '../useMessage' @@ -36,6 +37,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { const { getAdsorptionPoints } = useAdsorptionPoint() const { closePopup } = usePopup() const { getMessage } = useMessage() + const { saveSnapshot, setDrawing, popLastSnapshot } = useUndoRedo() const adsorptionRange = useRecoilValue(adsorptionRangeState) @@ -44,6 +46,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { const mousePointerArr = useRef([]) const roofAdsorptionPoints = useRef([]) const lineHistory = useRef([]) + const isFix = useRef(false) const length1Ref = useRef(0) const length2Ref = useRef(0) @@ -117,6 +120,24 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { } }, []) + // 보조선 그리기 시작 시 스냅샷 저장 (undo 하면 보조선 적용 전 상태로 복원). + // 그리기 진행 중에는 undo/redo/snapshot 을 차단하므로 force 로 저장한다. + // 편집 모달(AuxiliaryEdit) 은 id 없이 hook 을 호출하므로 제외한다. + useEffect(() => { + if (!id) return + const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) + if (!roofs || roofs.length === 0) return + setDrawing(true) + saveSnapshot(null, { force: true }) + return () => { + setDrawing(false) + // 확정(완료)하지 않고 닫은 경우 스냅샷 제거 + if (!isFix.current) { + popLastSnapshot() + } + } + }, []) + useEffect(() => { if (type === null) { return @@ -918,6 +939,9 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { return } + // 완료 확정 → 마운트 시 저장한 "보조선 적용 전" 스냅샷을 유지 (언마운트 cleanup 에서 pop 하지 않음) + isFix.current = true + // cutAuxiliary() const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) @@ -978,6 +1002,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { roofBase.innerLines = lineHistory.current.length !== 0 ? [...roofInnerLines] : roofBase.innerLines canvas.renderAll() }) + closePopup(id) } diff --git a/src/hooks/roofcover/useRoofShapePassivitySetting.js b/src/hooks/roofcover/useRoofShapePassivitySetting.js index 17dd164a..9e3b3885 100644 --- a/src/hooks/roofcover/useRoofShapePassivitySetting.js +++ b/src/hooks/roofcover/useRoofShapePassivitySetting.js @@ -1,6 +1,5 @@ -import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, currentObjectState, pitchTextSelector } from '@/store/canvasAtom' +import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, currentMenuState, currentObjectState, pitchTextSelector } from '@/store/canvasAtom' import { useRecoilValue, useSetRecoilState } from 'recoil' -import { currentMenuState } from '@/store/canvasAtom' import { useEffect, useRef, useState } from 'react' import { useLine } from '@/hooks/useLine' import { useMessage } from '@/hooks/useMessage' @@ -38,6 +37,7 @@ export function useRoofShapePassivitySetting(id) { const history = useRef([]) const [type, setType] = useState(TYPES.EAVES) const isFix = useRef(false) + const isConverted = useRef(false) const initLines = useRef([]) const [isLoading, setIsLoading] = useState(false) @@ -52,7 +52,7 @@ export function useRoofShapePassivitySetting(id) { useEffect(() => { const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') if (!canvas.outerLineFix || outerLines.length === 0) { - swalFire({ text: getMessage('wall.line.not.found'),icon: 'warning' }) + swalFire({ text: getMessage('wall.line.not.found'), icon: 'warning' }) closePopup(id) //return } @@ -176,7 +176,6 @@ export function useRoofShapePassivitySetting(id) { } } - saveSnapshot() currentLineRef.current.set({ attributes: { ...attributes, isFixed: true }, }) @@ -202,7 +201,13 @@ export function useRoofShapePassivitySetting(id) { } const handleSave = () => { - saveSnapshot(null, { force: true }) + // 모든 외곽선이 처마/케라바/편류 중 하나로 설정되었는지 검증 → 미설정 라인이 있으면 완료 차단 + const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') + const isAllSet = lines.length > 0 && lines.every((line) => !!line.attributes?.type) + if (!isAllSet) { + swalFire({ text: getMessage('modal.canvas.setting.roofline.properties.setting.not.setting'), icon: 'warning' }) + return + } isFix.current = true handleLineToPolygon() // 수동설정 완료 → 지붕면 할당 메뉴로 전환 (undo 가드 해제) @@ -211,6 +216,10 @@ export function useRoofShapePassivitySetting(id) { } const handleLineToPolygon = () => { + // 완료(handleSave) 후 closePopup → 언마운트 cleanup 에서 중복 호출되는 것을 방지 (스냅샷/지붕 중복 생성 차단) + if (isConverted.current) return + isConverted.current = true + const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) const exceptObjs = canvas.getObjects().filter((obj) => obj.name !== 'outerLine' && obj.parent?.name !== 'outerLine') const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') @@ -259,6 +268,8 @@ export function useRoofShapePassivitySetting(id) { if (isFix.current) { // 완료 한 경우에는 지붕까지 그려줌 addPitchTextsByOuterLines() + // 지붕 그리기 직전(라인 세팅 완료) 상태를 스냅샷 → undo 시 지붕이 그려지기 전 화면으로 복귀 + saveSnapshot(null, { force: true }) const roof = drawRoofPolygon(wall) addLengthText(roof) }