qcast-front/src/hooks/usePlan.js
yjnoh 8b81998f8c [1068] : [견적서 생성된 플랜복사 --> 복사된 플랜에서 모듈삭제 --> 견적서 진입이 됨.. ]
[작업내용] : 가대까지 완성 -> 삭제 안함, 그 전상태면 무조건 삭제 로직으로 구현
2025-05-30 17:56:39 +09:00

656 lines
22 KiB
JavaScript

'use client'
import { useContext, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil'
import {
canvasState,
currentCanvasPlanState,
plansState,
canvasSettingState,
currentObjectState,
moduleSetupSurfaceState,
currentMenuState,
} 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'
/**
* 플랜 처리 훅
* 플랜을 표시하는 탭 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 } = 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()
/**
* 마우스 포인터의 가이드라인 제거
*/
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) => {
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)
} else {
if (isCopy) {
const currentSelectedMenu = selectedMenu
/* 복제 플랜 생성인 경우 현재 캔버스 데이터를 복제 */
newPlan.canvasStatus = currentCanvasData()
newPlan.bgImageName = currentCanvasPlan?.bgImageName ?? null
newPlan.mapPositionAddress = currentCanvasPlan?.mapPositionAddress ?? null
/* 복제 시 배치면 초기설정 복사 */
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(/&quot;/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)
} 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),
}
await promisePut({ url: '/api/canvas-management/canvas-statuses', data: planData })
.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) => {
swalFire({ text: error.message, icon: 'error' })
})
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 })))
},
})
}
}
useEffect(() => {
setSelectedPlan(currentCanvasPlan)
handleCurrentPlanUrl()
// resetCurrentObject()
resetModuleSetupSurface()
}, [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)
}
}
/**
* 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,
}
}