undo redo 시 메뉴 변경

This commit is contained in:
hyojun.choi 2026-04-13 11:10:38 +09:00
parent 53226d00fd
commit d4766e30a3
4 changed files with 47 additions and 11 deletions

View File

@ -6,9 +6,11 @@ import {
adsorptionPointModeState, adsorptionPointModeState,
adsorptionRangeState, adsorptionRangeState,
canvasState, canvasState,
currentMenuState,
dotLineIntervalSelector, dotLineIntervalSelector,
verticalHorizontalModeState, verticalHorizontalModeState,
} from '@/store/canvasAtom' } from '@/store/canvasAtom'
import { MENU } from '@/common/common'
import { useEvent } from '@/hooks/useEvent' import { useEvent } from '@/hooks/useEvent'
import { useMouse } from '@/hooks/useMouse' import { useMouse } from '@/hooks/useMouse'
import { useLine } from '@/hooks/useLine' import { useLine } from '@/hooks/useLine'
@ -88,6 +90,7 @@ export function useOuterLineWall(id, propertiesId) {
const { addPopup, closePopup } = usePopup() const { addPopup, closePopup } = usePopup()
const setOuterLineFix = useSetRecoilState(outerLineFixState) const setOuterLineFix = useSetRecoilState(outerLineFixState)
const setCurrentMenu = useSetRecoilState(currentMenuState)
const outerLineDiagonalLengthRef = useRef(null) const outerLineDiagonalLengthRef = useRef(null)
@ -257,6 +260,8 @@ export function useOuterLineWall(id, propertiesId) {
canvas.outerLineFix = true canvas.outerLineFix = true
closePopup(id) closePopup(id)
ccwCheck() ccwCheck()
// 외벽선 완료 → 지붕형상 설정 메뉴로 전환
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS)
addPopup(propertiesId, 1, <RoofShapeSetting id={propertiesId} pos={{ x: 50, y: 230 }} />) addPopup(propertiesId, 1, <RoofShapeSetting id={propertiesId} pos={{ x: 50, y: 230 }} />)
} }

View File

