Compare commits
No commits in common. "8afff34ff0d8b44de84152eda39858bb957defa4" and "a71f56cc1228c797d93926ff18253aaf99cbc130" have entirely different histories.
8afff34ff0
...
a71f56cc12
@ -3,37 +3,6 @@ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } fro
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { Jimp } from 'jimp'
|
import { Jimp } from 'jimp'
|
||||||
|
|
||||||
/**
|
|
||||||
* ======================================================================
|
|
||||||
* 캔버스 도면 이미지 처리 API
|
|
||||||
* ======================================================================
|
|
||||||
*
|
|
||||||
* [개요]
|
|
||||||
* Fabric.js 캔버스에서 캡처한 도면 이미지를 2벌로 가공하여 S3에 업로드한다.
|
|
||||||
*
|
|
||||||
* [생성되는 이미지 2벌]
|
|
||||||
* 1. 셀범위용 (기존 이미지명 유지)
|
|
||||||
* - 파일명: {objectNo}_{planNo}_{type}.png
|
|
||||||
* - 용도: 割付図・系統図, 架台図 시트 (jx:image lastCell 셀범위에 맞춤)
|
|
||||||
* - 비율: 엑셀 셀 영역 비율 35.4:12.89 (~2.75:1) 에 맞춰 흰 여백 추가
|
|
||||||
* - 템플릿: data.drawingImg1, data.drawingImg2
|
|
||||||
*
|
|
||||||
* 2. 풀사이즈용 (신규)
|
|
||||||
* - 파일명: {objectNo}_{planNo}_{type}_full.png
|
|
||||||
* - 용도: P1, P2 시트 (A4 가로 전체 페이지에 이미지만 출력)
|
|
||||||
* - 비율: A4 가로 비율 27.7:19.0 (~1.46:1) 에 맞춰 흰 여백 최소화
|
|
||||||
* - 템플릿: data.drawingImg1Full, data.drawingImg2Full
|
|
||||||
*
|
|
||||||
* [처리 흐름]
|
|
||||||
* 프론트(useImgLoader) → 도면 영역만 캡처하여 PNG 전송
|
|
||||||
* → S3에 원본 임시 저장 (uuid 키)
|
|
||||||
* → autocrop으로 흰 여백 제거
|
|
||||||
* → 풀사이즈/셀범위 2벌 리사이즈
|
|
||||||
* → S3에 2벌 업로드
|
|
||||||
* → 원본 삭제
|
|
||||||
* ======================================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
const Bucket = process.env.AMPLIFY_BUCKET
|
const Bucket = process.env.AMPLIFY_BUCKET
|
||||||
const s3 = new S3Client({
|
const s3 = new S3Client({
|
||||||
region: process.env.AWS_REGION,
|
region: process.env.AWS_REGION,
|
||||||
@ -43,140 +12,113 @@ const s3 = new S3Client({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
const checkArea = (obj) => {
|
||||||
* S3에서 이미지를 읽어 autocrop된 Jimp 이미지를 반환한다.
|
const { width, height, left, top } = obj
|
||||||
* 프론트에서 배경을 흰색으로 설정하여 캡처하므로 autocrop이 정상 동작한다.
|
|
||||||
*
|
if (left < 0 || top < 0 || width > 1600 || height > 1000) {
|
||||||
* @param {string} Key - S3 오브젝트 키
|
return false
|
||||||
* @returns {Promise<Jimp>} autocrop된 Jimp 이미지 객체
|
}
|
||||||
*/
|
|
||||||
const loadAndCrop = async (Key) => {
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const cropImage = async (Key, width, height, left, top) => {
|
||||||
|
try {
|
||||||
|
// Get the image from S3
|
||||||
const { Body } = await s3.send(
|
const { Body } = await s3.send(
|
||||||
new GetObjectCommand({ Bucket, Key }),
|
new GetObjectCommand({
|
||||||
|
Bucket,
|
||||||
|
Key,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const chunks = []
|
const chunks = []
|
||||||
for await (const chunk of Body) {
|
for await (const chunk of Body) {
|
||||||
chunks.push(chunk)
|
chunks.push(chunk)
|
||||||
}
|
}
|
||||||
const buffer = Buffer.concat(chunks)
|
const buffer = Buffer.concat(chunks)
|
||||||
const image = await Jimp.read(buffer)
|
|
||||||
|
|
||||||
// tolerance: 배경색과 5% 차이까지 여백으로 인식
|
let image = await Jimp.read(buffer)
|
||||||
// leaveBorder: 크롭 후 5px 테두리 여유
|
image.autocrop({ tolerance: 0.0002, leaveBorder: 10 })
|
||||||
image.autocrop({ tolerance: 0.05, leaveBorder: 5 })
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
const resizedImage = await resizeImage(image).then((result) => {
|
||||||
* 풀사이즈용 리사이즈 (P1, P2 시트)
|
return result
|
||||||
*
|
})
|
||||||
* A4 가로(297mm × 210mm) 인쇄 영역에 맞춰 이미지를 리사이즈한다.
|
|
||||||
* - 가로 도면: A4에 거의 꽉 참
|
|
||||||
* - 세로 도면: 상하가 꽉 차고 좌우에 최소 여백
|
|
||||||
*
|
|
||||||
* 엑셀 템플릿 설정: jx:image(lastCell="V47" src="data.drawingImg1Full" imageType="PNG")
|
|
||||||
*
|
|
||||||
* @param {Jimp} image - autocrop된 Jimp 이미지 객체
|
|
||||||
* @returns {Promise<Jimp>} A4 비율에 맞춘 이미지
|
|
||||||
*/
|
|
||||||
const resizeForFull = async (image) => {
|
|
||||||
const DPI = 200
|
|
||||||
|
|
||||||
// A4 가로 인쇄 영역 (여백 제외): 약 277mm × 190mm
|
return await resizedImage.getBuffer('image/png')
|
||||||
const maxWidth = Math.round((27.7 * DPI) / 2.54) // ~2181px
|
|
||||||
const maxHeight = Math.round((19.0 * DPI) / 2.54) // ~1496px
|
|
||||||
|
|
||||||
// 이미지를 인쇄 영역의 95%까지 채움 (약간의 여유)
|
// Convert stream to buffer
|
||||||
const targetW = maxWidth * 0.95
|
// const chunks = []
|
||||||
const targetH = maxHeight * 0.95
|
// for await (const chunk of Body) {
|
||||||
const scaleX = targetW / image.bitmap.width
|
// chunks.push(chunk)
|
||||||
const scaleY = targetH / image.bitmap.height
|
// }
|
||||||
const scale = Math.min(scaleX, scaleY) // 비율 유지하며 최대 크기
|
// const imageBuffer = Buffer.concat(chunks)
|
||||||
|
|
||||||
image.resize({ w: Math.round(image.bitmap.width * scale), h: Math.round(image.bitmap.height * scale) })
|
// const image = await Jimp.read(Body)
|
||||||
|
|
||||||
// A4 비율의 흰 배경 캔버스에 이미지를 중앙 배치
|
// if (!checkResult) {
|
||||||
const background = new Jimp({ width: maxWidth, height: maxHeight, color: 0xffffffff })
|
// processedImage = await image.toBuffer()
|
||||||
const x = Math.floor((maxWidth - image.bitmap.width) / 2)
|
// }
|
||||||
const y = Math.floor((maxHeight - image.bitmap.height) / 2)
|
|
||||||
background.composite(image, x, y)
|
|
||||||
|
|
||||||
return background
|
//let processedImage
|
||||||
}
|
// if (!checkResult) {
|
||||||
|
// processedImage = await sharp(imageBuffer).toBuffer()
|
||||||
/**
|
// } else {
|
||||||
* 셀범위용 리사이즈 (割付図・系統図, 架台図 시트)
|
// processedImage = await sharp(imageBuffer)
|
||||||
*
|
// .extract({
|
||||||
* 엑셀 템플릿의 이미지 셀 영역(35.4cm × 12.89cm) 비율에 맞춰 리사이즈한다.
|
// width: parseInt(width),
|
||||||
* 가로로 넓은 비율(~2.75:1)이므로 세로 도면은 좌우 여백이 많아진다.
|
// height: parseInt(height),
|
||||||
*
|
// left: parseInt(left),
|
||||||
* 엑셀 템플릿 설정: jx:image(lastCell="V38" src="data.drawingImg1" imageType="PNG")
|
// top: parseInt(top),
|
||||||
*
|
// })
|
||||||
* @param {Jimp} image - autocrop된 Jimp 이미지 객체
|
// .png()
|
||||||
* @returns {Promise<Jimp>} 셀 비율에 맞춘 이미지
|
// .toBuffer()
|
||||||
*/
|
// }
|
||||||
const resizeForCell = async (image) => {
|
// return processedImage
|
||||||
const DPI = 200
|
|
||||||
|
|
||||||
// 엑셀 셀 영역 크기: 35.4cm × 12.89cm
|
|
||||||
const maxWidth = Math.round((35.4 * DPI) / 2.54) // ~2787px
|
|
||||||
const maxHeight = Math.round((12.89 * DPI) / 2.54) // ~1015px
|
|
||||||
|
|
||||||
// 이미지를 셀 영역의 98%까지 채움
|
|
||||||
const targetW = maxWidth * 0.98
|
|
||||||
const targetH = maxHeight * 0.98
|
|
||||||
const scaleX = targetW / image.bitmap.width
|
|
||||||
const scaleY = targetH / image.bitmap.height
|
|
||||||
const scale = Math.min(scaleX, scaleY) // 비율 유지하며 최대 크기
|
|
||||||
|
|
||||||
image.resize({ w: Math.round(image.bitmap.width * scale), h: Math.round(image.bitmap.height * scale) })
|
|
||||||
|
|
||||||
// 셀 비율의 흰 배경 캔버스에 이미지를 중앙 배치
|
|
||||||
const background = new Jimp({ width: maxWidth, height: maxHeight, color: 0xffffffff })
|
|
||||||
const x = Math.floor((maxWidth - image.bitmap.width) / 2)
|
|
||||||
const y = Math.floor((maxHeight - image.bitmap.height) / 2)
|
|
||||||
background.composite(image, x, y)
|
|
||||||
|
|
||||||
return background
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 이미지 2벌 생성: 풀사이즈(P1,P2) + 셀범위(割付図 등)
|
|
||||||
*
|
|
||||||
* @param {string} Key - S3에 저장된 원본 이미지 키
|
|
||||||
* @returns {Promise<{fullBuffer: Buffer, cellBuffer: Buffer}>} 2벌의 PNG 버퍼
|
|
||||||
*/
|
|
||||||
const processImage = async (Key) => {
|
|
||||||
try {
|
|
||||||
const image = await loadAndCrop(Key)
|
|
||||||
const imageCopy = image.clone() // 동일 원본에서 2벌 생성을 위해 복제
|
|
||||||
|
|
||||||
const fullBuffer = await resizeForFull(image).then((img) => img.getBuffer('image/png'))
|
|
||||||
const cellBuffer = await resizeForCell(imageCopy).then((img) => img.getBuffer('image/png'))
|
|
||||||
|
|
||||||
return { fullBuffer, cellBuffer }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing image:', error)
|
console.error('Error processing image:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//크롭된 이미지를 배경 크기에 맞게 리사이즈
|
||||||
* POST: 캔버스 도면 이미지 처리 및 S3 업로드
|
const resizeImage = async (image) => {
|
||||||
*
|
//엑셀 템플릿 너비 35.4cm, 높이 12.89cm
|
||||||
* [요청 FormData]
|
const convertStandardWidth = Math.round((35.4 * 96) / 2.54)
|
||||||
* - file: PNG 이미지 (프론트에서 도면 영역만 캡처)
|
const convertStandardHeight = Math.round((12.89 * 96) / 2.54)
|
||||||
* - objectNo: 물건번호
|
|
||||||
* - planNo: 프랜번호
|
// 이미지를 배경의 98%까지 확대 (훨씬 더 크게)
|
||||||
* - type: 1(모듈만) / 2(가대포함)
|
const targetImageWidth = convertStandardWidth * 0.98
|
||||||
* - width, height, left, top: 크롭 좌표 (프론트에서 이미 크롭하여 전송하므로 left=0, top=0)
|
const targetImageHeight = convertStandardHeight * 0.98
|
||||||
*
|
|
||||||
* [응답 JSON]
|
const scaleX = targetImageWidth / image.bitmap.width
|
||||||
* - filePath: 셀범위용 이미지 URL (기존 이미지명)
|
const scaleY = targetImageHeight / image.bitmap.height
|
||||||
* - fullFilePath: 풀사이즈용 이미지 URL (_full 접미사)
|
let scale = Math.min(scaleX, scaleY) // 비율 유지하면서 최대한 크게
|
||||||
* - fileName: 셀범위용 S3 키
|
|
||||||
* - fullFileName: 풀사이즈용 S3 키
|
let finalWidth = Math.round(image.bitmap.width * scale)
|
||||||
*/
|
let finalHeight = Math.round(image.bitmap.height * scale)
|
||||||
|
|
||||||
|
// 항상 리사이즈 실행 (scale >= 0.6 조건 제거)
|
||||||
|
image.resize({ w: finalWidth, h: finalHeight })
|
||||||
|
|
||||||
|
//배경 이미지를 생성
|
||||||
|
const mixedImage = new Jimp({ width: convertStandardWidth, height: convertStandardHeight, color: 0xffffffff })
|
||||||
|
|
||||||
|
//이미지를 중앙에 배치
|
||||||
|
const x = Math.floor((mixedImage.bitmap.width - image.bitmap.width) / 2)
|
||||||
|
const y = Math.floor((mixedImage.bitmap.height - image.bitmap.height) / 2)
|
||||||
|
|
||||||
|
//이미지를 배경 이미지에 합성
|
||||||
|
mixedImage.composite(image, x, y, {
|
||||||
|
opacitySource: 1, // 원본 투명도 유지
|
||||||
|
opacityDest: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 1.5x 확대 로직 제거 - 이미지가 템플릿 크기를 초과하지 않도록 함
|
||||||
|
|
||||||
|
return mixedImage
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(req) {
|
export async function POST(req) {
|
||||||
try {
|
try {
|
||||||
const formData = await req.formData()
|
const formData = await req.formData()
|
||||||
@ -184,12 +126,16 @@ export async function POST(req) {
|
|||||||
const objectNo = formData.get('objectNo')
|
const objectNo = formData.get('objectNo')
|
||||||
const planNo = formData.get('planNo')
|
const planNo = formData.get('planNo')
|
||||||
const type = formData.get('type')
|
const type = formData.get('type')
|
||||||
|
const width = formData.get('width')
|
||||||
|
const height = formData.get('height')
|
||||||
|
const left = formData.get('left')
|
||||||
|
const top = formData.get('top')
|
||||||
|
|
||||||
const OriginalKey = `Drawing/${uuidv4()}`
|
const OriginalKey = `Drawing/${uuidv4()}`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1단계: 원본 이미지를 S3에 임시 저장
|
* 원본 이미지를 우선 저장한다.
|
||||||
* - uuid를 사용하여 이미지 이름 충돌 방지
|
* 이미지 이름이 겹지는 현상을 방지하기 위해 uuid 를 사용한다.
|
||||||
*/
|
*/
|
||||||
await s3.send(
|
await s3.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
@ -201,26 +147,29 @@ export async function POST(req) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2단계: 이미지 2벌 생성
|
* 저장된 원본 이미지를 기준으로 크롭여부를 결정하여 크롭 이미지를 저장한다.
|
||||||
* - fullBuffer: P1, P2 시트용 (A4 가로 비율, 여백 최소화)
|
|
||||||
* - cellBuffer: 割付図・架台図 시트용 (셀범위 비율, 여백 포함)
|
|
||||||
*/
|
*/
|
||||||
const { fullBuffer, cellBuffer } = await processImage(OriginalKey)
|
const bufferImage = await cropImage(OriginalKey, width, height, left, top)
|
||||||
|
|
||||||
// 기존 이미지명 유지 (셀범위용), _full 접미사 추가 (풀사이즈용)
|
|
||||||
const cellKey = `Drawing/${process.env.S3_PROFILE}/${objectNo}_${planNo}_${type}.png`
|
|
||||||
const fullKey = `Drawing/${process.env.S3_PROFILE}/${objectNo}_${planNo}_${type}_full.png`
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3단계: 2벌 이미지를 S3에 병렬 업로드
|
* 크롭 이미지 이름을 결정한다.
|
||||||
*/
|
*/
|
||||||
await Promise.all([
|
const Key = `Drawing/${process.env.S3_PROFILE}/${objectNo}_${planNo}_${type}.png`
|
||||||
s3.send(new PutObjectCommand({ Bucket, Key: fullKey, Body: fullBuffer, ContentType: 'image/png' })),
|
|
||||||
s3.send(new PutObjectCommand({ Bucket, Key: cellKey, Body: cellBuffer, ContentType: 'image/png' })),
|
|
||||||
])
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 4단계: 원본 임시 이미지 삭제
|
* 크롭이 완료된 이미지를 업로드한다.
|
||||||
|
*/
|
||||||
|
await s3.send(
|
||||||
|
new PutObjectCommand({
|
||||||
|
Bucket,
|
||||||
|
Key,
|
||||||
|
Body: bufferImage,
|
||||||
|
ContentType: 'image/png',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 크롭이미지 저장이 완료되면 원본 이미지를 삭제한다.
|
||||||
*/
|
*/
|
||||||
await s3.send(
|
await s3.send(
|
||||||
new DeleteObjectCommand({
|
new DeleteObjectCommand({
|
||||||
@ -230,10 +179,8 @@ export async function POST(req) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${cellKey}`,
|
filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${Key}`,
|
||||||
fullFilePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${fullKey}`,
|
fileName: Key,
|
||||||
fileName: cellKey,
|
|
||||||
fullFileName: fullKey,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(result)
|
return NextResponse.json(result)
|
||||||
|
|||||||
@ -8,53 +8,30 @@ import { useContext } from 'react'
|
|||||||
import Config from '@/config/config.export'
|
import Config from '@/config/config.export'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ======================================================================
|
* 이미지 로더 hook
|
||||||
* 이미지 로더 Hook
|
* 캔버스를 바이너리로 변환하고 이미지 객체에 붙여서 다시 이미지를 바이너리로 전달
|
||||||
* ======================================================================
|
* 캔버스 데이터가 바이너리로 변경되면 용량이 너무 커서 post전송에 실패할 수 있음
|
||||||
*
|
* @returns {function} handleCanvasToPng
|
||||||
* [개요]
|
|
||||||
* Fabric.js 캔버스에서 도면 영역만 PNG로 캡처하여 서버(/api/image/canvas)로 전송한다.
|
|
||||||
* 서버에서 2벌(풀사이즈/셀범위)로 가공 후 S3에 업로드한다.
|
|
||||||
*
|
|
||||||
* [캡처 방식]
|
|
||||||
* - getBoundingRect()로 도면 오브젝트(지붕, 치수, 화살표)의 정확한 바운딩 박스 계산
|
|
||||||
* - canvas.toDataURL()에 크롭 좌표를 전달하여 도면 영역만 직접 캡처
|
|
||||||
* - 배경을 흰색으로 설정하여 서버 autocrop이 정상 동작하도록 함
|
|
||||||
* - multiplier: 2로 고해상도(2배) 캡처
|
|
||||||
*
|
|
||||||
* @returns {function} handleCanvasToPng - 캔버스를 PNG로 캡처하여 서버에 전송
|
|
||||||
*/
|
*/
|
||||||
export function useImgLoader() {
|
export function useImgLoader() {
|
||||||
const canvas = useRecoilValue(canvasState)
|
const canvas = useRecoilValue(canvasState)
|
||||||
const { currentCanvasPlan } = usePlan()
|
const { currentCanvasPlan } = usePlan()
|
||||||
const { post } = useAxios()
|
const { post } = useAxios()
|
||||||
const { setIsGlobalLoading } = useContext(QcastContext)
|
const { setIsGlobalLoading } = useContext(QcastContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 도면 오브젝트의 바운딩 박스를 계산한다.
|
* 이미지 저장 시 왼쪽 위, 오른쪽 아래 좌표
|
||||||
* 지붕(ROOF), 치수 텍스트(lengthText), 방위 화살표(arrow)를 대상으로 한다.
|
* return [start, end]
|
||||||
* getBoundingRect()를 사용하여 스케일/회전이 적용된 정확한 좌표를 구한다.
|
|
||||||
*
|
|
||||||
* @returns {Array<{x: number, y: number}>} [좌상단 좌표, 우하단 좌표] (margin 포함)
|
|
||||||
*/
|
*/
|
||||||
const getImageCoordinates = () => {
|
const getImageCoordinates = () => {
|
||||||
const margin = 20
|
const margin = 20
|
||||||
|
|
||||||
const objects = canvas.getObjects().filter((obj) => [POLYGON_TYPE.ROOF, 'lengthText', 'arrow'].includes(obj.name))
|
const objects = canvas.getObjects().filter((obj) => [POLYGON_TYPE.ROOF, 'lengthText', 'arrow'].includes(obj.name))
|
||||||
|
|
||||||
let minX = Infinity
|
const minX = objects.reduce((acc, cur) => (cur.left < acc ? cur.left : acc), objects[0].left)
|
||||||
let minY = Infinity
|
const minY = objects.reduce((acc, cur) => (cur.top < acc ? cur.top : acc), objects[0].top)
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const maxX = objects.reduce((acc, cur) => (cur.left + cur.width > acc ? cur.left : acc), 0)
|
||||||
|
const maxY = objects.reduce((acc, cur) => (cur.top + cur.height > acc ? cur.top : acc), 0)
|
||||||
return [
|
return [
|
||||||
{ x: minX - margin, y: minY - margin },
|
{ x: minX - margin, y: minY - margin },
|
||||||
{ x: maxX + margin, y: maxY + margin },
|
{ x: maxX + margin, y: maxY + margin },
|
||||||
@ -62,29 +39,13 @@ export function useImgLoader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캔버스를 PNG 이미지로 캡처하여 서버에 전송한다.
|
* 캔버스를 이미지로 저장
|
||||||
*
|
* @param {integer} type 1: 모듈만 있는 상태, 2: 가대까지 올린 상태
|
||||||
* [처리 순서]
|
|
||||||
* 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) => {
|
const handleCanvasToPng = async (type) => {
|
||||||
try {
|
try {
|
||||||
// 1. 그리드, 흡착점, 마우스라인 등 제거/숨김
|
|
||||||
toggleLineEtc(false)
|
toggleLineEtc(false)
|
||||||
|
|
||||||
// 2. 배경을 흰색으로 설정 (격자 패턴이 autocrop을 방해하므로)
|
|
||||||
const originalBg = canvas.backgroundColor
|
|
||||||
canvas.backgroundColor = '#ffffff'
|
|
||||||
|
|
||||||
// CORS 대응: 이미지 오브젝트에 crossOrigin 설정
|
|
||||||
canvas.getObjects('image').forEach((obj) => {
|
canvas.getObjects('image').forEach((obj) => {
|
||||||
if (obj.getSrc) {
|
if (obj.getSrc) {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
@ -97,72 +58,51 @@ export function useImgLoader() {
|
|||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
// 고해상도 캡처를 위해 multiplier 옵션 추가 (2배 해상도)
|
||||||
// 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 multiplier = 2
|
||||||
const dataUrl = canvas.toDataURL({
|
const dataUrl = canvas.toDataURL({
|
||||||
format: 'png',
|
format: 'png',
|
||||||
multiplier: multiplier,
|
multiplier: multiplier,
|
||||||
left: cropLeft,
|
|
||||||
top: cropTop,
|
|
||||||
width: cropWidth,
|
|
||||||
height: cropHeight,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// base64 → Blob 변환
|
|
||||||
const blobBin = atob(dataUrl.split(',')[1])
|
const blobBin = atob(dataUrl.split(',')[1])
|
||||||
const array = []
|
const array = []
|
||||||
for (let i = 0; i < blobBin.length; i++) {
|
for (let i = 0; i < blobBin.length; i++) {
|
||||||
array.push(blobBin.charCodeAt(i))
|
array.push(blobBin.charCodeAt(i))
|
||||||
}
|
}
|
||||||
const file = new Blob([new Uint8Array(array)], { type: 'image/png' })
|
const file = new Blob([new Uint8Array(array)], { type: 'image/png' })
|
||||||
|
|
||||||
// 5. FormData 구성 및 서버 전송
|
|
||||||
formData.append('file', file, 'canvas.png')
|
formData.append('file', file, 'canvas.png')
|
||||||
formData.append('objectNo', currentCanvasPlan.objectNo)
|
formData.append('objectNo', currentCanvasPlan.objectNo)
|
||||||
formData.append('planNo', currentCanvasPlan.planNo)
|
formData.append('planNo', currentCanvasPlan.planNo)
|
||||||
formData.append('type', type)
|
formData.append('type', type)
|
||||||
formData.append('width', Math.round(cropWidth * multiplier))
|
/** 이미지 크롭 좌표 계산 (multiplier 배율 적용) */
|
||||||
formData.append('height', Math.round(cropHeight * multiplier))
|
const positionObj = getImageCoordinates()
|
||||||
formData.append('left', 0) // 프론트에서 이미 크롭하여 전송하므로 0
|
console.log('🚀 ~ handleCanvasToPng ~ positionObj:', positionObj)
|
||||||
formData.append('top', 0)
|
formData.append('width', Math.round((positionObj[1].x - positionObj[0].x + 100) * multiplier))
|
||||||
|
formData.append('height', Math.round((positionObj[1].y - positionObj[0].y + 100) * multiplier))
|
||||||
|
formData.append('left', Math.round(positionObj[0].x * multiplier))
|
||||||
|
formData.append('top', Math.round(positionObj[0].y * multiplier))
|
||||||
console.log('🚀 ~ handleCanvasToPng ~ formData:', formData)
|
console.log('🚀 ~ handleCanvasToPng ~ formData:', formData)
|
||||||
|
|
||||||
|
/** 이미지 크롭 요청 */
|
||||||
const result = await post({
|
const result = await post({
|
||||||
|
// url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/image/canvas`,
|
||||||
url: `${Config().baseUrl}/api/image/canvas`,
|
url: `${Config().baseUrl}/api/image/canvas`,
|
||||||
data: formData,
|
data: formData,
|
||||||
})
|
})
|
||||||
console.log('🚀 ~ handleCanvasToPng ~ result:', result)
|
console.log('🚀 ~ handleCanvasToPng ~ result:', result)
|
||||||
|
|
||||||
// 6. 배경/그리드 원복
|
|
||||||
canvas.backgroundColor = originalBg
|
|
||||||
canvas.renderAll()
|
|
||||||
|
|
||||||
toggleLineEtc(true)
|
toggleLineEtc(true)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
canvas.backgroundColor = originalBg
|
|
||||||
canvas.renderAll()
|
|
||||||
setIsGlobalLoading(false)
|
setIsGlobalLoading(false)
|
||||||
console.log('🚀 ~ handleCanvasToPng ~ e:', e)
|
console.log('🚀 ~ handleCanvasToPng ~ e:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 캡처 시 불필요한 요소(마우스라인, 그리드, 흡착점)를 숨기거나 복원한다.
|
* 마우스 포인터 그리드, 임의그리드, 흡착점 등 제거.
|
||||||
*
|
*
|
||||||
* @param {boolean} visible - true: 복원, false: 숨김
|
|
||||||
*/
|
*/
|
||||||
const toggleLineEtc = (visible = false) => {
|
const toggleLineEtc = (visible = false) => {
|
||||||
if (canvas?._objects.length > 0) {
|
if (canvas?._objects.length > 0) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user