184 lines
6.6 KiB
JavaScript
184 lines
6.6 KiB
JavaScript
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<Object>} 서버 응답 (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 }
|
|
}
|