@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
import { useMessage } from '@/hooks/useMessage' import { useMessage } from '@/hooks/useMessage'
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, currentMenuState, currentObjectState, pitchTextSelector } from '@/store/canvasAtom' import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, currentMenuState, currentObjectState, pitchTextSelector } from '@/store/canvasAtom'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
import { usePolygon } from '@/hooks/usePolygon' import { usePolygon } from '@/hooks/usePolygon'
import { useMode } from '@/hooks/useMode' import { useMode } from '@/hooks/useMode'
import { useLine } from '@/hooks/useLine' import { useLine } from '@/hooks/useLine'
@ -21,6 +21,8 @@ import {
import { useAxios } from '@/hooks/useAxios' import { useAxios } from '@/hooks/useAxios'
import { globalLocaleStore } from '@/store/localeAtom' import { globalLocaleStore } from '@/store/localeAtom'
import { useUndoRedo } from '@/hooks/useUndoRedo' import { useUndoRedo } from '@/hooks/useUndoRedo'
import { pendingPopupState } from '@/store/popupAtom'
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
// 지붕형상 설정 // 지붕형상 설정
export function useRoofShapeSetting(id) { export function useRoofShapeSetting(id) {
@ -71,6 +73,7 @@ export function useRoofShapeSetting(id) {
const { addLine, removeLine } = useLine() const { addLine, removeLine } = useLine()
const { saveSnapshot } = useUndoRedo() const { saveSnapshot } = useUndoRedo()
const [pendingPopup, setPendingPopup] = useRecoilState(pendingPopupState)
useEffect(() => { useEffect(() => {
pitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? pitch : getChonByDegree(pitch) pitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? pitch : getChonByDegree(pitch)
@ -82,6 +85,18 @@ export function useRoofShapeSetting(id) {
jerkinHeadPitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? jerkinHeadPitch : getChonByDegree(jerkinHeadPitch) jerkinHeadPitchRef.current = currentAngleType === ANGLE_TYPE.SLOPE ? jerkinHeadPitch : getChonByDegree(jerkinHeadPitch)
}, [jerkinHeadPitch]) }, [jerkinHeadPitch])
// undo/redo 로 pendingPopup 이 복원되면 해당 팝업을 다시 연다
useEffect(() => {
if (!pendingPopup) return
if (pendingPopup.type === 'RoofShapeSetting') {
addPopup(pendingPopup.id, 1, <RoofShapeSetting id={pendingPopup.id} pos={pendingPopup.pos} />)
setPendingPopup(null)
} else if (pendingPopup.type === 'RoofAllocation') {
addPopup(pendingPopup.id, 1, <RoofAllocationSetting id={pendingPopup.id} pos={pendingPopup.pos} />)
setPendingPopup(null)
}
}, [pendingPopup])
useEffect(() => { useEffect(() => {
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
if (!canvas.outerLineFix || outerLines.length === 0) { if (!canvas.outerLineFix || outerLines.length === 0) {
@ -184,7 +199,8 @@ export function useRoofShapeSetting(id) {
] ]
const handleSave = () => { const handleSave = () => {
saveSnapshot() // undo 시 지붕형상 설정 팝업이 다시 열리도록 pendingPopup 을 atomOverrides 로 직접 전달
saveSnapshot({ pendingPopup: { type: 'RoofShapeSetting', id, pos: { x: 50, y: 230 } } })
let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine') let outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
let direction let direction
@ -485,6 +501,8 @@ export function useRoofShapeSetting(id) {
canvas?.renderAll() canvas?.renderAll()
roof.drawHelpLine(settingModalFirstOptions) roof.drawHelpLine(settingModalFirstOptions)
isFixRef.current = true isFixRef.current = true
// 지붕형상 설정 완료 → 지붕면 할당 메뉴로 전환
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC)
addPopup(id, 1, <RoofAllocationSetting id={id} pos={{ x: 50, y: 230 }} />) addPopup(id, 1, <RoofAllocationSetting id={id} pos={{ x: 50, y: 230 }} />)
} }

View File

@ -15,7 +15,7 @@ import {
} from '@/store/canvasAtom' } from '@/store/canvasAtom'
import { outerLinePointsState } from '@/store/outerLineAtom' import { outerLinePointsState } from '@/store/outerLineAtom'
import { selectedMenuState } from '@/store/menuAtom' import { selectedMenuState } from '@/store/menuAtom'
import { popupState } from '@/store/popupAtom' import { pendingPopupState, popupState } from '@/store/popupAtom'
import { POLYGON_TYPE, SAVE_KEY } from '@/common/common' import { POLYGON_TYPE, SAVE_KEY } from '@/common/common'
const MAX_HISTORY = 3 const MAX_HISTORY = 3
@ -103,6 +103,7 @@ export function useUndoRedo() {
const [currentMenu, setCurrentMenu] = useRecoilState(currentMenuState) const [currentMenu, setCurrentMenu] = useRecoilState(currentMenuState)
const [, setCanvasZoom] = useRecoilState(canvasZoomState) const [, setCanvasZoom] = useRecoilState(canvasZoomState)
const setPopup = useSetRecoilState(popupState) const setPopup = useSetRecoilState(popupState)
const [pendingPopup, setPendingPopup] = useRecoilState(pendingPopupState)
// 최신 스택 값을 ref로 유지하여 클로저 stale 문제 방지 // 최신 스택 값을 ref로 유지하여 클로저 stale 문제 방지
const undoStackRef = useRef(undoStack) const undoStackRef = useRef(undoStack)
@ -118,8 +119,9 @@ export function useUndoRedo() {
outerLinePoints: safeClone(outerLinePoints || []), outerLinePoints: safeClone(outerLinePoints || []),
selectedMenu, selectedMenu,
currentMenu, currentMenu,
pendingPopup: pendingPopup ? safeClone(pendingPopup) : null,
} }
}, [moduleIsSetup, checkedModule, moduleRowColArray, outerLinePoints, selectedMenu, currentMenu]) }, [moduleIsSetup, checkedModule, moduleRowColArray, outerLinePoints, selectedMenu, currentMenu, pendingPopup])
// loadFromJSON 후 오브젝트 간 라이브 참조 관계를 재구축 // loadFromJSON 후 오브젝트 간 라이브 참조 관계를 재구축
const rebuildObjectGraph = useCallback(() => { const rebuildObjectGraph = useCallback(() => {
@ -266,6 +268,8 @@ export function useUndoRedo() {
setOuterLinePoints(atomSnapshot.outerLinePoints || []) setOuterLinePoints(atomSnapshot.outerLinePoints || [])
if (atomSnapshot.selectedMenu !== undefined) setSelectedMenu(atomSnapshot.selectedMenu) if (atomSnapshot.selectedMenu !== undefined) setSelectedMenu(atomSnapshot.selectedMenu)
if (atomSnapshot.currentMenu !== undefined) setCurrentMenu(atomSnapshot.currentMenu) if (atomSnapshot.currentMenu !== undefined) setCurrentMenu(atomSnapshot.currentMenu)
// undo/redo 시 복원해야 할 팝업 정보
setPendingPopup(atomSnapshot.pendingPopup ?? null)
}, []) }, [])
// canvas.toJSON(SAVE_KEY)의 결과를 순환 참조 없이 안전하게 직렬화 // canvas.toJSON(SAVE_KEY)의 결과를 순환 참조 없이 안전하게 직렬화
@ -281,7 +285,7 @@ export function useUndoRedo() {
} }
}, [canvas]) }, [canvas])
const saveSnapshot = useCallback(() => { const saveSnapshot = useCallback((atomOverrides = null) => {
if (!canvas || _isRestoring) return if (!canvas || _isRestoring) return
// snapshot 전에 mouseLine 제거 // snapshot 전에 mouseLine 제거
@ -293,6 +297,11 @@ export function useUndoRedo() {
const atomSnapshot = getAtomSnapshot() const atomSnapshot = getAtomSnapshot()
// atomOverrides 로 비동기 state 반영이 안 된 값을 즉시 덮어쓸 수 있다
if (atomOverrides) {
Object.assign(atomSnapshot, atomOverrides)
}
const snapshot = { const snapshot = {
canvasJSON, canvasJSON,
atomSnapshot, atomSnapshot,
@ -329,9 +338,8 @@ export function useUndoRedo() {
const newUndoStack = [...currentUndoStack] const newUndoStack = [...currentUndoStack]
const snapshot = newUndoStack.pop() const snapshot = newUndoStack.pop()
if (snapshot.atomSnapshot?.currentMenu !== currentMenu) { // undo/redo 시 항상 팝업을 닫고 복원한다
closeAllPopups() closeAllPopups()
}
setUndoStack(newUndoStack) setUndoStack(newUndoStack)
setRedoStack((prev) => [...prev, currentSnapshot]) setRedoStack((prev) => [...prev, currentSnapshot])
@ -355,9 +363,8 @@ export function useUndoRedo() {
const newRedoStack = [...currentRedoStack] const newRedoStack = [...currentRedoStack]
const snapshot = newRedoStack.pop() const snapshot = newRedoStack.pop()
if (snapshot.atomSnapshot?.currentMenu !== currentMenu) { // undo/redo 시 항상 팝업을 닫고 복원한다
closeAllPopups() closeAllPopups()
}
setRedoStack(newRedoStack) setRedoStack(newRedoStack)
setUndoStack((prev) => [...prev, currentSnapshot]) setUndoStack((prev) => [...prev, currentSnapshot])

View File

@ -39,3 +39,9 @@ export const promisePopupState = atom({
key: 'promisePopupStore', key: 'promisePopupStore',
default: false, default: false,
}) })
/** undo/redo 시 복원해야 할 팝업 정보 (JSX 직렬화 불가 → 타입+파라미터만 저장) */
export const pendingPopupState = atom({
key: 'pendingPopupState',
default: null, // { type: 'RoofAllocation', id, pos } 등
})