# Conflicts: # src/components/fabric/QPolygon.js # src/hooks/module/useModule.js # src/hooks/roofcover/useEavesGableEdit.js # src/hooks/roofcover/useRoofAllocationSetting.js # src/hooks/usePlan.js
768 lines
28 KiB
JavaScript
768 lines
28 KiB
JavaScript
'use client'
|
||
|
||
import { useContext, useEffect, useRef, useState } from 'react'
|
||
import { usePathname, useRouter } from 'next/navigation'
|
||
|
||
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'
|
||
|
||
import {
|
||
canvasSettingState,
|
||
canvasState,
|
||
currentCanvasPlanState,
|
||
currentMenuState,
|
||
currentObjectState,
|
||
moduleSetupSurfaceState,
|
||
plansState,
|
||
} from '@/store/canvasAtom'
|
||
import { useAxios } from '@/hooks/useAxios'
|
||
import { useMessage } from '@/hooks/useMessage'
|
||
import { useSwal } from '@/hooks/useSwal'
|
||
import { POLYGON_TYPE, SAVE_KEY } from '@/common/common'
|
||
import { removeImage } from '@/lib/fileAction'
|
||
import { FloorPlanContext } from '@/app/floor-plan/FloorPlanProvider'
|
||
import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController'
|
||
import { outerLinePointsState } from '@/store/outerLineAtom'
|
||
import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom'
|
||
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
|
||
import { compasDegAtom } from '@/store/orientationAtom'
|
||
import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions'
|
||
import { useCanvasPopupStatusController } from './common/useCanvasPopupStatusController'
|
||
import { useCanvasMenu } from './common/useCanvasMenu'
|
||
import { QcastContext } from '@/app/QcastProvider'
|
||
import { unescapeString } from '@/util/common-utils'
|
||
import { useTrestle } from '@/hooks/module/useTrestle'
|
||
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||
import { logger } from '@/util/logger'
|
||
|
||
// putCanvasStatus 의 in-flight 요청 — module-scope 로 두어 usePlan() 인스턴스 간 공유.
|
||
// 큰 fabric JSON 의 동시 PUT 이 SQL Server 데드락 victim 을 양산해 온 이력 때문에 직전 호출은 abort.
|
||
const canvasStatusInflight = { controller: null }
|
||
|
||
/**
|
||
* 플랜 처리 훅
|
||
* 플랜을 표시하는 탭 UI 전반적인 처리 로직 관리
|
||
* @param {*} params
|
||
* @returns
|
||
*/
|
||
export function usePlan(params = {}) {
|
||
const { floorPlanState } = useContext(FloorPlanContext)
|
||
|
||
const [selectedPlan, setSelectedPlan] = useState(null)
|
||
const setCurrentMenu = useSetRecoilState(currentMenuState)
|
||
const [canvas, setCanvas] = useRecoilState(canvasState)
|
||
|
||
const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState)
|
||
const [plans, setPlans] = useRecoilState(plansState)
|
||
|
||
const router = useRouter()
|
||
const pathname = usePathname()
|
||
|
||
const { swalFire } = useSwal()
|
||
const { getMessage } = useMessage()
|
||
const { get, post, promisePost, promisePut, promiseDel, promiseGet } = useAxios()
|
||
|
||
const { setEstimateContextState, handleDeleteEstimate, handleEstimateImageCopy } = useEstimateController()
|
||
|
||
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
||
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
||
|
||
const { fetchBasicSettings, basicSettingCopySave } = useCanvasSetting()
|
||
const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState)
|
||
|
||
/** 전역 로딩바 컨텍스트 */
|
||
const { setIsGlobalLoading } = useContext(QcastContext)
|
||
|
||
/**
|
||
* 플랜 복사 시 모듈이 있을경우 모듈 데이터 복사하기 위한 처리
|
||
*/
|
||
const { getModuleSelection } = useCanvasPopupStatusController()
|
||
const [compasDeg, setCompasDeg] = useRecoilState(compasDegAtom)
|
||
const [moduleSelectionDataStore, setModuleSelectionDataStore] = useRecoilState(moduleSelectionDataState)
|
||
const [selectedModules, setSelectedModules] = useRecoilState(selectedModuleState)
|
||
const { selectedMenu, setSelectedMenu } = useCanvasMenu()
|
||
|
||
//선택된 객체 초기화
|
||
const resetCurrentObject = useResetRecoilState(currentObjectState)
|
||
//선택된 모듈 배치면 초기화
|
||
const resetModuleSetupSurface = useResetRecoilState(moduleSetupSurfaceState)
|
||
const { isAllComplete } = useTrestle()
|
||
// undo/redo 스택 초기화 (플랜 전환 시 사용)
|
||
const { clearStacks } = useUndoRedo()
|
||
|
||
/**
|
||
* 마우스 포인터의 가이드라인 제거
|
||
*/
|
||
const removeMouseLines = () => {
|
||
if (canvas?._objects.length > 0) {
|
||
const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine')
|
||
mouseLines.forEach((item) => canvas?.remove(item))
|
||
}
|
||
canvas?.renderAll()
|
||
}
|
||
|
||
/**
|
||
* 현재 캔버스 데이터를 JSON 문자열로 직렬화
|
||
*
|
||
* @returns {string} 직렬화된 캔버스 데이터
|
||
*/
|
||
const addCanvas = () => {
|
||
const objs = canvas?.toJSON(SAVE_KEY)
|
||
const str = JSON.stringify(objs)
|
||
return str
|
||
}
|
||
|
||
/**
|
||
* 현재 캔버스에 그려진 데이터를 추출
|
||
*
|
||
* @param {string} mode
|
||
* @returns {string} 캔버스 데이터
|
||
*/
|
||
const currentCanvasData = (mode = '') => {
|
||
removeMouseLines()
|
||
canvas.discardActiveObject()
|
||
|
||
if (mode === 'save') {
|
||
const groups = canvas.getObjects().filter((obj) => obj.type === 'group')
|
||
|
||
if (groups.length > 0) {
|
||
groups.forEach((group) => {
|
||
canvas?.remove(group)
|
||
canvas?.renderAll()
|
||
const restore = group._restoreObjectsState() //그룹 좌표 복구
|
||
|
||
//그룹시 좌표가 틀어지는 이슈
|
||
restore._objects.forEach((obj) => {
|
||
obj.set({
|
||
...obj,
|
||
groupYn: true,
|
||
groupName: group.name,
|
||
groupId: group.id,
|
||
})
|
||
|
||
//디렉션이 있는 경우에만
|
||
if (group.lineDirection) obj.set({ lineDirection: group.lineDirection })
|
||
//부모객체가 있으면 (면형상 위에 도머등..)
|
||
if (group.parentId) obj.set({ parentId: group.parentId })
|
||
|
||
canvas?.add(obj)
|
||
obj.setCoords()
|
||
canvas?.renderAll()
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
return addCanvas()
|
||
}
|
||
|
||
/**
|
||
* DB에 저장된 데이터를 canvas에서 사용할 수 있도록 포맷화
|
||
*
|
||
* @param {string} cs - DB에 저장된 캔버스 데이터 문자열
|
||
* @returns {string} 캔버스에서 사용할 수 있도록 포맷팅된 문자열
|
||
*/
|
||
const dbToCanvasFormat = (cs) => {
|
||
return cs.replace(/##/g, '"').replace(/∠/g, '∠').replace(/°/g, '°')
|
||
}
|
||
|
||
/**
|
||
* canvas의 데이터를 DB에 저장할 수 있도록 포맷화
|
||
*
|
||
* @param {string} cs - 캔버스 데이터 문자열
|
||
* @returns {string} DB 저장용 포맷 문자열
|
||
*/
|
||
const canvasToDbFormat = (cs) => {
|
||
return cs.replace(/"/g, '##')
|
||
}
|
||
|
||
/**
|
||
* 페이지 내 캔버스를 저장
|
||
*
|
||
* @param {boolean} saveAlert - 저장 완료 알림 표시 여부
|
||
*/
|
||
const saveCanvas = async (saveAlert = true) => {
|
||
// [PLAN-CORRUPT-COORD-SAVE-GUARD 2026-05-18] 손상 plan(예: S241X135260518004) 저장 차단형 가드.
|
||
// 그림의 목적: 지붕→모듈→견적→사진. 깨진 모듈도 사용자가 그린 데이터 → 자동 제거 금지.
|
||
// 손상 객체 발견 시 저장 차단 + swal 안내 → 사용자가 화면에서 복원/재배치/명시적 삭제 결정.
|
||
// 향후 [PLAN-CORRUPT-RESTORE] 작업으로 메타 데이터(top/height/surfaceId)에서 좌표 복원 예정.
|
||
const corruptedObjs = canvas.getObjects().filter((obj) => obj.__corrupted)
|
||
if (corruptedObjs.length > 0) {
|
||
// 자세한 객체별 진단 (top/height/fill/moduleInfo/points-sample 까지 — 복원 단서로 사용)
|
||
const detail = corruptedObjs.map((o) => ({
|
||
id: o.id,
|
||
name: o.name,
|
||
parentId: o.parentId,
|
||
surfaceId: o.surfaceId,
|
||
top: o.top,
|
||
left: o.left,
|
||
width: o.width,
|
||
height: o.height,
|
||
fill: typeof o.fill === 'string' ? o.fill : o.fill?.type,
|
||
opacity: o.opacity,
|
||
direction: o.direction,
|
||
moduleInfo: o.moduleInfo ? { itemId: o.moduleInfo.itemId, moduleTpCd: o.moduleInfo.moduleTpCd, capacity: o.moduleInfo.capacity } : null,
|
||
pointsSample: Array.isArray(o.points) ? o.points.slice(0, 4) : null,
|
||
}))
|
||
logger.error('[PLAN-CORRUPT-COORD-SAVE-GUARD 2026-05-18] 保存失敗 — corrupted objects', {
|
||
count: corruptedObjs.length,
|
||
objectNo: currentCanvasPlan?.objectNo,
|
||
planNo: currentCanvasPlan?.planNo,
|
||
detail,
|
||
})
|
||
swalFire({
|
||
icon: 'error',
|
||
title: '保存に失敗しました',
|
||
text: `破損したモジュール ${corruptedObjs.length} 件が含まれています。\n再配置または明示的に削除してから再度保存してください。\n(詳細はコンソールログをご確認ください)`,
|
||
})
|
||
return
|
||
}
|
||
|
||
// [ROOF-MATERIAL-SAVE-GUARD 2026-05-08] 면(roof) 에 시각적 지붕재 패턴은 칠해져 있는데
|
||
// 데이터(roofMaterial) 가 비어있는 "불일치" 케이스만 저장 차단.
|
||
// - fill 비어있고 roofMaterial 도 없음 → 미선택 상태, 저장 허용
|
||
// - fill 이 fabric.Pattern 인데 roofMaterial 누락 → 저장 시 누락 JSON 이 DB 로 가고
|
||
// 재로드 시 setSurfaceShapePattern 0×0 patternCanvas → drawImage InvalidStateError
|
||
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.wall)
|
||
const inconsistent = roofs.filter((r) => {
|
||
const f = r.fill
|
||
const hasPatternFill = f && typeof f === 'object' && (f.source || f.type === 'pattern')
|
||
return hasPatternFill && !r.roofMaterial
|
||
})
|
||
if (inconsistent.length > 0) {
|
||
console.warn(
|
||
'[ROOF-MATERIAL-SAVE-GUARD] pattern drawn but roofMaterial missing:',
|
||
inconsistent.map((r) => ({ id: r.id, name: r.name, fillType: r.fill?.type })),
|
||
)
|
||
swalFire({
|
||
text: '面に屋根材パターンが描画されていますが、屋根材データが保存されていません。\n屋根材を再設定してから保存してください。',
|
||
icon: 'warning',
|
||
})
|
||
return
|
||
}
|
||
|
||
// 저장 전 선택되어 있는 object 제거
|
||
const setupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
||
|
||
setupSurfaces.forEach((surface) => {
|
||
surface.set({ fill: 'rgba(255,255,255,0.1)', strokeDashArray: [10, 4], strokeWidth: 1 })
|
||
})
|
||
|
||
const canvasStatus = currentCanvasData('save')
|
||
const result = await putCanvasStatus(canvasStatus, saveAlert)
|
||
//캔버스 저장 완료 후
|
||
if (result && !isAllComplete()) {
|
||
handleDeleteEstimate(currentCanvasPlan)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* objectNo에 해당하는 canvas 목록을 조회
|
||
*
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {string} planNo - 플랜번호
|
||
* @returns {Promise<Array>} canvas 목록
|
||
*/
|
||
const getCanvasByObjectNo = async (objectNo, planNo) => {
|
||
return await get({ url: `/api/canvas-management/canvas-statuses/by-object/${objectNo}` }).then((res) =>
|
||
res.map((item) => ({
|
||
id: item.id,
|
||
objectNo: item.objectNo,
|
||
planNo: item.planNo,
|
||
userId: item.userId,
|
||
canvasStatus: dbToCanvasFormat(item.canvasStatus),
|
||
isCurrent: planNo === item.planNo,
|
||
bgImageName: item.bgImageName,
|
||
mapPositionAddress: item.mapPositionAddress,
|
||
})),
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 신규 plan 추가
|
||
*
|
||
* @param {string} userId - 사용자 ID
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {boolean} isCopy - 복제 여부
|
||
* @param {boolean} isInitPlan - 초기 플랜 생성 여부
|
||
*
|
||
* case 1) 초기 플랜 생성 : isInitPlan = true, isCopy = false
|
||
* case 2) 빈 플랜 생성 : isInitPlan = false, isCopy = false
|
||
* case 3) 복제 플랜 생성 : isInitPlan = false, isCopy = true
|
||
*/
|
||
const postObjectPlan = async (userId, objectNo, isCopy = false, isInitPlan = false) => {
|
||
const planData = isCopy
|
||
? {
|
||
userId: userId,
|
||
objectNo: objectNo,
|
||
copyFlg: '1',
|
||
planNo: currentCanvasPlan?.planNo,
|
||
}
|
||
: {
|
||
userId: userId,
|
||
objectNo: objectNo,
|
||
copyFlg: '0',
|
||
}
|
||
|
||
const res = await promisePost({ url: '/api/object/add-plan', data: planData })
|
||
let newPlan = {
|
||
id: res.data.canvasId,
|
||
objectNo: objectNo,
|
||
planNo: res.data.planNo,
|
||
userId: userId,
|
||
canvasStatus: '',
|
||
isCurrent: true,
|
||
bgImageName: null,
|
||
mapPositionAddress: null,
|
||
}
|
||
|
||
if (isInitPlan) {
|
||
/* 초기 플랜 생성인 경우 플랜 목록 초기화 */
|
||
setCurrentCanvasPlan(newPlan)
|
||
setPlans([newPlan])
|
||
|
||
/* 플랜 추가 시 배치면초기설정 정보 조회 */
|
||
fetchBasicSettings(newPlan.planNo, null, objectNo)
|
||
} else {
|
||
if (isCopy) {
|
||
const currentSelectedMenu = selectedMenu
|
||
/* 복제 플랜 생성인 경우 현재 캔버스 데이터를 복제 */
|
||
newPlan.canvasStatus = currentCanvasData()
|
||
newPlan.bgImageName = currentCanvasPlan?.bgImageName ?? null
|
||
newPlan.mapPositionAddress = currentCanvasPlan?.mapPositionAddress ?? null
|
||
|
||
/* 복제 시 배치면 초기설정 복사 */
|
||
await basicSettingCopySave({
|
||
...canvasSetting,
|
||
planNo: newPlan.planNo,
|
||
selectedRoofMaterial: {
|
||
...canvasSetting.selectedRoofMaterial,
|
||
planNo: newPlan.planNo,
|
||
},
|
||
roofsData: canvasSetting.roofsData.map((roof) => ({
|
||
...roof,
|
||
planNo: newPlan.planNo,
|
||
})),
|
||
})
|
||
|
||
/**
|
||
* 방위 데이터 복사
|
||
*/
|
||
const sourceDegree = await getModuleSelection(1)
|
||
console.log('🚀 ~ sourceDegree:', sourceDegree)
|
||
const degreeData = {
|
||
objectNo,
|
||
planNo: parseInt(newPlan.planNo),
|
||
popupType: 1,
|
||
popupStatus: unescapeString(sourceDegree.popupStatus),
|
||
}
|
||
console.log('🚀 ~ postObjectPlan ~ degreeData:', degreeData)
|
||
await post({ url: `/api/v1/canvas-popup-status`, data: degreeData })
|
||
/** 리코일 세팅 */
|
||
setCompasDeg(sourceDegree.popupStatus)
|
||
|
||
/**
|
||
* 모듈 선택 데이터 복사
|
||
*/
|
||
const moduleSelectionData = await getModuleSelection(2)
|
||
console.log('🚀 ~ moduleSelectionData:', moduleSelectionData)
|
||
const moduleStatus = moduleSelectionData.popupStatus.replace(/"/g, '\"')
|
||
const moduleData = {
|
||
objectNo,
|
||
planNo: parseInt(newPlan.planNo),
|
||
popupType: 2,
|
||
popupStatus: moduleStatus,
|
||
}
|
||
console.log('🚀 ~ postObjectPlan ~ moduleData:', moduleData)
|
||
await post({ url: `/api/v1/canvas-popup-status`, data: moduleData })
|
||
const copyData = JSON.parse(moduleStatus)
|
||
console.log('🚀 ~ postObjectPlan ~ copyData:', copyData)
|
||
|
||
/** 리코일 세팅 */
|
||
setModuleSelectionDataStore(copyData)
|
||
if (copyData.module) setSelectedModules(copyData.module)
|
||
setSelectedMenu(currentSelectedMenu)
|
||
|
||
//이미지 복사
|
||
handleEstimateImageCopy(planData.objectNo, planData.planNo, planData.objectNo, newPlan.planNo)
|
||
} else {
|
||
setSelectedMenu('placement')
|
||
}
|
||
setCurrentCanvasPlan(newPlan)
|
||
setPlans((plans) => [...plans.map((plan) => ({ ...plan, isCurrent: false })), newPlan])
|
||
|
||
swalFire({ text: getMessage('plan.message.save') })
|
||
}
|
||
}
|
||
|
||
/**
|
||
* id에 해당하는 canvas 데이터를 수정
|
||
*
|
||
* @param {string} canvasStatus - 캔버스 데이터
|
||
* @param {boolean} saveAlert - 저장 완료 알림 표시 여부
|
||
*/
|
||
const putCanvasStatus = async (canvasStatus, saveAlert = true) => {
|
||
let rtn = false
|
||
const planData = {
|
||
id: currentCanvasPlan.id,
|
||
bgImageName: currentCanvasPlan?.bgImageName ?? null,
|
||
mapPositionAddress: currentCanvasPlan?.mapPositionAddress ?? null,
|
||
canvasStatus: canvasToDbFormat(canvasStatus),
|
||
}
|
||
|
||
// 직전 in-flight 가 살아있다면 취소. abort 된 호출은 .catch 에서 silent return.
|
||
if (canvasStatusInflight.controller) {
|
||
canvasStatusInflight.controller.abort()
|
||
}
|
||
const controller = new AbortController()
|
||
canvasStatusInflight.controller = controller
|
||
|
||
await promisePut({
|
||
url: '/api/canvas-management/canvas-statuses',
|
||
data: planData,
|
||
option: { signal: controller.signal },
|
||
})
|
||
.then((res) => {
|
||
setPlans((plans) =>
|
||
plans.map((plan) =>
|
||
plan.id === currentCanvasPlan.id
|
||
? {
|
||
...plan,
|
||
canvasStatus: canvasStatus,
|
||
}
|
||
: plan,
|
||
),
|
||
)
|
||
if (saveAlert) swalFire({ text: getMessage('plan.message.save') })
|
||
rtn = true
|
||
})
|
||
.catch((error) => {
|
||
// AbortController 로 인한 취소는 정상 흐름 — 새 호출이 진행 중이므로 UX 메시지/로딩 해제 안 함
|
||
if (error?.name === 'CanceledError' || error?.code === 'ERR_CANCELED' || error?.name === 'AbortError') return
|
||
swalFire({ text: error.message, icon: 'error' })
|
||
setIsGlobalLoading(false)
|
||
})
|
||
.finally(() => {
|
||
if (canvasStatusInflight.controller === controller) {
|
||
canvasStatusInflight.controller = null
|
||
}
|
||
})
|
||
return rtn
|
||
}
|
||
|
||
/**
|
||
* id에 해당하는 canvas 데이터를 삭제
|
||
*
|
||
* @param {string} id - canvas ID
|
||
* @returns {Promise<boolean>} 결과
|
||
*/
|
||
const delCanvasById = async (id) => {
|
||
return await promiseDel({ url: `/api/canvas-management/canvas-statuses/by-id/${id}` })
|
||
}
|
||
|
||
/**
|
||
* objectNo에 해당하는 canvas 데이터들을 삭제
|
||
*
|
||
* @param {string} objectNo - 물건번호
|
||
* @returns {Promise<boolean>} 결과
|
||
*/
|
||
const delCanvasByObjectNo = async (objectNo) => {
|
||
return await promiseDel({ url: `/api/canvas-management/canvas-statuses/by-object/${objectNo}` })
|
||
}
|
||
|
||
/**
|
||
* plan 이동
|
||
* 현재 plan의 작업상태를 저장 후 이동
|
||
*
|
||
* @param {string} newCurrentId - 이동할 plan ID
|
||
*/
|
||
const handleCurrentPlan = async (newCurrentId) => {
|
||
const planNo = plans?.find((obj) => obj.id === newCurrentId).planNo
|
||
const objectNo = floorPlanState.objectNo
|
||
|
||
//견적서 or 발전시뮬
|
||
if (pathname !== '/floor-plan') {
|
||
await promiseGet({ url: `/api/estimate/${objectNo}/${planNo}/detail` })
|
||
.then((res) => {
|
||
if (res.status === 200) {
|
||
const estimateDetail = res.data
|
||
if (pathname === '/floor-plan/estimate/5') {
|
||
// 견적서 이동 조건 수정
|
||
// if (estimateDetail.tempFlg === '0' && estimateDetail.estimateDate !== null) {
|
||
if (estimateDetail.estimateDate !== null) {
|
||
res.data.resetFlag = 'N'
|
||
|
||
if (res.data.itemList.length > 0) {
|
||
res.data.itemList.map((item) => {
|
||
item.delFlg = '0'
|
||
})
|
||
}
|
||
if (res.data.pkgAsp === null || res.data.pkgAsp == undefined) {
|
||
res.data.pkgAsp = '0.00'
|
||
} else {
|
||
const number = parseFloat(res.data.pkgAsp)
|
||
const roundedNumber = isNaN(number) ? '0.00' : number.toFixed(2)
|
||
|
||
res.data.pkgAsp = roundedNumber.toString()
|
||
}
|
||
setEstimateContextState(res.data)
|
||
// 클릭한 플랜 탭으로 이동
|
||
setCurrentCanvasPlan(plans.find((plan) => plan.id === newCurrentId))
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, isCurrent: plan.id === newCurrentId })))
|
||
} else {
|
||
swalFire({ text: getMessage('estimate.menu.move.valid1') })
|
||
}
|
||
} else {
|
||
// 발전시뮬레이션
|
||
if (estimateDetail.estimateDate !== null && estimateDetail.docNo) {
|
||
setCurrentCanvasPlan(plans.find((plan) => plan.id === newCurrentId))
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, isCurrent: plan.id === newCurrentId })))
|
||
} else {
|
||
swalFire({ text: getMessage('simulator.menu.move.valid1') })
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
if (pathname === '/floor-plan/estimate/5') {
|
||
swalFire({ text: getMessage('estimate.menu.move.valid1') })
|
||
} else {
|
||
swalFire({ text: getMessage('simulator.menu.move.valid1') })
|
||
}
|
||
})
|
||
} else {
|
||
swalFire({
|
||
text: getMessage('plan.message.confirm.save'),
|
||
type: 'confirm',
|
||
confirmFn: async () => {
|
||
//저장 전에 플랜이 이동되어 state가 변경되는 이슈가 있음
|
||
await saveCanvas(true)
|
||
clearRecoilState()
|
||
setCurrentCanvasPlan(plans.find((plan) => plan.id === newCurrentId))
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, isCurrent: plan.id === newCurrentId })))
|
||
},
|
||
denyFn: async () => {
|
||
setCurrentCanvasPlan(plans.find((plan) => plan.id === newCurrentId))
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, isCurrent: plan.id === newCurrentId })))
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
// usePlan() 인스턴스가 새로 mount 될 때 clearStacks 가 실수로 호출되지 않도록
|
||
// 직전 플랜 id 를 ref 로 보관하고, 실제 id 변경이 일어났을 때만 스택을 비운다.
|
||
const prevPlanIdRef = useRef(currentCanvasPlan?.id)
|
||
useEffect(() => {
|
||
setSelectedPlan(currentCanvasPlan)
|
||
handleCurrentPlanUrl()
|
||
// resetCurrentObject()
|
||
resetModuleSetupSurface()
|
||
// 플랜 ID가 실제로 바뀐 경우에만 undo/redo 히스토리를 초기화한다.
|
||
if (prevPlanIdRef.current !== currentCanvasPlan?.id) {
|
||
clearStacks()
|
||
prevPlanIdRef.current = currentCanvasPlan?.id
|
||
}
|
||
}, [currentCanvasPlan])
|
||
|
||
/**
|
||
* clear가 필요한 recoil state 관리
|
||
*/
|
||
const clearRecoilState = () => {
|
||
resetOuterLinePoints()
|
||
resetPlacementShapeDrawingPoints()
|
||
}
|
||
|
||
/**
|
||
* 현재 plan의 정보를 URL에 추가
|
||
*/
|
||
const handleCurrentPlanUrl = () => {
|
||
if (currentCanvasPlan?.planNo && currentCanvasPlan?.objectNo)
|
||
router.push(`${pathname}?pid=${currentCanvasPlan?.planNo}&objectNo=${currentCanvasPlan?.objectNo}`)
|
||
}
|
||
|
||
/**
|
||
* 새로운 plan 생성
|
||
* 현재 plan의 데이터가 있을 경우 현재 plan 저장 및 복제 여부를 확인
|
||
*
|
||
* @param {string} userId - 사용자 ID
|
||
* @param {string} objectNo - 물건번호
|
||
*/
|
||
const handleAddPlan = async (userId, objectNo) => {
|
||
let isSelected = false
|
||
|
||
if (currentCanvasPlan?.id) {
|
||
swalFire({
|
||
text: getMessage('plan.message.confirm.save'),
|
||
type: 'confirm',
|
||
confirmFn: async () => {
|
||
//저장 전에 플랜이 이동되어 state가 변경되는 이슈가 있음
|
||
await saveCanvas(true)
|
||
handleAddPlanCopyConfirm(userId, objectNo)
|
||
},
|
||
denyFn: async () => {
|
||
handleAddPlanCopyConfirm(userId, objectNo)
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleAddPlanCopyConfirm = async (userId, objectNo) => {
|
||
if (JSON.parse(currentCanvasData()).objects.length > 0) {
|
||
swalFire({
|
||
text: `Plan ${currentCanvasPlan.planNo} ` + getMessage('plan.message.confirm.copy'),
|
||
type: 'confirm',
|
||
confirmFn: async () => {
|
||
setIsGlobalLoading(true)
|
||
await postObjectPlan(userId, objectNo, true, false)
|
||
setIsGlobalLoading(false)
|
||
},
|
||
denyFn: async () => {
|
||
setIsGlobalLoading(true)
|
||
await postObjectPlan(userId, objectNo, false, false)
|
||
setIsGlobalLoading(false)
|
||
},
|
||
})
|
||
} else {
|
||
setIsGlobalLoading(true)
|
||
await postObjectPlan(userId, objectNo, false, false)
|
||
setIsGlobalLoading(false)
|
||
}
|
||
}
|
||
/**
|
||
* 물건번호(object) plan 삭제 (canvas 삭제 전 planNo 삭제)
|
||
*
|
||
* @param {string} userId - 사용자 ID
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {string} planNo - 플랜번호
|
||
* @returns {Promise<boolean>} 결과
|
||
*/
|
||
const deleteObjectPlan = async (userId, objectNo, planNo) => {
|
||
return await promiseDel({ url: `/api/object/plan/${objectNo}/${planNo}?userId=${userId}` })
|
||
.then((res) => {
|
||
return true
|
||
})
|
||
.catch((error) => {
|
||
swalFire({ text: error.response.data.message, icon: 'error' })
|
||
return false
|
||
})
|
||
}
|
||
|
||
/**
|
||
* plan 삭제
|
||
*
|
||
* @param {Event} e - 이벤트
|
||
* @param {Object} targetPlan - 삭제할 plan
|
||
*/
|
||
const handleDeletePlan = async (e, targetPlan) => {
|
||
e.stopPropagation() // 이벤트 버블링 방지
|
||
|
||
const isSuccess = await deleteObjectPlan(targetPlan.id, targetPlan.objectNo, targetPlan.planNo)
|
||
if (!isSuccess) return
|
||
|
||
await delCanvasById(targetPlan.id)
|
||
.then((res) => {
|
||
setPlans((plans) => plans.filter((plan) => plan.id !== targetPlan.id))
|
||
removeImage(currentCanvasPlan.id)
|
||
|
||
/* 플랜 삭제 후 배치면 초기설정 삭제 */
|
||
deleteBasicSettings(targetPlan.objectNo, targetPlan.planNo)
|
||
|
||
swalFire({ text: getMessage('plan.message.delete') })
|
||
})
|
||
.catch((error) => {
|
||
swalFire({ text: error.message, icon: 'error' })
|
||
})
|
||
|
||
/* 삭제 후 last 데이터에 포커싱 */
|
||
const lastPlan = plans.filter((plan) => plan.id !== targetPlan.id).at(-1)
|
||
if (!lastPlan) {
|
||
setCurrentCanvasPlan(null)
|
||
} else if (targetPlan.id !== lastPlan.id) {
|
||
setCurrentCanvasPlan(lastPlan)
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, isCurrent: plan.id === lastPlan.id })))
|
||
|
||
/* 플랜 삭제 시 그 전 플랫의 배치면초기설정 정보 조회 */
|
||
fetchBasicSettings(lastPlan.planNo, null, lastPlan.objectNo)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* plan 조회
|
||
*
|
||
* @param {string} userId - 사용자 ID
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {string} planNo - 플랜번호
|
||
*/
|
||
const loadCanvasPlanData = async (userId, objectNo, planNo) => {
|
||
console.log('🚀 ~ loadCanvasPlanData ~ userId, objectNo, planNo:', userId, objectNo, planNo)
|
||
await getCanvasByObjectNo(objectNo, planNo).then(async (res) => {
|
||
if (res.length > 0) {
|
||
setCurrentCanvasPlan(res.find((plan) => plan.planNo === planNo))
|
||
setPlans(res)
|
||
} else {
|
||
await postObjectPlan(userId, objectNo, false, true)
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* plan canvasStatus 초기화
|
||
*/
|
||
const resetCanvasStatus = () => {
|
||
setCurrentCanvasPlan((prev) => ({ ...prev, canvasStatus: null, objectNo: null, planNo: null, id: null }))
|
||
setPlans((plans) => plans.map((plan) => ({ ...plan, canvasStatus: null })))
|
||
setCurrentMenu(null)
|
||
setSelectedMenu(null)
|
||
}
|
||
|
||
/**
|
||
* plan canvasStatus 불러오기
|
||
* 견적서/발전시뮬레이션에서 플랜 이동 시 현재 플랜의 canvasStatus를 불러오기 위해 사용
|
||
*
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {string} planNo - 플랜번호
|
||
*/
|
||
const reloadCanvasStatus = async (objectNo, planNo) => {
|
||
if (pathname === '/floor-plan/estimate/5' || pathname === '/floor-plan/simulator/6') {
|
||
await getCanvasByObjectNo(objectNo, planNo).then((res) => {
|
||
if (res.length > 0) {
|
||
// setCurrentCanvasPlan((prev) => ({ ...prev, canvasStatus: res.find((plan) => plan.planNo === planNo).canvasStatus }))
|
||
// setPlans((plans) => plans.map((plan) => ({ ...plan, canvasStatus: res.find((resPlan) => resPlan.planNo === plan.planNo).canvasStatus })))
|
||
setCurrentCanvasPlan(res.find((plan) => plan.planNo === planNo))
|
||
setPlans(res)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 플랜 삭제 시 배치면 초기설정 데이터 삭제
|
||
*
|
||
* @param {string} objectNo - 물건번호
|
||
* @param {string} planNo - 플랜번호
|
||
*/
|
||
const deleteBasicSettings = async (objectNo, planNo) => {
|
||
try {
|
||
await promiseDel({ url: `/api/canvas-management/canvas-basic-settings/delete-basic-settings/${objectNo}/${planNo}` })
|
||
} catch (error) {
|
||
/* 오류를 무시하고 계속 진행 */
|
||
console.log('Basic settings delete failed or not found:', error)
|
||
// swalFire({ text: error.message, icon: 'error' })
|
||
}
|
||
}
|
||
|
||
return {
|
||
canvas,
|
||
plans,
|
||
currentCanvasPlan,
|
||
setCurrentCanvasPlan,
|
||
selectedPlan,
|
||
saveCanvas,
|
||
handleCurrentPlan,
|
||
handleAddPlan,
|
||
handleDeletePlan,
|
||
loadCanvasPlanData,
|
||
resetCanvasStatus,
|
||
reloadCanvasStatus,
|
||
}
|
||
}
|