import { useRecoilValue } from 'recoil' import { canvasState } from '@/store/canvasAtom' import { useAxios } from '../useAxios' import { usePlan } from '../usePlan' import { POLYGON_TYPE } from '@/common/common' import { QcastContext } from '@/app/QcastProvider' import { useContext } from 'react' import Config from '@/config/config.export' /** * ====================================================================== * 이미지 로더 Hook * ====================================================================== * * [개요] * Fabric.js 캔버스에서 도면 영역만 PNG로 캡처하여 서버(/api/image/canvas)로 전송한다. * 서버에서 2벌(풀사이즈/셀범위)로 가공 후 S3에 업로드한다. * * [캡처 방식] * - getBoundingRect()로 도면 오브젝트(지붕, 치수, 화살표)의 정확한 바운딩 박스 계산 * - canvas.toDataURL()에 크롭 좌표를 전달하여 도면 영역만 직접 캡처 * - 배경을 흰색으로 설정하여 서버 autocrop이 정상 동작하도록 함 * - multiplier: 2로 고해상도(2배) 캡처 * * @returns {function} handleCanvasToPng - 캔버스를 PNG로 캡처하여 서버에 전송 */ export function useImgLoader() { const canvas = useRecoilValue(canvasState) const { currentCanvasPlan } = usePlan() const { post } = useAxios() const { setIsGlobalLoading } = useContext(QcastContext) /** * 도면 오브젝트의 바운딩 박스를 계산한다. * 지붕(ROOF), 치수 텍스트(lengthText), 방위 화살표(arrow)를 대상으로 한다. * getBoundingRect()를 사용하여 스케일/회전이 적용된 정확한 좌표를 구한다. * * @returns {Array<{x: number, y: number}>} [좌상단 좌표, 우하단 좌표] (margin 포함) */ const getImageCoordinates = () => { const margin = 20 const objects = canvas.getObjects().filter((obj) => [POLYGON_TYPE.ROOF, 'lengthText', 'arrow'].includes(obj.name)) let minX = Infinity let minY = Infinity let maxX = -Infinity let maxY = -Infinity objects.forEach((obj) => { const rect = obj.getBoundingRect() minX = Math.min(minX, rect.left) minY = Math.min(minY, rect.top) maxX = Math.max(maxX, rect.left + rect.width) maxY = Math.max(maxY, rect.top + rect.height) }) return [ { x: minX - margin, y: minY - margin }, { x: maxX + margin, y: maxY + margin }, ] } /** * 캔버스를 PNG 이미지로 캡처하여 서버에 전송한다. * * [처리 순서] * 1. 그리드/흡착점 등 불필요한 요소 숨김 * 2. 배경을 흰색으로 변경 (격자 패턴 제거 → autocrop 가능하도록) * 3. 도면 오브젝트의 바운딩 박스 계산 * 4. 해당 영역만 2배 해상도로 캡처 (toDataURL) * 5. FormData로 서버 API에 전송 * 6. 배경/그리드 원복 * * @param {number} type - 1: 모듈만 있는 상태, 2: 가대까지 올린 상태 * @returns {Promise} 서버 응답 (filePath, fullFilePath, fileName, fullFileName) */ const handleCanvasToPng = async (type) => { try { // 1. 그리드, 흡착점, 마우스라인 등 제거/숨김 toggleLineEtc(false) // 2. 배경을 흰색으로 설정 (격자 패턴이 autocrop을 방해하므로) const originalBg = canvas.backgroundColor canvas.backgroundColor = '#ffffff' // CORS 대응: 이미지 오브젝트에 crossOrigin 설정 canvas.getObjects('image').forEach((obj) => { if (obj.getSrc) { const img = new Image() img.crossOrigin = 'anonymous' img.src = obj.getSrc() obj.setElement(img) } }) canvas.renderAll() const formData = new FormData() // 3. 도면 오브젝트의 바운딩 박스 계산 const positionObj = getImageCoordinates() console.log('🚀 ~ handleCanvasToPng ~ positionObj:', positionObj) const cropLeft = positionObj[0].x const cropTop = positionObj[0].y const cropWidth = positionObj[1].x - positionObj[0].x const cropHeight = positionObj[1].y - positionObj[0].y // 4. 도면 영역만 2배 해상도로 캡처 // toDataURL에 left/top/width/height를 전달하면 해당 영역만 캡처됨 const multiplier = 2 const dataUrl = canvas.toDataURL({ format: 'png', multiplier: multiplier, left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight, }) // base64 → Blob 변환 const blobBin = atob(dataUrl.split(',')[1]) const array = [] for (let i = 0; i < blobBin.length; i++) { array.push(blobBin.charCodeAt(i)) } const file = new Blob([new Uint8Array(array)], { type: 'image/png' }) // 5. FormData 구성 및 서버 전송 formData.append('file', file, 'canvas.png') formData.append('objectNo', currentCanvasPlan.objectNo) formData.append('planNo', currentCanvasPlan.planNo) formData.append('type', type) formData.append('width', Math.round(cropWidth * multiplier)) formData.append('height', Math.round(cropHeight * multiplier)) formData.append('left', 0) // 프론트에서 이미 크롭하여 전송하므로 0 formData.append('top', 0) console.log('🚀 ~ handleCanvasToPng ~ formData:', formData) const result = await post({ url: `${Config().baseUrl}/api/image/canvas`, data: formData, }) console.log('🚀 ~ handleCanvasToPng ~ result:', result) // 6. 배경/그리드 원복 canvas.backgroundColor = originalBg canvas.renderAll() toggleLineEtc(true) return result } catch (e) { canvas.backgroundColor = originalBg canvas.renderAll() setIsGlobalLoading(false) console.log('🚀 ~ handleCanvasToPng ~ e:', e) } } /** * 캡처 시 불필요한 요소(마우스라인, 그리드, 흡착점)를 숨기거나 복원한다. * * @param {boolean} visible - true: 복원, false: 숨김 */ const toggleLineEtc = (visible = false) => { if (canvas?._objects.length > 0) { const mouseLines = canvas?._objects.filter((obj) => obj.name === 'mouseLine') mouseLines.forEach((item) => canvas?.remove(item)) } const adsorptionPoints = canvas?._objects.filter((obj) => obj.name === 'adsorptionPoint') const gridLines = canvas?._objects.filter((obj) => obj.name === 'lineGrid' || obj.name === 'tempGrid' || obj.name === 'dotGrid') adsorptionPoints.forEach((item) => item.set({ visible: visible })) gridLines.forEach((item) => item.set({ visible: visible })) canvas?.renderAll() } return { handleCanvasToPng } }