'use client' import { useContext, useEffect, useState } from 'react' import { usePathname, useRouter } from 'next/navigation' import { useRecoilState, useResetRecoilState } from 'recoil' import { canvasState, currentCanvasPlanState, plansState, canvasSettingState, currentObjectState, moduleSetupSurfaceState } 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' /** * 플랜 처리 훅 * 플랜을 표시하는 탭 UI 전반적인 처리 로직 관리 * @param {*} params * @returns */ export function usePlan(params = {}) { const { floorPlanState } = useContext(FloorPlanContext) const [selectedPlan, setSelectedPlan] = useState(null) 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 } = 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 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') await putCanvasStatus(canvasStatus, saveAlert) } /** * objectNo에 해당하는 canvas 목록을 조회 * * @param {string} objectNo - 물건번호 * @param {string} planNo - 플랜번호 * @returns {Promise} 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(/"/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) => { 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') }) }) .catch((error) => { swalFire({ text: error.message, icon: 'error' }) }) } /** * id에 해당하는 canvas 데이터를 삭제 * * @param {string} id - canvas ID * @returns {Promise} 결과 */ const delCanvasById = async (id) => { return await promiseDel({ url: `/api/canvas-management/canvas-statuses/by-id/${id}` }) } /** * objectNo에 해당하는 canvas 데이터들을 삭제 * * @param {string} objectNo - 물건번호 * @returns {Promise} 결과 */ 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 { if (!currentCanvasPlan || currentCanvasPlan.id !== newCurrentId) { await saveCanvas(true) clearRecoilState() } 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) => { if (currentCanvasPlan?.id) { await saveCanvas(false) } 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} 결과 */ 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 })) setPlans((plans) => plans.map((plan) => ({ ...plan, canvasStatus: 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 }))) } }) } } /** * 플랜 삭제 시 배치면 초기설정 데이터 삭제 * * @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, } }