diff --git a/.env.development b/.env.development index cfc50e71..f35f6208 100644 --- a/.env.development +++ b/.env.development @@ -1,6 +1,8 @@ -NEXT_PUBLIC_API_SERVER_PATH="http://1.248.227.176:38080" +NEXT_PUBLIC_API_SERVER_PATH="https://dev-api.hanasys.jp" -NEXT_PUBLIC_HOST_URL="http://1.248.227.176:4000" +NEXT_PUBLIC_HOST_URL="//1.248.227.176:4000" + +NEXT_PUBLIC_API_HOST_URL="http://1.248.227.176:5000" SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y=" @@ -8,4 +10,18 @@ SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y=" NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secret=secret_yAS4QDalL9jgQ7vS" NEXT_PUBLIC_Q_ORDER_AUTO_LOGIN_URL="http://q-order-stg.q-cells.jp:8120/eos/login/autoLogin" -NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-stg.q-cells.jp:8120/qm/login/autoLogin" \ No newline at end of file +NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-stg.q-cells.jp:8120/qm/login/autoLogin" + +# 테스트용 +# AWS_REGION="ap-northeast-2" +# AMPLIFY_BUCKET="interplug" +# AWS_ACCESS_KEY_ID="AKIAVWMWJCUXFHEAZ4FR" +# AWS_SECRET_ACCESS_KEY="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" +# NEXT_PUBLIC_AWS_S3_BASE_URL="https://interplug.s3.ap-northeast-2.amazonaws.com" + +# 실제 일본 서버 +AWS_REGION="ap-northeast-1" +AMPLIFY_BUCKET="files.hanasys.jp" +AWS_ACCESS_KEY_ID="AKIA3K4QWLZHFZRJOM2E" +AWS_SECRET_ACCESS_KEY="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" +NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp" \ No newline at end of file diff --git a/.env.production b/.env.production index 09bb27d3..b2d50cdd 100644 --- a/.env.production +++ b/.env.production @@ -1,6 +1,8 @@ NEXT_PUBLIC_API_SERVER_PATH="https://api.hanasys.jp/" -NEXT_PUBLIC_HOST_URL="http://1.248.227.176:4000" +NEXT_PUBLIC_HOST_URL="//1.248.227.176:4000" + +NEXT_PUBLIC_API_HOST_URL="https://www.hanasys.jp/" SESSION_SECRET="i3iHH1yp2/2SpQSIySQ4bpyc4g0D+zCF9FAn5xUG0+Y=" @@ -10,4 +12,10 @@ NEXT_PUBLIC_CONVERTER_API_URL="https://v2.convertapi.com/convert/dwg/to/png?Secr # NEXT_PUBLIC_Q_ORDER_AUTO_LOGIN_URL="https://q-order.q-cells.jp/eos/login/autoLogin" # NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="https://q-musubi.q-cells.jp/qm/login/autoLogin" NEXT_PUBLIC_Q_ORDER_AUTO_LOGIN_URL="http://q-order-stg.q-cells.jp:8120/eos/login/autoLogin" -NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-stg.q-cells.jp:8120/qm/login/autoLogin" \ No newline at end of file +NEXT_PUBLIC_Q_MUSUBI_AUTO_LOGIN_URL="http://q-musubi-stg.q-cells.jp:8120/qm/login/autoLogin" + +AWS_REGION="ap-northeast-2" +AMPLIFY_BUCKET="interplug" +AWS_ACCESS_KEY_ID="AKIAVWMWJCUXFHEAZ4FR" +AWS_SECRET_ACCESS_KEY="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" +NEXT_PUBLIC_AWS_S3_BASE_URL="//files.hanasys.jp" \ No newline at end of file diff --git a/package.json b/package.json index d9c1af06..bbddf9ad 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "serve": "node server.js" }, "dependencies": { + "@aws-sdk/client-s3": "^3.772.0", "ag-grid-react": "^32.0.2", "axios": "^1.7.8", "big.js": "^6.2.2", @@ -24,7 +25,7 @@ "js-cookie": "^3.0.5", "mathjs": "^13.0.2", "mssql": "^11.0.1", - "next": "14.2.26", + "next": "14.2.28", "next-international": "^1.2.4", "react": "^18", "react-chartjs-2": "^5.2.0", @@ -38,6 +39,7 @@ "react-responsive-modal": "^6.4.2", "react-select": "^5.8.1", "recoil": "^0.7.7", + "sharp": "^0.33.5", "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "sweetalert2": "^11.14.1", diff --git a/qcast3.database.sqlite b/qcast3.database.sqlite index c0c43e6f..6a7a809a 100644 Binary files a/qcast3.database.sqlite and b/qcast3.database.sqlite differ diff --git a/src/app/api/image/cad/route.js b/src/app/api/image/cad/route.js new file mode 100644 index 00000000..b7d64a50 --- /dev/null +++ b/src/app/api/image/cad/route.js @@ -0,0 +1,69 @@ +import { NextResponse } from 'next/server' +import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3' + +const Bucket = process.env.AMPLIFY_BUCKET +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, +}) + +const uploadImage = async (file) => { + const Body = Buffer.from(await file.arrayBuffer()) + const Key = `cads/${file.name}` + const ContentType = file.ContentType + + await s3.send( + new PutObjectCommand({ + Bucket, + Key, + Body, + ContentType, + }), + ) + + return { + filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${Key}`, + fileName: Key, + } +} + +export async function POST(req) { + try { + const formData = await req.formData() + const file = formData.get('file') + + const result = await uploadImage(file) + + return NextResponse.json(result) + } catch (error) { + console.error(error) + return NextResponse.json({ error: 'Failed to upload image' }, { status: 500 }) + } +} + +export async function DELETE(req) { + try { + const searchParams = req.nextUrl.searchParams + const Key = `cads/${searchParams.get('fileName')}` + console.log('🚀 ~ DELETE ~ Key:', Key) + + if (!Key) { + return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) + } + + await s3.send( + new DeleteObjectCommand({ + Bucket, + Key, + }), + ) + + return NextResponse.json({ message: '이미지 삭제 성공' }, { status: 200 }) + } catch (error) { + console.error('S3 Delete Error:', error) + return NextResponse.json({ error: 'Failed to delete image' }, { status: 500 }) + } +} diff --git a/src/app/api/image/canvas/route.js b/src/app/api/image/canvas/route.js new file mode 100644 index 00000000..0a4d0be2 --- /dev/null +++ b/src/app/api/image/canvas/route.js @@ -0,0 +1,133 @@ +import { NextResponse } from 'next/server' +import { S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3' +import sharp from 'sharp' +import { v4 as uuidv4 } from 'uuid' +const Bucket = process.env.AMPLIFY_BUCKET +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, +}) + +const checkArea = (obj) => { + const { width, height, left, top } = obj + + if (left < 0 || top < 0 || width > 1600 || height > 1000) { + return false + } + + return true +} + +const cropImage = async (Key, width, height, left, top) => { + try { + const checkResult = checkArea({ width, height, left, top }) + + // Get the image from S3 + const { Body } = await s3.send( + new GetObjectCommand({ + Bucket, + Key, + }), + ) + + // Convert stream to buffer + const chunks = [] + for await (const chunk of Body) { + chunks.push(chunk) + } + const imageBuffer = Buffer.concat(chunks) + + let processedImage + if (!checkResult) { + processedImage = await sharp(imageBuffer).toBuffer() + } else { + processedImage = await sharp(imageBuffer) + .extract({ + width: parseInt(width), + height: parseInt(height), + left: parseInt(left), + top: parseInt(top), + }) + .png() + .toBuffer() + } + return processedImage + } catch (error) { + console.error('Error processing image:', error) + throw error + } +} + +export async function POST(req) { + try { + const formData = await req.formData() + const file = formData.get('file') + const objectNo = formData.get('objectNo') + const planNo = formData.get('planNo') + 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()}` + + /** + * 원본 이미지를 우선 저장한다. + * 이미지 이름이 겹지는 현상을 방지하기 위해 uuid 를 사용한다. + */ + await s3.send( + new PutObjectCommand({ + Bucket, + Key: OriginalKey, + Body: Buffer.from(await file.arrayBuffer()), + ContentType: 'image/png', + }), + ) + + /** + * 저장된 원본 이미지를 기준으로 크롭여부를 결정하여 크롭 이미지를 저장한다. + */ + const bufferImage = await cropImage(OriginalKey, width, height, left, top) + + /** + * 크롭 이미지 이름을 결정한다. + */ + const Key = `Drawing/${objectNo}_${planNo}_${type}.png` + + /** + * 크롭이 완료된 이미지를 업로드한다. + */ + await s3.send( + new PutObjectCommand({ + Bucket, + Key, + Body: bufferImage, + ContentType: 'image/png', + }), + ) + + /** + * 크롭이미지 저장이 완료되면 원본 이미지를 삭제한다. + */ + await s3.send( + new DeleteObjectCommand({ + Bucket, + Key: OriginalKey, + }), + ) + + const result = { + filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${Key}`, + fileName: Key, + } + + return NextResponse.json(result) + } catch (error) { + console.error('Error in POST:', error) + return NextResponse.json({ error: 'Failed to process and upload image' }, { status: 500 }) + } +} diff --git a/src/app/api/image/map/route.js b/src/app/api/image/map/route.js new file mode 100644 index 00000000..0cc76c02 --- /dev/null +++ b/src/app/api/image/map/route.js @@ -0,0 +1,78 @@ +import { NextResponse } from 'next/server' +import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3' + +const Bucket = process.env.AMPLIFY_BUCKET +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, +}) + +export async function GET(req) { + try { + const searchParams = req.nextUrl.searchParams + const q = searchParams.get('q') + const fileNm = searchParams.get('fileNm') + const zoom = searchParams.get('zoom') + + /** 구글 맵을 이미지로 변경하기 위한 API */ + const API_KEY = 'AIzaSyDO7nVR1N_D2tKy60hgGFavpLaXkHpiHpc' + const targetUrl = `https://maps.googleapis.com/maps/api/staticmap?center=${q}&zoom=${zoom}&maptype=satellite&size=640x640&scale=1&key=${API_KEY}` + const decodeUrl = decodeURIComponent(targetUrl) + + /** 구글 맵을 이미지로 변경하기 위한 API 호출 */ + const response = await fetch(decodeUrl) + const data = await response.arrayBuffer() + // const buffer = Buffer.from(data) + + /** 변경된 이미지를 S3에 업로드 */ + const Body = Buffer.from(data) + const Key = `maps/${fileNm}` + const ContentType = 'image/png' + + await s3.send( + new PutObjectCommand({ + Bucket, + Key, + Body, + ContentType, + }), + ) + + const result = { + filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${Key}`, + fileName: Key, + } + + return NextResponse.json(result) + } catch (error) { + console.error(error) + return NextResponse.json({ error: 'Failed to upload image' }, { status: 500 }) + } +} + +export async function DELETE(req) { + try { + const searchParams = req.nextUrl.searchParams + const Key = `maps/${searchParams.get('fileName')}` + console.log('🚀 ~ DELETE ~ Key:', Key) + + if (!Key) { + return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) + } + + await s3.send( + new DeleteObjectCommand({ + Bucket, + Key, + }), + ) + + return NextResponse.json({ message: '이미지 삭제 성공' }, { status: 200 }) + } catch (error) { + console.error('S3 Delete Error:', error) + return NextResponse.json({ error: 'Failed to delete image' }, { status: 500 }) + } +} diff --git a/src/app/api/image/upload/route.js b/src/app/api/image/upload/route.js new file mode 100644 index 00000000..4d875257 --- /dev/null +++ b/src/app/api/image/upload/route.js @@ -0,0 +1,72 @@ +import { NextResponse } from 'next/server' +import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3' + +const Bucket = process.env.AMPLIFY_BUCKET +const s3 = new S3Client({ + region: process.env.AWS_REGION, + credentials: { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + }, +}) + +const uploadImage = async (file) => { + const Body = Buffer.from(await file.arrayBuffer()) + const Key = `upload/${file.name}` + const ContentType = file.ContentType + + await s3.send( + new PutObjectCommand({ + Bucket, + Key, + Body, + ContentType, + }), + ) + + return { + filePath: `https://${process.env.AMPLIFY_BUCKET}.s3.${process.env.AWS_REGION}.amazonaws.com/${Key}`, + fileName: Key, + } +} + +export async function POST(req) { + try { + const formData = await req.formData() + const file = formData.get('file') + + const result = await uploadImage(file) + result.message = '이미지 업로드 성공' + + return NextResponse.json(result) + } catch (error) { + console.error(error) + return NextResponse.json({ error: 'Failed to upload image' }, { status: 500 }) + } +} + +export async function DELETE(req) { + try { + const searchParams = req.nextUrl.searchParams + const fileName = searchParams.get('fileName') + + if (!fileName) { + return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) + } + + const Key = `upload/${fileName}` + console.log('🚀 ~ DELETE ~ Key:', Key) + + await s3.send( + new DeleteObjectCommand({ + Bucket, + Key, + }), + ) + + return NextResponse.json({ message: '이미지 삭제 성공' }, { status: 200 }) + } catch (error) { + console.error('S3 Delete Error:', error) + return NextResponse.json({ error: 'Failed to delete image' }, { status: 500 }) + } +} diff --git a/src/components/estimate/Estimate.jsx b/src/components/estimate/Estimate.jsx index 8ce54402..7237e056 100644 --- a/src/components/estimate/Estimate.jsx +++ b/src/components/estimate/Estimate.jsx @@ -60,7 +60,7 @@ export default function Estimate({}) { const [cableItemList, setCableItemList] = useState([]) //케이블 리스트 const [cableItem, setCableItem] = useState('') //케이블 선택값 - + const [cableDbItem, setCableDbItem] = useState('') //케이블 선택값 const [startDate, setStartDate] = useState(new Date()) const singleDatePickerProps = { startDate, @@ -98,7 +98,7 @@ export default function Estimate({}) { } const initEstimate = (currPid = currentPid) => { - console.log('🚀 ~ initEstimate ~ currPid:', currPid) + // console.log('🚀 ~ initEstimate ~ currPid:', currPid) closeAll() setObjectNo(objectRecoil.floorPlanObjectNo) @@ -117,6 +117,7 @@ export default function Estimate({}) { item.value = item.clRefChr1 item.label = item.clRefChr2 }) + // console.log(code2) setCableItemList(code2) } @@ -152,7 +153,7 @@ export default function Estimate({}) { } useEffect(() => { - console.log('🚀 ~ Estimate ~ selectedPlan:', selectedPlan) + // console.log('🚀 ~ Estimate ~ selectedPlan:', selectedPlan) if (selectedPlan) initEstimate(selectedPlan.planNo) }, [selectedPlan]) @@ -739,6 +740,18 @@ export default function Estimate({}) { setCableItem(value) } + /* 케이블 select 변경시 */ + const onChangeDisplayDoubleCableItem = (value, itemList) => { + itemList.map((item, index) => { + if (item.dispCableFlg === '1' && item.itemTpCd === 'M12') { + if (value !== '') { + onChangeDisplayItem(value, item.dispOrder, index, true) + } + } + }) + setCableDbItem(value) + } + // 아이템 자동완성 검색시 아이템 추가/변경시 const onChangeDisplayItem = (itemId, dispOrder, index, flag) => { const param = { @@ -1088,15 +1101,20 @@ export default function Estimate({}) { item.showSaleTotPrice = '0' } - if (item.dispCableFlg === '1' && item.itemTpCd !== 'M12') { + if (item.dispCableFlg === '1' ) { dispCableFlgCnt++ - setCableItem(item.itemId) + if(item.itemTpCd === 'M12') { + setCableDbItem(item.itemId) + }else{ + setCableItem(item.itemId) + } } } }) if (dispCableFlgCnt === 0) { setCableItem('100038') + setCableDbItem('100037') } let pkgAsp = estimateContextState.pkgAsp ? Number(estimateContextState.pkgAsp.replaceAll(',', '')) : 0 @@ -1159,14 +1177,20 @@ export default function Estimate({}) { dispCableFlgCnt++ } - if (item.dispCableFlg === '1' && item.itemTpCd !== 'M12') { - setCableItem(item.itemId) + if (item.dispCableFlg === '1'){ + + if(item.itemTpCd === 'M12') { + setCableDbItem(item.itemId) + }else{ + setCableItem(item.itemId) + } } } }) if (dispCableFlgCnt === 0) { setCableItem('100038') + setCableDbItem('100037') } totals.vatPrice = totals.supplyPrice * 0.1 @@ -1227,6 +1251,7 @@ export default function Estimate({}) { if (dispCableFlgCnt === 0) { setCableItem('100038') + setCableDbItem('100037') } } }, [estimateContextState?.itemList, cableItemList]) @@ -1831,6 +1856,7 @@ export default function Estimate({}) {
+
{getMessage('estimate.detail.header.singleCable')}
+
+
+
+
{getMessage('estimate.detail.header.doubleCable')}
+
+
@@ -1927,7 +1976,7 @@ export default function Estimate({}) { onChangeSelect(item.dispOrder)} checked={!!selection.has(item.dispOrder)} /> diff --git a/src/components/floor-plan/modal/ImgLoad.jsx b/src/components/floor-plan/modal/ImgLoad.jsx index dda395a9..f0a86938 100644 --- a/src/components/floor-plan/modal/ImgLoad.jsx +++ b/src/components/floor-plan/modal/ImgLoad.jsx @@ -120,7 +120,7 @@ export default function ImgLoad() { value={refImage ? (refImage?.name ?? '') : (currentCanvasPlan?.bgImageName ?? '')} readOnly /> - {refImage && } + {currentCanvasPlan?.bgImageName && } diff --git a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx index 4a4d72a2..281ae5a8 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx @@ -86,6 +86,26 @@ export default function PassivityCircuitAllocation(props) { .map((obj) => obj.circuitNumber), ), ] + + const surfaceList = targetModules.map((module) => { + return canvas.getObjects().filter((obj) => obj.id === canvas.getObjects().filter((obj) => obj.id === module)[0].surfaceId)[0] + }) + + if (surfaceList.length > 1) { + let surfaceType = {} + + surfaceList.forEach((surface) => { + surfaceType[`${surface.direction}-${surface.roofMaterial.pitch}`] = surface + }) + if (Object.keys(surfaceType).length > 1) { + swalFire({ + text: getMessage('module.circuit.fix.not.same.roof.error'), + type: 'alert', + icon: 'warning', + }) + return + } + } if (!circuitNumber || circuitNumber === 0) { swalFire({ text: getMessage('module.circuit.minimun.error'), diff --git a/src/hooks/common/useCanvasPopupStatusController.js b/src/hooks/common/useCanvasPopupStatusController.js index b6499099..e7bd8457 100644 --- a/src/hooks/common/useCanvasPopupStatusController.js +++ b/src/hooks/common/useCanvasPopupStatusController.js @@ -37,6 +37,7 @@ export function useCanvasPopupStatusController(param = 1) { * @returns */ const getModuleSelection = async (popupTypeParam) => { + if (!currentCanvasPlan.objectNo || !currentCanvasPlan.planNo) return const result = await promiseGet({ url: `/api/v1/canvas-popup-status?objectNo=${currentCanvasPlan.objectNo}&planNo=${currentCanvasPlan.planNo}&popupType=${popupTypeParam}`, }) @@ -48,7 +49,7 @@ export function useCanvasPopupStatusController(param = 1) { return null }) - return result.data + return result?.data } /** @@ -59,7 +60,7 @@ export function useCanvasPopupStatusController(param = 1) { let resultData = {} for (let i = 1; i < 3; i++) { const result = await getModuleSelection(i) - if (!result.objectNo) return + if (!result?.objectNo) return if (i === 1) { if (result.popupStatus && unescapeString(result.popupStatus)) { const data = JSON.parse(unescapeString(result.popupStatus)) diff --git a/src/hooks/common/useRefFiles.js b/src/hooks/common/useRefFiles.js index b21cc954..d13e7213 100644 --- a/src/hooks/common/useRefFiles.js +++ b/src/hooks/common/useRefFiles.js @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { useRecoilState } from 'recoil' +import { useRecoilState, useSetRecoilState } from 'recoil' import { useSwal } from '@/hooks/useSwal' import { useAxios } from '../useAxios' @@ -7,6 +7,7 @@ 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' /** * 배경 이미지 관리 @@ -15,6 +16,9 @@ import { settingModalFirstOptionsState } from '@/store/settingAtom' * 이미지 -> 캔버스 배경에 이미지 로드 * 주소 -> 구글 맵에서 주소 검색 후 이미지로 다운로드 받아서 캔버스 배경에 이미지 로드 * .dwg -> api를 통해서 .png로 변환 후 캔버스 배경에 이미지 로드 + * + * setCurrentBgImage 에 이미지를 세팅하면 도면에 배경으로 로딩된다. + * 다만 S3 Response에 aws 고유 주소가 나오는데 여기서는 cloudfront 사용을 위해서 NEXT_PUBLIC_AWS_S3_BASE_URL 도메인을 사용한다. * @returns {object} */ export function useRefFiles() { @@ -28,7 +32,8 @@ export function useRefFiles() { const [settingModalFirstOptions, setSettingModalFirstOptions] = useRecoilState(settingModalFirstOptionsState) const { handleBackImageLoadToCanvas } = useCanvas() const { swalFire } = useSwal() - const { get, post } = useAxios() + const { get, post, del } = useAxios() + const setPopSpinnerStore = useSetRecoilState(popSpinnerState) useEffect(() => { if (refFileMethod === '1') { @@ -39,6 +44,7 @@ export function useRefFiles() { }, [refFileMethod]) /** + * 최초 input type="file" 에 대한 이벤트 * 파일 불러오기 버튼 컨트롤 * @param {*} file */ @@ -59,6 +65,10 @@ export function useRefFiles() { } } + /** + * 허용하는 파일인지 체크한다 + * @param {File} file + */ const refFileSetting = (file) => { console.log('🚀 ~ refFileSetting ~ file:', file) if (file.name.split('.').pop() === 'dwg') { @@ -77,34 +87,40 @@ export function useRefFiles() { } /** - * 파일 삭제 + * 이미지 파일 삭제 */ const handleFileDelete = async () => { swalFire({ text: '삭제하시겠습니까?', type: 'confirm', confirmFn: async () => { - setRefImage(null) - setCurrentCanvasPlan((prev) => ({ ...prev, bgImageName: null })) + setPopSpinnerStore(true) + console.log('🚀 ~ handleFileDelete ~ handleFileDelete:', refImage) + console.log('🚀 ~ handleFileDelete ~ currentCanvasPlan.bgImageName:', currentCanvasPlan.bgImageName) + await del({ url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/upload?fileName=${currentCanvasPlan.bgImageName}` }) + setCurrentBgImage(null) await deleteBackGroundImage({ objectId: currentCanvasPlan.id, planNo: currentCanvasPlan.planNo, }) + setPopSpinnerStore(false) }, }) } /** - * 주소 삭제 + * 주소 맵 이미지 삭제 */ const handleAddressDelete = async () => { swalFire({ text: '삭제하시겠습니까?', type: 'confirm', confirmFn: async () => { + console.log('🚀 ~ handleAddressDelete ~ handleAddressDelete:', refImage) + console.log('🚀 ~ handleAddressDelete ~ currentCanvasPlan.bgImageName:', currentCanvasPlan.bgImageName) + await del({ url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/map?fileName=${currentCanvasPlan.bgImageName}` }) setMapPositionAddress('') setCurrentBgImage(null) - setCurrentCanvasPlan((prev) => ({ ...prev, mapPositionAddress: null })) await deleteBackGroundImage({ objectId: currentCanvasPlan.id, planNo: currentCanvasPlan.planNo, @@ -114,7 +130,7 @@ export function useRefFiles() { } /** - * 주소로 구글 맵 이미지 다운로드 + * 주소로 구글 맵 이미지 다운로드하여 캔버스 배경으로 로드 */ const handleMapImageDown = async () => { console.log('🚀 ~ handleMapImageDown ~ handleMapImageDown:') @@ -133,15 +149,16 @@ export function useRefFiles() { })) const res = await get({ - url: `${process.env.NEXT_PUBLIC_HOST_URL}/map/convert?q=${queryRef.current.value}&fileNm=${currentCanvasPlan.id}&zoom=20`, + url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/map?q=${queryRef.current.value}&fileNm=${currentCanvasPlan.id}&zoom=20`, }) console.log('🚀 ~ handleMapImageDown ~ res:', res) - setCurrentBgImage(`${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`) + 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: `${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`, + imagePath: `${res.filePath}`, }) } @@ -149,16 +166,26 @@ export function useRefFiles() { * 배경 이미지 로드를 위한 세팅 */ useEffect(() => { - if (!currentBgImage) { - return - } + // if (!currentBgImage) { + // return + // } console.log('🚀 ~ useEffect ~ currentBgImage:', currentBgImage) - handleBackImageLoadToCanvas(currentBgImage) - setCurrentCanvasPlan((prev) => ({ - ...prev, - bgImageName: refImage?.name ?? null, - mapPositionAddress: queryRef.current.value, - })) + if (currentBgImage) { + handleBackImageLoadToCanvas(currentBgImage) + 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]) /** @@ -166,6 +193,7 @@ export function useRefFiles() { * @param {*} file */ const handleUploadImageRefFile = async (file) => { + setPopSpinnerStore(true) const newOption1 = settingModalFirstOptions.option1.map((option) => ({ ...option, selected: option.column === 'imageDisplay' ? true : option.selected, @@ -180,20 +208,22 @@ export function useRefFiles() { formData.append('file', file) const res = await post({ - url: `${process.env.NEXT_PUBLIC_HOST_URL}/image/upload`, + url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/upload`, data: formData, }) console.log('🚀 ~ handleUploadImageRefFile ~ res:', res) - setCurrentBgImage(`${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`) + 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: `${process.env.NEXT_PUBLIC_HOST_URL}${res.filePath}`, + imagePath: `${res.filePath}`, } console.log('🚀 ~ handleUploadImageRefFile ~ params:', params) await setBackGroundImage(params) + setPopSpinnerStore(false) } /** @@ -204,15 +234,28 @@ export function useRefFiles() { const formData = new FormData() formData.append('file', file) + /** 캐드 도면 파일 변환 */ const res = await post({ url: converterUrl, data: formData }) console.log('🚀 ~ handleUploadConvertRefFile ~ res:', res) + + /** 캐드 도면 파일 업로드 */ const result = await post({ - url: `${process.env.NEXT_PUBLIC_HOST_URL}/cad/convert`, + url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/cad`, data: res, }) console.log('🚀 ~ handleUploadConvertRefFile ~ result:', result) - setCurrentBgImage(`${process.env.NEXT_PUBLIC_HOST_URL}${result.filePath}`) - setRefImage(res.Files[0].FileData) + + 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: `${result.filePath}`, + } + console.log('🚀 ~ handleUploadImageRefFile ~ params:', params) + await setBackGroundImage(params) } /** diff --git a/src/hooks/floorPlan/useImgLoader.js b/src/hooks/floorPlan/useImgLoader.js index e51a4a2b..ca5fef79 100644 --- a/src/hooks/floorPlan/useImgLoader.js +++ b/src/hooks/floorPlan/useImgLoader.js @@ -79,7 +79,8 @@ export function useImgLoader() { /** 이미지 크롭 요청 */ const result = await post({ - url: `${process.env.NEXT_PUBLIC_HOST_URL}/image/canvas`, + // url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/image/canvas`, + url: `${process.env.NEXT_PUBLIC_API_HOST_URL}/api/image/canvas`, data: formData, }) console.log('🚀 ~ handleCanvasToPng ~ result:', result) diff --git a/src/hooks/roofcover/useAuxiliaryDrawing.js b/src/hooks/roofcover/useAuxiliaryDrawing.js index 78d0b883..c854ae77 100644 --- a/src/hooks/roofcover/useAuxiliaryDrawing.js +++ b/src/hooks/roofcover/useAuxiliaryDrawing.js @@ -704,6 +704,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) { removeLine(line1) } intersectionPoints.current.push(interSectionPointsWithRoofLines[0]) + return } } diff --git a/src/hooks/roofcover/useOuterLineWall.js b/src/hooks/roofcover/useOuterLineWall.js index e0418a75..00929a6b 100644 --- a/src/hooks/roofcover/useOuterLineWall.js +++ b/src/hooks/roofcover/useOuterLineWall.js @@ -96,6 +96,10 @@ export function useOuterLineWall(id, propertiesId) { } addCanvasMouseEventListener('mouse:down', mouseDown) + addDocumentEventListener('contextmenu', document, (e) => { + handleRollback() + }) + clear() return () => { initEvent() @@ -682,6 +686,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } + enterCheck(e) // 포커스가 length1에 있지 않으면 length1에 포커스를 줌 const activeElem = document.activeElement if (activeElem !== length1Ref.current) { @@ -746,6 +751,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } + enterCheck(e) const key = e.key const activeElem = document.activeElement @@ -779,6 +785,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } + enterCheck(e) const key = e.key switch (key) { case 'Down': // IE/Edge에서 사용되는 값 @@ -804,6 +811,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } + enterCheck(e) const key = e.key switch (key) { case 'Enter': { @@ -828,7 +836,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } - + enterCheck(e) const key = e.key switch (key) { case 'Down': // IE/Edge에서 사용되는 값 @@ -892,6 +900,12 @@ export function useOuterLineWall(id, propertiesId) { isFix.current = true } + const enterCheck = (e) => { + if (e.key === 'Enter') { + handleFix() + } + } + return { points, setPoints, diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index d53faa39..be23cd7c 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -92,6 +92,9 @@ export function usePlacementShapeDrawing(id) { } addCanvasMouseEventListener('mouse:down', mouseDown) + addDocumentEventListener('contextmenu', document, (e) => { + handleRollback() + }) clear() }, [verticalHorizontalMode, points, adsorptionPointAddMode, adsorptionPointMode, adsorptionRange, interval, tempGridMode]) @@ -175,7 +178,7 @@ export function usePlacementShapeDrawing(id) { } } -/* + /* mouseMove */ const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) @@ -184,7 +187,7 @@ mouseMove const { getAdsorptionPoints } = useAdsorptionPoint() const mouseMove = (e) => { - removeMouseLine(); + removeMouseLine() const pointer = canvas.getPointer(e.e) const roofsPoints = roofs.map((roof) => roof.points).flat() roofAdsorptionPoints.current = [...roofsPoints] @@ -246,7 +249,6 @@ mouseMove }) canvas?.add(horizontalLine, verticalLine) canvas?.renderAll() - } useEffect(() => { @@ -768,6 +770,7 @@ mouseMove if (points.length === 0) { return } + enterCheck(e) // 포커스가 length1에 있지 않으면 length1에 포커스를 줌 const activeElem = document.activeElement @@ -833,6 +836,7 @@ mouseMove if (points.length === 0) { return } + enterCheck(e) const key = e.key const activeElem = document.activeElement @@ -866,6 +870,7 @@ mouseMove if (points.length === 0) { return } + enterCheck(e) const key = e.key switch (key) { case 'Down': // IE/Edge에서 사용되는 값 @@ -891,6 +896,7 @@ mouseMove if (points.length === 0) { return } + enterCheck(e) const key = e.key switch (key) { case 'Enter': { @@ -915,6 +921,7 @@ mouseMove if (points.length === 0) { return } + enterCheck(e) const key = e.key switch (key) { @@ -979,6 +986,12 @@ mouseMove isFix.current = true } + const enterCheck = (e) => { + if (e.key === 'Enter') { + handleFix() + } + } + return { points, setPoints, diff --git a/src/hooks/surface/useSurfaceShapeBatch.js b/src/hooks/surface/useSurfaceShapeBatch.js index 16f68668..863de4bf 100644 --- a/src/hooks/surface/useSurfaceShapeBatch.js +++ b/src/hooks/surface/useSurfaceShapeBatch.js @@ -72,16 +72,12 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) { length5 = surfaceRefs.length5.current.value } - console.log(' before length : ', length1, length2, length3, length4, length5) - length1 = parseFloat(length1 === undefined ? 0 : Number(length1 / 10).toFixed(1)) length2 = parseFloat(length2 === undefined ? 0 : Number(length2 / 10).toFixed(1)) length3 = parseFloat(length3 === undefined ? 0 : Number(length3 / 10).toFixed(1)) length4 = parseFloat(length4 === undefined ? 0 : Number(length4 / 10).toFixed(1)) length5 = parseFloat(length5 === undefined ? 0 : Number(length5 / 10).toFixed(1)) - console.log(' after length : ', length1, length2, length3, length4, length5) - let isDrawing = true let obj = null let points = [] diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 577274ad..91562948 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1127,6 +1127,9 @@ export const usePolygon = () => { case 'left': defense = 'north' break + default: + defense = 'south' + break } pitch = polygon.lines[index]?.attributes?.pitch ?? 0 diff --git a/src/locales/ja.json b/src/locales/ja.json index 35f0b757..7593b6b6 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -125,9 +125,9 @@ "modal.module.basic.settting.module.error6": "垂木の間隔を入力してください。\n(屋根材: {0})", "modal.module.basic.settting.module.error7": "下在ビーチを入力してください。\n(屋根材: {0})", "modal.module.basic.settting.module.error8": "モジュール配置領域の値を入力してください。\n(屋根材: {0})", - "modal.module.basic.settting.module.error9": "軒側の値は{0} mm以上でなければなりません。\n(屋根材: {1})", - "modal.module.basic.settting.module.error10": "吊下側の値は{0} mm以上でなければなりません。\n(屋根材: {1})", - "modal.module.basic.settting.module.error11": "ケラバ側の値は{0} mm以上でなければなりません。\n(屋根材: {1})", + "modal.module.basic.settting.module.error9": "軒側の配置領域の値を{0} mm以上に変更してください。\n(屋根材: {1})", + "modal.module.basic.settting.module.error10": "棟側の配置領域の値を{0} mm以上に変更してください。\n(屋根材: {1})", + "modal.module.basic.settting.module.error11": "ケラバ側の配置領域の値を{0} mm以上に変更してください。\n(屋根材: {1})", "modal.module.basic.settting.module.error12": "施工方法を選択してください。\n(屋根材: {0})", "modal.module.basic.setting.module.placement": "モジュールの配置", "modal.module.basic.setting.module.placement.select.fitting.type": "設置形態を選択してください。", @@ -786,7 +786,7 @@ "stuff.search.schObjectNo": "物件番号", "stuff.search.schSaleStoreName": "販売代理店名", "stuff.search.schAddress": "商品アドレス", - "stuff.search.schObjectName": "商品名", + "stuff.search.schObjectName": "物件名", "stuff.search.schDispCompanyName": "見積先", "stuff.search.schSelSaleStoreId": "販売代理店選択", "stuff.search.schReceiveUser": "担当者", @@ -944,6 +944,8 @@ "estimate.detail.sepcialEstimateProductInfo.pkgPrice": "PKG金額", "estimate.detail.header.showPrice": "価格表示", "estimate.detail.header.unitPrice": "定価", + "estimate.detail.header.singleCable": "片端ケーブル長さ", + "estimate.detail.header.doubleCable": "両端ケーブル長さ", "estimate.detail.showPrice.pricingBtn": "Pricing", "estimate.detail.showPrice.pricingBtn.noItemId": "Pricingが欠落しているアイテムがあります。 Pricingを進めてください。", "estimate.detail.showPrice.pricingBtn.confirm": "価格登録初期化されますがよろしいですか?", diff --git a/src/locales/ko.json b/src/locales/ko.json index daf9ad60..e7ab9711 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -945,6 +945,8 @@ "estimate.detail.sepcialEstimateProductInfo.pkgPrice": "PKG 금액", "estimate.detail.header.showPrice": "가격표시", "estimate.detail.header.unitPrice": "정가", + "estimate.detail.header.singleCable": "편당케이블 길이", + "estimate.detail.header.doubleCable": "양단케이블 길이", "estimate.detail.showPrice.pricingBtn": "Pricing", "estimate.detail.showPrice.pricingBtn.noItemId": "Pricing이 누락된 아이템이 있습니다. 제품 선택 후 Pricing을 진행해주세요.", "estimate.detail.showPrice.pricingBtn.confirm": "가격등록을 초기화 하시겠습니까?", @@ -1050,6 +1052,7 @@ "module.not.found": "모듈을 선택하세요.", "module.circuit.minimun.error": "회로번호는 1 이상입력해주세요.", "module.already.exist.error": "회로번호가 같은 다른 파워 컨디셔너 모듈이 있습니다. 다른 회로번호를 설정하십시오.", + "module.circuit.fix.not.same.roof.error": "다른 지붕면의 모듈이 선택되어 있습니다. 모듈 선택을 다시 하세요.", "construction.length.difference": "지붕면 공법을 모두 선택하십시오.", "menu.validation.canvas.roof": "패널을 배치하려면 지붕면을 입력해야 합니다.", "batch.object.outside.roof": "오브젝트는 지붕내에 설치해야 합니다.",