- solveRectangleKerab: 4변 타입만으로 힙/마루 재생성(forward·revert 단일 공식) - nGable=0 우진각·1 Y apex·1 CASE B 케라바·2 맞배 지원, 그 외 기존 maze 폴백 - hip attributes.extended=true 부여 → integrateExtensionLines merge 스킵 → split 4면 정상 할당 - outerLine bbox 기준 sideOf 수정(出幅 offset 좌표 불일치 해소) - apex depth clamp min(L/2, oppDist) → R3-outside 방지 - detectKerabHipMarks·extendKerabHipsTo45 함수 추출(reclick·revert 45° 통합) - KERAB-EXTHIP-COORD-CLEAN: orphan ext-hip 좌표 기반 재제거 - useRefFiles: 변환 API FileData 누락 시 에러 메시지 swalFire 안내 - kerab-offset-surgical: skipKerabHips 옵션(케라바 hip t-ratio 재계산 방지) Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
346 lines
11 KiB
JavaScript
346 lines
11 KiB
JavaScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useRecoilState, useSetRecoilState } from 'recoil'
|
|
|
|
import { useSwal } from '@/hooks/useSwal'
|
|
import { useAxios } from '../useAxios'
|
|
import { currentCanvasPlanState } from '@/store/canvasAtom'
|
|
import { useCanvas } from '@/hooks/useCanvas'
|
|
import { deleteBackGroundImage, setBackGroundImage } from '@/lib/imageActions'
|
|
import { settingModalFirstOptionsState } from '@/store/settingAtom'
|
|
import { popSpinnerState } from '@/store/popupAtom'
|
|
import Config from '@/config/config.export'
|
|
import { useMessage } from '@/hooks/useMessage'
|
|
import { logger } from '@/util/logger'
|
|
|
|
/**
|
|
* 배경 이미지 관리
|
|
* 도면에 배경이미지를 로딩하는 기능을 제공
|
|
*
|
|
* 이미지 -> 캔버스 배경에 이미지 로드
|
|
* 주소 -> 구글 맵에서 주소 검색 후 이미지로 다운로드 받아서 캔버스 배경에 이미지 로드
|
|
* .dwg -> api를 통해서 .png로 변환 후 캔버스 배경에 이미지 로드
|
|
*
|
|
* setCurrentBgImage 에 이미지를 세팅하면 도면에 배경으로 로딩된다.
|
|
* 다만 S3 Response에 aws 고유 주소가 나오는데 여기서는 cloudfront 사용을 위해서 NEXT_PUBLIC_AWS_S3_BASE_URL 도메인을 사용한다.
|
|
* @returns {object}
|
|
*/
|
|
export function useRefFiles() {
|
|
const converterDwgUrl = process.env.NEXT_PUBLIC_CONVERTER_DWG_API_URL
|
|
const converterDxfUrl = process.env.NEXT_PUBLIC_CONVERTER_DXF_API_URL
|
|
const { getMessage } = useMessage()
|
|
const [refImage, setRefImage] = useState(null)
|
|
const [refFileMethod, setRefFileMethod] = useState('1')
|
|
const [mapPositionAddress, setMapPositionAddress] = useState('')
|
|
const [currentBgImage, setCurrentBgImage] = useState(null)
|
|
const queryRef = useRef(null)
|
|
const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState)
|
|
const [settingModalFirstOptions, setSettingModalFirstOptions] = useRecoilState(settingModalFirstOptionsState)
|
|
const { handleBackImageLoadToCanvas } = useCanvas()
|
|
const { swalFire } = useSwal()
|
|
const { get, post, del } = useAxios()
|
|
const setPopSpinnerStore = useSetRecoilState(popSpinnerState)
|
|
|
|
useEffect(() => {
|
|
if (refFileMethod === '1') {
|
|
setMapPositionAddress('')
|
|
} else {
|
|
setRefImage(null)
|
|
}
|
|
}, [refFileMethod])
|
|
|
|
/**
|
|
* 최초 input type="file" 에 대한 이벤트
|
|
* 파일 불러오기 버튼 컨트롤
|
|
* @param {*} file
|
|
*/
|
|
const handleRefFile = (file) => {
|
|
logger.log('handleRefFile', file)
|
|
logger.log('refImage', refImage)
|
|
|
|
if (refImage) {
|
|
swalFire({
|
|
text: getMessage('common.message.ref.file.change'),
|
|
type: 'confirm',
|
|
confirmFn: () => {
|
|
refFileSetting(file)
|
|
},
|
|
})
|
|
} else {
|
|
refFileSetting(file)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 허용하는 파일인지 체크한다
|
|
* @param {File} file
|
|
*/
|
|
const refFileSetting = (file) => {
|
|
logger.log('🚀 ~ refFileSetting ~ file:', file)
|
|
|
|
setPopSpinnerStore(true)
|
|
const newOption1 = settingModalFirstOptions.option1.map((option) => ({
|
|
...option,
|
|
selected: option.column === 'imageDisplay' ? true : option.selected,
|
|
}))
|
|
|
|
setSettingModalFirstOptions((prev) => ({
|
|
...prev,
|
|
option1: newOption1,
|
|
}))
|
|
|
|
if (file.name.split('.').pop() === 'dwg') {
|
|
handleUploadConvertRefFile(file, converterDwgUrl);
|
|
} else if (file.name.split('.').pop() === 'dxf') {
|
|
handleUploadConvertRefFile(file, converterDxfUrl);
|
|
} else {
|
|
if (file && ['image/png', 'image/jpg', 'image/jpeg', 'image/bmp', 'image/gif'].includes(file.type)) {
|
|
handleUploadImageRefFile(file)
|
|
} else {
|
|
swalFire({
|
|
text: getMessage('common.message.ref.file.noImage'),
|
|
type: 'alert',
|
|
icon: 'error',
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이미지 파일 삭제
|
|
*/
|
|
const handleFileDelete = async () => {
|
|
swalFire({
|
|
text: getMessage('common.message.data.delete'),
|
|
type: 'confirm',
|
|
confirmFn: async () => {
|
|
setPopSpinnerStore(true)
|
|
logger.log('🚀 ~ handleFileDelete ~ handleFileDelete:', refImage)
|
|
logger.log('🚀 ~ handleFileDelete ~ currentCanvasPlan.bgImageName:', currentCanvasPlan.bgImageName)
|
|
await del({ url: `${Config().baseUrl}/api/image/upload?fileName=${currentCanvasPlan.bgImageName}` })
|
|
setCurrentBgImage(null)
|
|
await deleteBackGroundImage({
|
|
objectId: currentCanvasPlan.id,
|
|
planNo: currentCanvasPlan.planNo,
|
|
})
|
|
setPopSpinnerStore(false)
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 주소 맵 이미지 삭제
|
|
*/
|
|
const handleAddressDelete = async () => {
|
|
swalFire({
|
|
text: getMessage('common.message.data.delete'),
|
|
type: 'confirm',
|
|
confirmFn: async () => {
|
|
logger.log('🚀 ~ handleAddressDelete ~ handleAddressDelete:', refImage)
|
|
logger.log('🚀 ~ handleAddressDelete ~ currentCanvasPlan.bgImageName:', currentCanvasPlan.bgImageName)
|
|
await del({ url: `${Config().baseUrl}/api/image/map?fileName=${currentCanvasPlan.bgImageName}` })
|
|
setMapPositionAddress('')
|
|
setCurrentBgImage(null)
|
|
await deleteBackGroundImage({
|
|
objectId: currentCanvasPlan.id,
|
|
planNo: currentCanvasPlan.planNo,
|
|
})
|
|
},
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 주소로 구글 맵 이미지 다운로드하여 캔버스 배경으로 로드
|
|
*/
|
|
const handleMapImageDown = async () => {
|
|
debugger; logger.log('🚀 ~ handleMapImageDown ~ handleMapImageDown:')
|
|
if (queryRef.current.value === '' || queryRef.current.value === null) {
|
|
return
|
|
}
|
|
|
|
const newOption1 = settingModalFirstOptions.option1.map((option) => ({
|
|
...option,
|
|
selected: option.column === 'imageDisplay' ? true : option.selected,
|
|
}))
|
|
|
|
setSettingModalFirstOptions((prev) => ({
|
|
...prev,
|
|
option1: newOption1,
|
|
}))
|
|
|
|
const res = await get({
|
|
url: `${Config().baseUrl}/api/image/map?q=${queryRef.current.value}&fileNm=${currentCanvasPlan.id}&zoom=20`,
|
|
})
|
|
logger.log('🚀 ~ handleMapImageDown ~ res:', res)
|
|
setCurrentBgImage(`${process.env.NEXT_PUBLIC_AWS_S3_BASE_URL}/${res.fileName}`)
|
|
|
|
await setBackGroundImage({
|
|
objectId: currentCanvasPlan.id,
|
|
planNo: currentCanvasPlan.planNo,
|
|
// imagePath: `${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`,
|
|
imagePath: `${res.filePath}`,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 배경 이미지 로드를 위한 세팅
|
|
*/
|
|
useEffect(() => {
|
|
// if (!currentBgImage) {
|
|
// return
|
|
// }
|
|
logger.log('🚀 ~ useEffect ~ currentBgImage:', currentBgImage)
|
|
if (currentBgImage) {
|
|
handleBackImageLoadToCanvas(currentBgImage, () => {
|
|
logger.log('Background image loaded successfully')
|
|
})
|
|
setCurrentCanvasPlan((prev) => ({
|
|
...prev,
|
|
// bgImageName: refImage?.name ?? null,
|
|
bgImageName: currentBgImage.split('/').pop(),
|
|
mapPositionAddress: queryRef.current.value,
|
|
}))
|
|
} else {
|
|
setRefImage(null)
|
|
setCurrentCanvasPlan((prev) => ({
|
|
...prev,
|
|
bgImageName: null,
|
|
mapPositionAddress: null,
|
|
}))
|
|
}
|
|
}, [currentBgImage])
|
|
|
|
/**
|
|
* 이미지 파일 업로드
|
|
* @param {*} file
|
|
*/
|
|
const handleUploadImageRefFile = async (file) => {
|
|
// setPopSpinnerStore(true)
|
|
// const newOption1 = settingModalFirstOptions.option1.map((option) => ({
|
|
// ...option,
|
|
// selected: option.column === 'imageDisplay' ? true : option.selected,
|
|
// }))
|
|
//
|
|
// setSettingModalFirstOptions((prev) => ({
|
|
// ...prev,
|
|
// option1: newOption1,
|
|
// }))
|
|
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
const res = await post({
|
|
url: `${Config().baseUrl}/api/image/upload`,
|
|
data: formData,
|
|
})
|
|
logger.log('🚀 ~ handleUploadImageRefFile ~ res:', res)
|
|
setCurrentBgImage(`${process.env.NEXT_PUBLIC_AWS_S3_BASE_URL}/${res.fileName}`)
|
|
setRefImage(file)
|
|
|
|
const params = {
|
|
objectId: currentCanvasPlan.id,
|
|
planNo: currentCanvasPlan.planNo,
|
|
// imagePath: `${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`,
|
|
imagePath: `${res.filePath}`,
|
|
}
|
|
logger.log('🚀 ~ handleUploadImageRefFile ~ params:', params)
|
|
await setBackGroundImage(params)
|
|
setPopSpinnerStore(false)
|
|
}
|
|
|
|
/**
|
|
* RefFile이 캐드 도면 파일일 경우 변환하여 이미지로 저장
|
|
* @param {*} file
|
|
* @param converterUrl
|
|
*/
|
|
const handleUploadConvertRefFile = async (file, converterUrl) => {
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
/** 캐드 도면 파일 변환 */
|
|
const res = await post({ url: converterUrl, data: formData })
|
|
logger.log('🚀 ~ handleUploadConvertRefFile ~ res:', res)
|
|
|
|
// 변환 API 가 정상 변환 대신 에러 본문(Code/Message)을 반환한 경우 그대로 throw [DXF-CONVERT-FAIL 2026-06-15]
|
|
if (!res?.Files?.[0]?.FileData) {
|
|
throw new Error(res?.Message || getMessage('common.message.ref.file.convertFail'))
|
|
}
|
|
|
|
// Convert Base64 to Blob
|
|
const base64Data = res.Files[0].FileData
|
|
const byteCharacters = atob(base64Data)
|
|
const byteArrays = []
|
|
|
|
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
|
const slice = byteCharacters.slice(offset, offset + 512)
|
|
const byteNumbers = new Array(slice.length)
|
|
|
|
for (let i = 0; i < slice.length; i++) {
|
|
byteNumbers[i] = slice.charCodeAt(i)
|
|
}
|
|
|
|
const byteArray = new Uint8Array(byteNumbers)
|
|
byteArrays.push(byteArray)
|
|
}
|
|
|
|
const blob = new Blob(byteArrays, { type: 'image/png' })
|
|
|
|
// Create File object from Blob
|
|
const convertImg = new File([blob], res.Files[0].FileName, { type: 'image/png' })
|
|
|
|
const newFormData = new FormData()
|
|
newFormData.append('file', convertImg)
|
|
|
|
/** 캐드 도면 파일 업로드 */
|
|
const result = await post({
|
|
url: `${Config().baseUrl}/api/image/cad`,
|
|
data: newFormData,
|
|
})
|
|
logger.log('🚀 ~ handleUploadConvertRefFile ~ result:', result)
|
|
|
|
setCurrentBgImage(`${process.env.NEXT_PUBLIC_AWS_S3_BASE_URL}/${result.fileName}`)
|
|
// setCurrentBgImage(result.filePath)
|
|
setRefImage(file)
|
|
|
|
const params = {
|
|
objectId: currentCanvasPlan.id,
|
|
planNo: currentCanvasPlan.planNo,
|
|
// imagePath: `${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`,
|
|
imagePath: `${result.filePath}`,
|
|
}
|
|
logger.log('🚀 ~ handleUploadImageRefFile ~ params:', params)
|
|
await setBackGroundImage(params)
|
|
} catch (error) {
|
|
// 변환/업로드 실패 시 API 응답 메시지를 그대로 노출 (없으면 기본 안내) [DXF-CONVERT-FAIL 2026-06-15]
|
|
logger.error('🚨 handleUploadConvertRefFile error:', error)
|
|
const apiMessage = error?.response?.data?.Message || error?.message || getMessage('common.message.ref.file.convertFail')
|
|
swalFire({ text: apiMessage, type: 'alert', icon: 'error' })
|
|
} finally {
|
|
setPopSpinnerStore(false)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 라디오 버튼 컨트롤
|
|
* @param {*} e
|
|
*/
|
|
const handleRefFileMethod = (e) => {
|
|
setRefFileMethod(e.target.value)
|
|
}
|
|
|
|
return {
|
|
refImage,
|
|
queryRef,
|
|
setRefImage,
|
|
handleRefFile,
|
|
refFileMethod,
|
|
setRefFileMethod,
|
|
mapPositionAddress,
|
|
setMapPositionAddress,
|
|
handleRefFileMethod,
|
|
handleFileDelete,
|
|
handleAddressDelete,
|
|
handleMapImageDown,
|
|
}
|
|
}
|