diff --git a/package.json b/package.json index 6c94a282..f338fbe4 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@nextui-org/react": "^2.4.2", "ag-grid-react": "^32.0.2", "axios": "^1.7.3", + "chart.js": "^4.4.6", "fabric": "^5.3.0", "framer-motion": "^11.2.13", "fs": "^0.0.1-security", @@ -22,6 +23,7 @@ "next": "14.2.3", "next-international": "^1.2.4", "react": "^18", + "react-chartjs-2": "^5.2.0", "react-colorful": "^5.6.1", "react-datepicker": "^7.3.0", "react-dom": "^18", diff --git a/src/app/api/html2canvas/route.js b/src/app/api/html2canvas/route.js index 54af611d..02731f07 100644 --- a/src/app/api/html2canvas/route.js +++ b/src/app/api/html2canvas/route.js @@ -13,7 +13,6 @@ export async function GET(req) { const decodeUrl = decodeURIComponent(targetUrl) const response = await fetch(decodeUrl) - const data = await response.arrayBuffer() const buffer = Buffer.from(data) diff --git a/src/app/floor-plan/simulator/[mid]/page.jsx b/src/app/floor-plan/simulator/[mid]/page.jsx new file mode 100644 index 00000000..939e1cc4 --- /dev/null +++ b/src/app/floor-plan/simulator/[mid]/page.jsx @@ -0,0 +1,9 @@ +import Simulator from '@/components/simulator/Simulator' + +export default function SimulatorPage() { + return ( + <> + + + ) +} diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index 82ba40c1..081eedc2 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -431,7 +431,16 @@ export default function Roof2(props) { { x: 450, y: 850 }, ] - const polygon = new QPolygon(type2, { + const test1 = [ + { x: 381, y: 178 }, + { x: 381, y: 659.3 }, + { x: 773.3, y: 659.3 }, + { x: 773.3, y: 497.9 }, + { x: 1457, y: 497.9 }, + { x: 1457, y: 178 }, + ] + + const polygon = new QPolygon(test1, { fill: 'transparent', stroke: 'green', strokeWidth: 1, diff --git a/src/components/estimate/Estimate.jsx b/src/components/estimate/Estimate.jsx index f794cb50..442c597b 100644 --- a/src/components/estimate/Estimate.jsx +++ b/src/components/estimate/Estimate.jsx @@ -9,12 +9,13 @@ import SingleDatePicker from '../common/datepicker/SingleDatePicker' import EstimateFileUploader from './EstimateFileUploader' import { useAxios } from '@/hooks/useAxios' import { globalLocaleStore } from '@/store/localeAtom' -import { isNotEmptyArray, isObjectNotEmpty } from '@/util/common-utils' +import { isNotEmptyArray, isObjectNotEmpty, queryStringFormatter } from '@/util/common-utils' import dayjs from 'dayjs' import { useCommonCode } from '@/hooks/common/useCommonCode' -import Select from 'react-select' import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController' import { SessionContext } from '@/app/SessionProvider' +import Select, { components } from 'react-select' +// import EstimateItemTable from './EstimateItemTable' export default function Estimate({ params }) { const { session } = useContext(SessionContext) @@ -33,6 +34,8 @@ export default function Estimate({ params }) { const { findCommonCode } = useCommonCode() const [honorificCodeList, setHonorificCodeList] = useState([]) //경칭 공통코드 + const [storePriceList, setStorePriceList] = useState([]) //가격표시 option + const [startDate, setStartDate] = useState(new Date()) const singleDatePickerProps = { startDate, @@ -44,7 +47,7 @@ export default function Estimate({ params }) { //견적서 상세데이터 const { state, setState } = useEstimateController(params.pid) - //견적특이사항 상세 데이터 LIST + const [itemList, setItemList] = useState([]) //견적특이사항 List const [specialNoteList, setSpecialNoteList] = useState([]) @@ -155,6 +158,46 @@ export default function Estimate({ params }) { }) } + //아이템 목록 + useEffect(() => { + if (isNotEmptyArray(state.itemList)) { + setItemList(state.itemList) + } + }, [state?.itemList]) + + //가격표시 option 세팅 + useEffect(() => { + const param = { + saleStoreId: session.storeId, + sapSalesStoreCd: session.custCd, + docTpCd: state?.estimateType, + } + const apiUrl = `/api/estimate/price/store-price-list?${queryStringFormatter(param)}` + get({ url: apiUrl }).then((res) => { + if (isNotEmptyArray(res?.data)) { + setStorePriceList(res.data) + } + }) + }, [state?.estimateType]) + + //Pricing 버튼 + const handlePricing = async (priceCd) => { + const param = { + saleStoreId: session.storeId, + sapSalesStoreCd: session.custCd, + docTpCd: state.estimateType, + priceCd: priceCd, + itemIdList: [], //아이템 + } + + console.log('param::', param) + return + await promisePost({ url: '/api/estimate/price/item-price-list', data: param }).then((res) => { + console.log('프라이싱결과::::::', res) + //아이템쪽 다 새로고침............SUCK!!! + }) + } + return (
@@ -304,6 +347,7 @@ export default function Estimate({ params }) { value={'YJSS'} checked={state?.estimateType === 'YJSS' ? true : false} onChange={(e) => { + //주문분류 setState({ estimateType: e.target.value }) }} /> @@ -380,9 +424,18 @@ export default function Estimate({ params }) {

{getMessage('estimate.detail.header.fileList1')}

- + { + setState({ + fileFlg: e.target.checked ? '1' : '0', + }) + }} + />
@@ -416,7 +469,7 @@ export default function Estimate({ params }) {
    - {isNotEmptyArray(originFiles) && + {originFiles.length > 0 && originFiles.map((originFile) => { return (
  • @@ -486,13 +539,13 @@ export default function Estimate({ params }) { return (
    {row.codeNm}
    -
    {row.remarks}
    +
    ) } })}
- {/* 견적특이사항 선택한 내용?영역끝 */} + {/* 견적특이사항 선택한 내용 영역끝 */}
@@ -565,12 +618,30 @@ export default function Estimate({ params }) {
{getMessage('estimate.detail.header.showPrice')}
- + {session?.storeLvl === '1' ? ( + + ) : ( + + )}
- +
{/* 가격표시영역끝 */} {/* html테이블시작 */} -
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
{getMessage('estimate.detail.itemTableHeader.col1')}{getMessage('estimate.detail.itemTableHeader.col2')}{getMessage('estimate.detail.itemTableHeader.col3')}{getMessage('estimate.detail.itemTableHeader.col4')}{getMessage('estimate.detail.itemTableHeader.col5')}{getMessage('estimate.detail.itemTableHeader.col6')}{getMessage('estimate.detail.itemTableHeader.col7')}
+
+ + +
+
100 +
+
{/*
+
+
HNW-MC4-CHN30
+
+ + +
+
+
+
+ +
+
セット +
+
+ +
+
+ +
+
+
5,561,000
+
+ {/* html테이블끝 */} diff --git a/src/components/estimate/EstimateFileUploader.jsx b/src/components/estimate/EstimateFileUploader.jsx index c2bccb22..dfc82d49 100644 --- a/src/components/estimate/EstimateFileUploader.jsx +++ b/src/components/estimate/EstimateFileUploader.jsx @@ -27,6 +27,7 @@ export default function EstimateFileUploader({ uploadFiles, setUploadFiles }) { // if (res.data > 0) setUploadFiles([...files, { name: e.target.files[0].name, id: uuidv4() }]) // }) setUploadFiles([...uploadFiles, { data: e.target.files[0], id: uuidv4() }]) + e.target.value = '' } const deleteFile = (id) => { diff --git a/src/components/estimate/popup/DocDownOptionPop.jsx b/src/components/estimate/popup/DocDownOptionPop.jsx new file mode 100644 index 00000000..0c859c59 --- /dev/null +++ b/src/components/estimate/popup/DocDownOptionPop.jsx @@ -0,0 +1,255 @@ +'use client' +import { useState } from 'react' +import { useMessage } from '@/hooks/useMessage' +import { useAxios } from '@/hooks/useAxios' +import { useRecoilValue } from 'recoil' +import { floorPlanObjectState } from '@/store/floorPlanObjectAtom' + +export default function DocDownOptionPop({ planNo, setEstimatePopupOpen }) { + // console.log('플랜번호::::::::::::', planNo) + const { getMessage } = useMessage() + const { promiseGet } = useAxios() + + //다운로드 파일 EXCEL + const [schUnitPriceFlg, setSchUnitPriceFlg] = useState('0') + + //견적제출서 표시명 + const [schDisplayFlg, setSchSchDisplayFlg] = useState('0') + //가대 중량표 포함 + const [schWeightFlg, setSchWeightFlg] = useState('0') + //도면/시뮬레이션 파일 포함 + const [schDrawingFlg, setSchDrawingFlg] = useState('0') + + // recoil 물건번호 + const objectRecoil = useRecoilValue(floorPlanObjectState) + + //문서 다운로드 + const handleFileDown = async () => { + // console.log('물건번호:::', objectRecoil.floorPlanObjectNo) + // console.log('planNo::', planNo) + // 고른 옵션값들 + //0 : 견적가 Excel 1 : 정가용Excel 2: 견적가 PDF 3 :정가용PDF + // console.log(schUnitPriceFlg) + // console.log(schDisplayFlg) + // console.log(schWeightFlg) + // console.log(schDrawingFlg) + const url = '/api/estimate/excel-download' + const params = {} + const options = { responseType: 'blob' } + } + + return ( +
+
+
+
+

{getMessage('estimate.detail.docPopup.title')}

+ +
+
+
+
{getMessage('estimate.detail.docPopup.explane')}
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ {getMessage('estimate.detail.docPopup.schUnitPriceFlg')} + * + +
+
+ { + setSchUnitPriceFlg(e.target.value) + }} + /> + +
+
+ { + setSchUnitPriceFlg(e.target.value) + }} + /> + +
+
+ { + setSchUnitPriceFlg(e.target.value) + }} + /> + +
+
+ { + setSchUnitPriceFlg(e.target.value) + }} + /> + +
+
+
+ {getMessage('estimate.detail.docPopup.schDisplayFlg')} * + +
+
+ { + setSchSchDisplayFlg(e.target.value) + }} + /> + +
+
+ { + setSchSchDisplayFlg(e.target.value) + }} + /> + +
+
+
+ {getMessage('estimate.detail.docPopup.schWeightFlg')} * + +
+
+ { + setSchWeightFlg(e.target.value) + }} + /> + +
+
+ { + setSchWeightFlg(e.target.value) + }} + /> + +
+
+
{getMessage('estimate.detail.docPopup.schDrawingFlg')} +
+
+ { + setSchDrawingFlg(e.target.value) + }} + /> + +
+
+ { + setSchDrawingFlg(e.target.value) + }} + /> + +
+
+
+
+
+
+ + +
+
+
+
+
+ ) +} diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index d1f97e0e..a248412e 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -790,7 +790,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { setViewLengthText(isView) { this.canvas ?.getObjects() - .filter((obj) => obj.name === 'lengthText' && obj.parent === this) + .filter((obj) => obj.name === 'lengthText' && obj.parentId === this.id) .forEach((text) => { text.set({ visible: isView }) }) @@ -803,9 +803,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.scaleY = scale this.addLengthText() }, - divideLine() { - // splitPolygonWithLines(this) - }, + calcOriginCoords() { const points = this.points const minX = Math.min(...points.map((p) => p.x)) diff --git a/src/components/floor-plan/CanvasMenu.jsx b/src/components/floor-plan/CanvasMenu.jsx index 326bc4f4..e544dbec 100644 --- a/src/components/floor-plan/CanvasMenu.jsx +++ b/src/components/floor-plan/CanvasMenu.jsx @@ -33,6 +33,8 @@ import useMenu from '@/hooks/common/useMenu' import { MENU } from '@/common/common' import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController' +import { estimateState } from '@/store/floorPlanObjectAtom' +import DocDownOptionPop from '../estimate/popup/DocDownOptionPop' export default function CanvasMenu(props) { const { menuNumber, setMenuNumber } = props @@ -53,7 +55,10 @@ export default function CanvasMenu(props) { const canvas = useRecoilValue(canvasState) const { handleZoomClear, handleZoom } = useCanvasEvent() const { handleMenu } = useMenu() + const { handleEstimateSubmit } = useEstimateController() + const estimateRecoilState = useRecoilValue(estimateState) + const [estimatePopupOpen, setEstimatePopupOpen] = useState(false) const { getMessage } = useMessage() const { currentCanvasPlan, saveCanvas } = usePlan() @@ -81,6 +86,9 @@ export default function CanvasMenu(props) { case 4: setType('module') break + case 6: + router.push(`/floor-plan/simulator/${menu.index}`) + break } if (pathname !== '/floor-plan') router.push('/floor-plan') @@ -135,6 +143,38 @@ export default function CanvasMenu(props) { addPopup(id, 1, , true) } + // 견적서 초기화 버튼 + const handleEstimateReset = () => { + // console.log('estimateRecoilState::', estimateRecoilState) + //objectNo, planNo + swalFire({ + //저장된 견적서 정보가 초기화되고, 도면정보가 반영됩니다. 정말로 초기화 하시겠습니까? + //물건정보 + text: getMessage('estimate.detail.reset.confirmMsg'), + type: 'confirm', + confirmFn: () => { + console.log('내용초기화 및 변경일시 갱신') + }, + denyFn: () => { + console.log('초기화하지 않음. 변경일시 갱신안함') + }, + }) + } + + /** + * 견적서 복사버튼 + * (견적서 번호(estimateRecoilState.docNo)가 생성된 이후 버튼 활성화 ) + * T01관리자 계정 및 1차판매점에게만 제공 + */ + + const handleEstimateCopy = () => { + // console.log('estimateRecoilState::', estimateRecoilState) + //objectNo, planNo + console.log('복사') + console.log('물건정보+도면+견적서를 모두 복사') + console.log('견적서 가격은 정가를 표시') + } + useEffect(() => { if (globalLocale === 'ko') { setAppMessageState(KO) @@ -225,7 +265,7 @@ export default function CanvasMenu(props) { {menuNumber === 5 && ( <>
- @@ -233,11 +273,24 @@ export default function CanvasMenu(props) { {getMessage('plan.menu.estimate.save')} - - @@ -263,6 +316,8 @@ export default function CanvasMenu(props) {
{(menuNumber === 2 || menuNumber === 3 || menuNumber === 4) && }
+ {/* 견적서(menuNumber=== 5) 상세화면인경우 문서다운로드 팝업 */} + {estimatePopupOpen && }
) } diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index decb7fa3..c8dbdbca 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -1,14 +1,20 @@ -import SizeGuide from '@/components/floor-plan/modal/placementShape/SizeGuide' -import MaterialGuide from '@/components/floor-plan/modal/placementShape/MaterialGuide' -import WithDraggable from '@/components/common/draggable/WithDraggable' +import { useEffect, useState } from 'react' import { useRecoilState } from 'recoil' -import { Fragment, useEffect, useState } from 'react' + import { canvasSettingState } from '@/store/canvasAtom' +import { basicSettingState } from '@/store/settingAtom' + import { useMessage } from '@/hooks/useMessage' import { useAxios } from '@/hooks/useAxios' import { useSwal } from '@/hooks/useSwal' import { usePopup } from '@/hooks/usePopup' -import { basicSettingState } from '@/store/settingAtom' +import useRefFiles from '@/hooks/common/useRefFiles' +import { usePlan } from '@/hooks/usePlan' + +import SizeGuide from '@/components/floor-plan/modal/placementShape/SizeGuide' +import MaterialGuide from '@/components/floor-plan/modal/placementShape/MaterialGuide' +import WithDraggable from '@/components/common/draggable/WithDraggable' +import { SessionContext } from '@/app/SessionProvider' export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, setShowPlaceShapeModal }) { const [objectNo, setObjectNo] = useState('test123241008001') // 후에 삭제 필요 @@ -18,7 +24,20 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, set const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState) const { closePopup } = usePopup() const [basicSetting, setBasicSettings] = useRecoilState(basicSettingState) - const [image, setImage] = useState(null) + const { + refImage, + queryRef, + setRefImage, + handleRefFile, + refFileMethod, + setRefFileMethod, + handleRefFileMethod, + mapPositionAddress, + setMapPositionAddress, + handleFileDelete, + handleMapImageDown, + } = useRefFiles() + const { currentCanvasPlan } = usePlan() const { getMessage } = useMessage() const { get, post } = useAxios() @@ -487,19 +506,85 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, set {getMessage('common.input.file')} -
+
+
+ handleRefFileMethod(e)} + checked={refFileMethod === '1'} + /> + +
+
+ handleRefFileMethod(e)} + checked={refFileMethod === '2'} + /> + +
+
+ + {/* 파일 불러오기 */} + {refFileMethod === '1' && ( +
+
+ + handleRefFile(e.target.files[0])} /> +
+
+ {currentCanvasPlan?.bgImageName === null ? ( + + ) : ( + + )} + {(refImage || currentCanvasPlan?.bgImageName) && } +
+
+ )} + + {/* 주소 불러오기 */} + {refFileMethod === '2' && ( +
+ setMapPositionAddress(e.target.value)} + /> +
+ +
+ {mapPositionAddress && } + {/* */} +
+ )} + {/*
- setImage(e.target.files[0])} /> + handleRefFile(e.target.files[0])} />
- - {image && } + + {refImage && }
-
+
*/} diff --git a/src/components/management/Stuff.jsx b/src/components/management/Stuff.jsx index 58b1c002..fb7ecd76 100644 --- a/src/components/management/Stuff.jsx +++ b/src/components/management/Stuff.jsx @@ -190,9 +190,11 @@ export default function Stuff() { }) } - if (stuffSearch.schSelSaleStoreId !== '') { - fetchData() - } + //if (session.storeId === 'T01') { + fetchData() + //} else if (stuffSearch.schSelSaleStoreId !== '') { + //fetchData() + //} } else if (stuffSearchParams?.code === 'M') { const params = { saleStoreId: session?.storeId, diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index 61cdc1df..602c6354 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -59,7 +59,7 @@ export default function StuffDetail() { standardWindSpeedId: '', //기준풍속 verticalSnowCover: '', //수직적설량 coldRegionFlg: false, //한랭지대책시행(true : 1 / false : 0) - surfaceType: 'III・IV', //면조도구분(III・IV / Ⅱ) + surfaceType: 'Ⅲ・Ⅳ', //면조도구분(Ⅲ・Ⅳ / Ⅱ) saltAreaFlg: false, //염해지역용아이템사용 (true : 1 / false : 0) installHeight: '', //설치높이 conType: '0', //계약조건(잉여 / 전량) @@ -550,7 +550,7 @@ export default function StuffDetail() { //면조도구분 surfaceType null로 내려오면 셋팅 안하고 저장할때 필수값 체크하도록 // form.setValue('surfaceType', 'Ⅱ') - // form.setValue('surfaceType', 'III・IV') + // form.setValue('surfaceType', 'Ⅲ・Ⅳ') form.setValue('surfaceType', detailData.surfaceType) //염해지역용아이템사용 saltAreaFlg 1이면 true form.setValue('saltAreaFlg', detailData.saltAreaFlg === '1' ? true : false) @@ -872,6 +872,9 @@ export default function StuffDetail() { form.setValue('standardWindSpeedId', `WL_${info.windSpeed}`) form.setValue('verticalSnowCover', info.verticalSnowCover) form.setValue('surfaceType', info.surfaceType) + if (info.surfaceType === 'Ⅱ') { + form.setValue('saltAreaFlg', true) + } form.setValue('installHeight', info.installHeight) form.setValue('remarks', info.remarks) @@ -893,6 +896,15 @@ export default function StuffDetail() { form.setValue('standardWindSpeedId', info.windSpeed) } + //면조도구분surfaceType & 염해지역용아이템사용saltAreaFlg 컨트롤 + const handleRadioChange = (e) => { + if (e.target.value === 'Ⅱ') { + form.setValue('saltAreaFlg', true) + } else { + form.setValue('saltAreaFlg', false) + } + } + //receiveUser: '', //담당자 const _receiveUser = watch('receiveUser') //objectName: '', //물건명 @@ -1561,7 +1573,7 @@ export default function StuffDetail() { onChange={onSelectionChange2} getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} - isDisabled={otherSaleStoreList.length > 1 ? false : true} + isDisabled={otherSaleStoreList.length > 0 ? false : true} isClearable={true} value={otherSaleStoreList.filter(function (option) { return option.saleStoreId === otherSelOptions @@ -1715,11 +1727,29 @@ export default function StuffDetail() {
- - + { + handleRadioChange(e) + }} + /> +
- + { + handleRadioChange(e) + }} + />
@@ -2211,11 +2241,29 @@ export default function StuffDetail() {
- - + { + handleRadioChange(e) + }} + /> +
- + { + handleRadioChange(e) + }} + />
diff --git a/src/components/management/StuffSearchCondition.jsx b/src/components/management/StuffSearchCondition.jsx index 7bd8f456..59a4d23c 100644 --- a/src/components/management/StuffSearchCondition.jsx +++ b/src/components/management/StuffSearchCondition.jsx @@ -184,34 +184,36 @@ export default function StuffSearchCondition() { setSchSelSaleStoreList(allList) setFavoriteStoreList(favList) setShowSaleStoreList(favList) - setSchSelSaleStoreId(session?.storeId) + // setSchSelSaleStoreId(session?.storeId) setStuffSearch({ ...stuffSearch, code: 'S', - schSelSaleStoreId: session?.storeId, + // schSelSaleStoreId: session?.storeId, }) //T01일때 2차 판매점 호출하기 디폴트로 1차점을 본인으로 셋팅해서 세션storeId사용 - url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=0&userId=${session?.userId}` + // 디폴트 셋팅 안하기로 + // url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=0&userId=${session?.userId}` - get({ url: url }).then((res) => { - if (!isEmptyArray(res)) { - res.map((row) => { - row.value = row.saleStoreId - row.label = row.saleStoreName - }) + // get({ url: url }).then((res) => { + // if (!isEmptyArray(res)) { + // res.map((row) => { + // row.value = row.saleStoreId + // row.label = row.saleStoreName + // }) - otherList = res - setOtherSaleStoreList(otherList) - } else { - setOtherSaleStoreList([]) - } - }) + // otherList = res + // setOtherSaleStoreList(otherList) + // } else { + // setOtherSaleStoreList([]) + // } + // }) } else { if (session?.storeLvl === '1') { allList = res favList = res.filter((row) => row.priority !== 'B') otherList = res.filter((row) => row.firstAgentYn === 'N') + setSchSelSaleStoreList(allList) setFavoriteStoreList(allList) setShowSaleStoreList(allList) @@ -584,7 +586,7 @@ export default function StuffSearchCondition() { onChange={onSelectionChange2} getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} - isDisabled={otherSaleStoreList.length > 1 ? false : true} + isDisabled={otherSaleStoreList.length > 0 ? false : true} isClearable={true} value={otherSaleStoreList.filter(function (option) { return option.saleStoreId === otherSaleStoreId diff --git a/src/components/simulator/Simulator.jsx b/src/components/simulator/Simulator.jsx new file mode 100644 index 00000000..c909ca17 --- /dev/null +++ b/src/components/simulator/Simulator.jsx @@ -0,0 +1,365 @@ +'use client' + +import 'chart.js/auto' +import { Bar } from 'react-chartjs-2' +import dayjs from 'dayjs' + +import { useEffect, useState, useRef } from 'react' +import { useRecoilValue } from 'recoil' +import { floorPlanObjectState } from '@/store/floorPlanObjectAtom' + +import { useAxios } from '@/hooks/useAxios' +import { useMessage } from '@/hooks/useMessage' +import { usePlan } from '@/hooks/usePlan' +import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' + +import { convertNumberToPriceDecimal } from '@/util/common-utils' + +export default function Simulator() { + const { plans } = usePlan() + const plan = plans.find((plan) => plan.isCurrent === true) + + const chartRef = useRef(null) + + // recoil 물건번호 + const objectRecoil = useRecoilValue(floorPlanObjectState) + const [objectNo, setObjectNo] = useState('') + + useEffect(() => { + setObjectNo(objectRecoil.floorPlanObjectNo) + }, [objectRecoil]) + + // 캔버스 메뉴 넘버 셋팅 + const { setMenuNumber } = useCanvasMenu() + + useEffect(() => { + setMenuNumber(6) + }, []) + + const { get } = useAxios() + const { getMessage } = useMessage() + + // 차트 관련 + const [chartData, setChartData] = useState([]) + const data = { + labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], + datasets: [ + { + label: 'kWh', + data: chartData.slice(0, 12), + + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(0, 99, 132, 0.2)', + 'rgba(0, 162, 235, 0.2)', + 'rgba(0, 206, 86, 0.2)', + 'rgba(0, 192, 192, 0.2)', + 'rgba(0, 102, 255, 0.2)', + 'rgba(0, 159, 64, 0.2)', + ], + borderColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(255, 206, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(0, 99, 132, 0.2)', + 'rgba(0, 162, 235, 0.2)', + 'rgba(0, 206, 86, 0.2)', + 'rgba(0, 192, 192, 0.2)', + 'rgba(0, 102, 255, 0.2)', + 'rgba(0, 159, 64, 0.2)', + ], + borderWidth: 1, + }, + ], + } + + const options = { + plugins: { + legend: { + position: 'top', + }, + }, + scales: { + x: { + grid: { + display: false, + }, + }, + y: { + beginAtZero: true, + grid: { + display: true, + }, + }, + }, + } + + useEffect(() => { + if (objectNo) { + fetchObjectDetail(objectNo) + } + fetchSimulatorNotice() + }, [objectNo, plan]) + + // 물건 상세 정보 조회 + const [objectDetail, setObjectDetail] = useState({}) + + // 모듈배치정보 조회 + const [moduleInfoList, setModuleInfoList] = useState([]) + + // 파워컨디셔너 조회 + const [pcsInfoList, setPcsInfoList] = useState([]) + + const fetchObjectDetail = async (objectNo) => { + const apiUrl = `/api/pwrGnrSimulation/calculations?objectNo=${objectNo}&planNo=${plan?.id}` + const resultData = await get({ url: apiUrl }) + if (resultData) { + setObjectDetail(resultData) + if (resultData.frcPwrGnrList) { + setChartData(resultData.frcPwrGnrList) + } + if (resultData.pcsList) { + setPcsInfoList(resultData.pcsList) + } + if (resultData.roofModuleList) { + setModuleInfoList(resultData.roofModuleList) + } + } + } + + // 시뮬레이션 안내사항 조회 + const [content, setContent] = useState('') + + const fetchSimulatorNotice = async () => { + get({ url: '/api/pwrGnrSimulation/guideInfo' }).then((res) => { + if (res.data) { + setContent(res.data.replaceAll('\n', '
')) + } else { + setContent(getMessage('common.message.no.data')) + } + }) + } + + return ( +
+
+
+
+
+ {/* 물건번호 */} +
+
{getMessage('simulator.title.sub1')}
+
+ {objectDetail.objectNo} (Plan No: {objectDetail.planNo}) +
+
+ {/* 작성일 */} +
+
{getMessage('simulator.title.sub2')}
+
{`${dayjs(objectDetail.drawingEstimateCreateDate).format('YYYY.MM.DD')}`}
+
+ {/* 시스템용량 */} +
+
{getMessage('simulator.title.sub3')}
+
{convertNumberToPriceDecimal(objectDetail.capacity)}kW
+
+ {/* 연간예측발전량 */} +
+
{getMessage('simulator.title.sub4')}
+
{convertNumberToPriceDecimal(objectDetail.anlFrcsGnrt)}
+
+
+
+ {/* 도도부현 */} +
+
{getMessage('simulator.title.sub5')}
+
{objectDetail.prefName}
+
+ {/* 일사량 관측지점 */} +
+
{getMessage('simulator.title.sub6')}
+
{objectDetail.areaName}
+
+ {/* 적설조건 */} +
+
{getMessage('simulator.title.sub7')}
+
{objectDetail.snowfall}
+
+ {/* 풍속조건 */} +
+
{getMessage('simulator.title.sub8')}
+
{objectDetail.standardWindSpeedId}
+
+
+
+
+
+
+
+
+
+ +
+
+
+

{getMessage('simulator.table.sub9')}

+
+
+ {/* 예측발전량 */} +
+ + + + + + + + + + + + + + + + + + + + {chartData.length > 0 ? ( + + {chartData.map((data) => ( + + ))} + + ) : ( + + + + )} + +
1月2月3月4月5月6月7月8月9月10月11月12月{getMessage('simulator.table.sub6')}
{convertNumberToPriceDecimal(data)}
{getMessage('common.message.no.data')}
+
+
+
+
+
+
+ + + + + + + + + + + + {moduleInfoList.length > 0 ? ( + moduleInfoList.map((moduleInfo) => { + return ( + <> + + {/* 지붕면 */} + + {/* 경사각 */} + + {/* 방위각(도) */} + + {/* 태양전지모듈 */} + + {/* 매수 */} + + + + ) + }) + ) : ( + + + + )} + +
{getMessage('simulator.table.sub1')}{getMessage('simulator.table.sub2')}{getMessage('simulator.table.sub3')}{getMessage('simulator.table.sub4')}{getMessage('simulator.table.sub5')}
{moduleInfo.roofSurface}{convertNumberToPriceDecimal(moduleInfo.slope)}寸{convertNumberToPriceDecimal(moduleInfo.angle)} +
{moduleInfo.itemNo}
+
{moduleInfo.amount}
{getMessage('common.message.no.data')}
+ {moduleInfoList.length > 0 && ( +
+
{getMessage('simulator.table.sub6')}
+
+ {moduleInfoList.reduce((acc, moduleInfo) => convertNumberToPriceDecimal(Number(acc) + Number(moduleInfo.amount)), 0)} +
+
+ )} +
+
+
+
+ + + + + + + + + {pcsInfoList.length > 0 ? ( + pcsInfoList.map((pcsInfo) => { + return ( + <> + + {/* 파워컨디셔너 */} + + {/* 대 */} + + + + ) + }) + ) : ( + + + + )} + +
{getMessage('simulator.table.sub7')}{getMessage('simulator.table.sub8')}
+
{pcsInfo.itemNo}
+
{pcsInfo.amount}
{getMessage('common.message.no.data')}
+
+
+
+
+
+
+
+
+
+ + {getMessage('simulator.notice.sub1')} +
+ {getMessage('simulator.notice.sub2')} +
+
+ {/* 시뮬레이션 안내사항 */} +
+
+
+
+
+
+ ) +} diff --git a/src/hooks/common/useRefFiles.js b/src/hooks/common/useRefFiles.js new file mode 100644 index 00000000..1b36a109 --- /dev/null +++ b/src/hooks/common/useRefFiles.js @@ -0,0 +1,112 @@ +import { useRef, useState } from 'react' +import { useRecoilState } from 'recoil' +import { v4 as uuidv4 } from 'uuid' + +import { useSwal } from '@/hooks/useSwal' +import { convertDwgToPng } from '@/lib/cadAction' +import { useAxios } from '../useAxios' +import { currentCanvasPlanState } from '@/store/canvasAtom' + +export default function useRefFiles() { + const converterUrl = process.env.NEXT_PUBLIC_CONVERTER_API_URL + const [refImage, setRefImage] = useState(null) + const [refFileMethod, setRefFileMethod] = useState('1') + const [mapPositionAddress, setMapPositionAddress] = useState('') + const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState) + const queryRef = useRef(null) + + const { swalFire } = useSwal() + const { get, promisePut } = useAxios() + // const { currentCanvasPlan, setCurrentCanvasPlan } = usePlan() + + /** + * 파일 불러오기 버튼 컨트롤 + * @param {*} file + */ + const handleRefFile = (file) => { + setRefImage(file) + file.name.split('.').pop() === 'dwg' ? handleUploadRefFile(file) : () => {} + // handleUploadRefFile(file) + } + + /** + * 파일 삭제 + */ + const handleFileDelete = () => { + setRefImage(null) + setCurrentCanvasPlan((prev) => ({ ...prev, bgFileName: null })) + } + + /** + * 주소 삭제 + */ + const handleAddressDelete = () => { + setCurrentCanvasPlan((prev) => ({ ...prev, mapPositionAddress: null })) + } + + /** + * 주소로 구글 맵 이미지 다운로드 + */ + const handleMapImageDown = async () => { + if (queryRef.current.value === '' || queryRef.current.value === null) { + return + } + + const res = await get({ url: `http://localhost:3000/api/html2canvas?q=${queryRef.current.value}&fileNm=${uuidv4()}&zoom=20` }) + console.log('🚀 ~ handleMapImageDown ~ res:', res) + setCurrentCanvasPlan((prev) => ({ ...prev, bgFileName: res.fileNm, mapPositionAddress: queryRef.current.value })) + } + + /** + * 현재 플랜이 변경되면 플랜 상태 저장 + */ + useEffect(() => { + const handleCurrentPlan = async () => { + await promisePut({ url: '/api/canvas-management/canvas-statuses', data: currentCanvasPlan }).then((res) => { + console.log('🚀 ~ awaitpromisePut ~ res:', res) + }) + } + handleCurrentPlan() + }, [currentCanvasPlan]) + + /** + * RefFile이 캐드 도면 파일일 경우 변환하여 이미지로 저장 + * @param {*} file + */ + const handleUploadRefFile = async (file) => { + const formData = new FormData() + formData.append('file', file) + + await promisePost({ url: converterUrl, data: formData }) + .then((res) => { + convertDwgToPng(res.data.Files[0].FileName, res.data.Files[0].FileData) + swalFire({ text: '파일 변환 성공' }) + }) + .catch((err) => { + swalFire({ text: '파일 변환 실패', icon: 'error' }) + }) + } + + /** + * 라디오 버튼 컨트롤 + * @param {*} e + */ + const handleRefFileMethod = (e) => { + setRefFileMethod(e.target.value) + } + + return { + refImage, + queryRef, + setRefImage, + handleRefFile, + refFileMethod, + setRefFileMethod, + mapPositionAddress, + setMapPositionAddress, + handleRefFileMethod, + handleFileDelete, + handleAddressDelete, + handleMapImageDown, + } +} diff --git a/src/hooks/floorPlan/estimate/useEstimateController.js b/src/hooks/floorPlan/estimate/useEstimateController.js index 2b92a14a..5a0c49e1 100644 --- a/src/hooks/floorPlan/estimate/useEstimateController.js +++ b/src/hooks/floorPlan/estimate/useEstimateController.js @@ -5,6 +5,7 @@ import { globalLocaleStore } from '@/store/localeAtom' import { estimateState, floorPlanObjectState } from '@/store/floorPlanObjectAtom' import { isObjectNotEmpty } from '@/util/common-utils' import { SessionContext } from '@/app/SessionProvider' +import { useMessage } from '@/hooks/useMessage' const reducer = (prevState, nextState) => { return { ...prevState, ...nextState } @@ -21,35 +22,33 @@ const defaultEstimateData = { estimateType: 'YJOD', //주문분류 remarks: '', //비고 estimateOption: '', //견적특이사항 - // itemList: [{ id: 1, name: '' }], //아이템에 필요없는거 빼기 itemList: [ - { - amount: '', - fileUploadFlg: '', - itemChangeFlg: '', - itemGroup: '', - itemId: '', //키값?? - itemName: '', - itemNo: '', - moduleFlg: '', - objectNo: '', - pkgMaterialFlg: '', - planNo: '', - pnowW: '', - salePrice: '', - saleTotPrice: '', - specification: '', - unit: '', - }, + // { + // amount: '', + // fileUploadFlg: '', + // itemChangeFlg: '', + // itemGroup: '', + // itemId: '', //키값?? + // itemName: '', + // itemNo: '', + // moduleFlg: '', + // objectNo: '', + // pkgMaterialFlg: '', + // planNo: '', + // pnowW: '', + // salePrice: '', + // saleTotPrice: '', + // specification: '', + // unit: '', + // }, ], fileList: [], + fileFlg: '0', //후일 자료 제출 (체크 1 노체크 0) } // Helper functions -// const updateItemInList = (itemList, id, updates) => { const updateItemInList = (itemList, itemId, updates) => { - // return itemList.map((item) => (item.id === id ? { ...item, ...updates } : item)) return itemList.map((item) => (item.itemId === itemId ? { ...item, ...updates } : item)) } @@ -59,6 +58,8 @@ export const useEstimateController = (planNo) => { const objectRecoil = useRecoilValue(floorPlanObjectState) const [estimateData, setEstimateData] = useRecoilState(estimateState) + const { getMessage } = useMessage() + const { get, post, promisePost } = useAxios(globalLocaleState) const [isLoading, setIsLoading] = useState(false) @@ -87,20 +88,15 @@ export const useEstimateController = (planNo) => { } } - // const updateItem = (id, updates) => { const updateItem = (itemId, updates) => { setState({ - // itemList: updateItemInList(state.itemList, id, updates), itemList: updateItemInList(state.itemList, itemId, updates), }) } const addItem = () => { - // const newId = Math.max(...state.itemList.map((item) => item.id)) + 1 const newItemId = Math.max(...state.itemList.map((item) => item.itemId)) + 1 setState({ - // itemList: [...state.itemList, { id: newId, name: '' }], - //셋팅할필요없는거 빼기 itemList: [ ...state.itemList, { @@ -126,42 +122,56 @@ export const useEstimateController = (planNo) => { } useEffect(() => { - setEstimateData({ ...state, userId: session.userId }) - //sapSalesStoreCd 추가예정 필수값 - // setEstimateData({ ...state, userId: session.userId, sapSalesStoreCd : session.sapSalesStoreCd }) + setEstimateData({ ...state, userId: session.userId, sapSalesStoreCd: session.custCd }) }, [state]) //견적서 저장 const handleEstimateSubmit = async () => { + //0. 필수체크 + let flag = true console.log('::담긴 estimateData:::', estimateData) - //1. 첨부파일 저장 - const formData = new FormData() - formData.append('file', estimateData.fileList) - formData.append('objectNo', estimateData.objectNo) - formData.append('planNo', estimateData.planNo) - formData.append('category', '10') - formData.append('userId', estimateData.userId) - for (const value of formData.values()) { - console.log('formData::', value) - } - - await promisePost({ url: '/api/file/fileUpload', data: formData }).then((res) => { - console.log('파일저장::::::::::', res) - }) - - //2. 상세데이터 저장 - - console.log('상세저장시작!!') - return - try { - const result = await promisePost({ - url: ESTIMATE_API_ENDPOINT, - data: estimateData, + //아이템 fileUploadFlg가1(첨부파일 필수)이 1개라도 있는데 후일 자료 제출(fileFlg) 체크안했으면(0) alert 저장안돼 + if (estimateData.itemList.length > 1) { + estimateData.itemList.map((row) => { + if (row.fileUploadFlg === '1') { + if (estimateData.fileFlg === '0') { + alert(getMessage('estimate.detail.save.requiredMsg')) + flag = false + } + } }) - return result - } catch (error) { - console.error('Failed to submit estimate:', error) - throw error + } + if (flag) { + //1. 첨부파일 저장 + const formData = new FormData() + formData.append('file', estimateData.fileList) + formData.append('objectNo', estimateData.objectNo) + formData.append('planNo', estimateData.planNo) + formData.append('category', '10') + formData.append('userId', estimateData.userId) + for (const value of formData.values()) { + console.log('formData::', value) + } + + await promisePost({ url: '/api/file/fileUpload', data: formData }).then((res) => { + console.log('파일저장결과::::::::::', res) + }) + + //2. 상세데이터 저장 + + console.log('상세저장시작!!') + return + try { + const result = await promisePost({ + url: ESTIMATE_API_ENDPOINT, + data: estimateData, + }) + alert(getMessage('estimate.detail.save.alertMsg')) + return result + } catch (error) { + console.error('Failed to submit estimate:', error) + throw error + } } } diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index 02691887..549cc9a4 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -5,7 +5,7 @@ import { globalLocaleStore } from '@/store/localeAtom' import { useMessage } from '@/hooks/useMessage' import { useAxios } from '@/hooks/useAxios' import { useSwal } from '@/hooks/useSwal' -import { settingModalFirstOptionsState, settingModalSecondOptionsState } from '@/store/settingAtom' +import { corridorDimensionSelector, settingModalFirstOptionsState, settingModalSecondOptionsState } from '@/store/settingAtom' import { setSurfaceShapePattern } from '@/util/canvas-util' import { POLYGON_TYPE } from '@/common/common' @@ -17,6 +17,8 @@ export function useCanvasSetting() { const { option1, option2, dimensionDisplay } = settingModalFirstOptions const { option3, option4 } = settingModalSecondOptions + const corridorDimension = useRecoilValue(corridorDimensionSelector) + const globalLocaleState = useRecoilValue(globalLocaleStore) const { get, post } = useAxios(globalLocaleState) const { getMessage } = useMessage() @@ -27,6 +29,36 @@ export function useCanvasSetting() { const [objectNo, setObjectNo] = useState('test123240912001') // 이후 삭제 필요 + useEffect(() => { + if (!canvas) { + return + } + const { column } = corridorDimension + const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText') + switch (column) { + case 'corridorDimension': + lengthTexts.forEach((obj) => { + if (obj.planeSize) { + obj.set({ text: obj.planeSize.toString() }) + } + }) + break + case 'realDimension': + lengthTexts.forEach((obj) => { + if (obj.actualSize) { + obj.set({ text: obj.actualSize.toString() }) + } + }) + break + case 'noneDimension': + lengthTexts.forEach((obj) => { + obj.set({ text: '' }) + }) + break + } + canvas.renderAll() + }, [corridorDimension]) + useEffect(() => { console.log('useCanvasSetting useEffect 실행1') fetchSettings() @@ -257,7 +289,7 @@ export function useCanvasSetting() { optionName = ['7'] break case 'flowDisplay': //흐름방향 표시 - optionName = ['arrow'] + optionName = ['arrow', 'flowText'] break case 'trestleDisplay': //가대 표시 optionName = ['8'] diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 84c4fac6..ce7b8210 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1782,8 +1782,13 @@ export function useMode() { } wall.lines.forEach((line, index) => { + const lineLength = Math.sqrt( + Math.pow(Math.round(Math.abs(line.x1 - line.x2) * 10), 2) + Math.pow(Math.round(Math.abs(line.y1 - line.y2) * 10), 2), + ) line.attributes.roofId = roof.id line.attributes.currentRoof = roof.lines[index].id + line.attributes.planeSize = lineLength + line.attributes.actualSize = lineLength }) setRoof(roof) diff --git a/src/hooks/usePlan.js b/src/hooks/usePlan.js index d1887f6f..ab67452e 100644 --- a/src/hooks/usePlan.js +++ b/src/hooks/usePlan.js @@ -187,6 +187,8 @@ export function usePlan() { userId: item.userId, canvasStatus: dbToCanvasFormat(item.canvasStatus), isCurrent: false, + bgImageName: item.bgImageName, + mapPositionAddress: item.mapPositionAddress, })), ) } diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 9ea8b3b7..699b4f09 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -693,8 +693,7 @@ export const usePolygon = () => { } } }) - - const direction = newRoofs.length === 1 ? polygon.direction : representLine.direction + const direction = polygon.direction ?? representLine.direction const polygonDirection = polygon.direction switch (direction) { @@ -723,7 +722,7 @@ export const usePolygon = () => { originY: 'center', selectable: true, defense: defense, - direction: newRoofs.length === 1 ? polygonDirection : defense, + direction: polygonDirection ?? defense, pitch: pitch, }) diff --git a/src/locales/ja.json b/src/locales/ja.json index 63855bc6..15f2473b 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -824,7 +824,7 @@ "estimate.detail.estimateType": "注文分類", "estimate.detail.roofCns": "屋根材・仕様施工", "estimate.detail.remarks": "備考", - "estimate.detail.nextSubmit": "後日資料提出", + "estimate.detail.fileFlg": "後日資料提出", "estimate.detail.header.fileList1": "ファイル添付", "estimate.detail.fileList.btn": "ファイル選択", "estimate.detail.header.fileList2": "添付ファイル一覧", @@ -841,11 +841,60 @@ "estimate.detail.sepcialEstimateProductInfo.calcFormula1": "(モジュール容量 × 数量)÷1000", "estimate.detail.sepcialEstimateProductInfo.calcFormula2": "PKG単価 (W)×PKG容量(W)", "estimate.detail.header.showPrice": "価格表示", - "estimate.detail.showPrice.btn1": "Pricing", + "estimate.detail.header.unitPrice": "定価", + "estimate.detail.showPrice.pricingBtn": "Pricing", "estimate.detail.showPrice.description1": "製品価格 OPEN", "estimate.detail.showPrice.description2": "追加, 変更資材", "estimate.detail.showPrice.description3": "添付必須", "estimate.detail.showPrice.description4": "クリックして製品の特異性を確認する", "estimate.detail.showPrice.btn2": "製品を追加", - "estimate.detail.showPrice.btn3": "製品削除" + "estimate.detail.showPrice.btn3": "製品削除", + "estimate.detail.itemTableHeader.col1": "アイテム", + "estimate.detail.itemTableHeader.col2": "品番", + "estimate.detail.itemTableHeader.col3": "型板", + "estimate.detail.itemTableHeader.col4": "数量", + "estimate.detail.itemTableHeader.col5": "単位", + "estimate.detail.itemTableHeader.col6": "単価", + "estimate.detail.itemTableHeader.col7": "金額 (税別別)", + "estimate.detail.docPopup.title": "ドキュメントダウンロードオプションの設定", + "estimate.detail.docPopup.explane": "ダウンロードする文書のオプションを選択したら、 [文書のダウンロード]ボタンをクリックします.", + "estimate.detail.docPopup.schUnitPriceFlg": "ダウンロードファイル", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg0": "見積もり Excel", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg1": "定価用 Excel", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg2": "見積もり PDF", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg3": "定価用 PDF", + "estimate.detail.docPopup.schDisplayFlg": "見積提出先表示名", + "estimate.detail.docPopup.schDisplayFlg.schDisplayFlg0": "販売店名", + "estimate.detail.docPopup.schDisplayFlg.schDisplayFlg1": "案件名", + "estimate.detail.docPopup.schWeightFlg": "架台重量表を含む", + "estimate.detail.docPopup.schWeightFlg.schWeightFlg0": "含む", + "estimate.detail.docPopup.schWeightFlg.schWeightFlg1": "含まない", + "estimate.detail.docPopup.schDrawingFlg": "図面/シミュレーションファイルを含む", + "estimate.detail.docPopup.schDrawingFlg.schDrawingFlg0": "含む", + "estimate.detail.docPopup.schDrawingFlg.schDrawingFlg1": "含まない", + "estimate.detail.docPopup.close": "閉じる", + "estimate.detail.docPopup.docDownload": "文書のダウンロード", + "estimate.detail.save.alertMsg": "保存されている見積書で製品を変更した場合、図面や回路には反映されません.", + "estimate.detail.save.requiredMsg": "ファイル添付が必須のアイテムがあります。ファイルを添付するか、後日添付をチェックしてください.", + "estimate.detail.reset.confirmMsg": "保存した見積書情報が初期化され、図面情報が反映されます。本当に初期化しますか?", + "simulator.title.sub1": "物件番号", + "simulator.title.sub2": "作成日", + "simulator.title.sub3": "システム容量", + "simulator.title.sub4": "年間予測発電量", + "simulator.title.sub5": "都道府県", + "simulator.title.sub6": "日射量観測地点", + "simulator.title.sub7": "積雪条件", + "simulator.title.sub8": "風速条件", + "simulator.title.sub9": "以下", + "simulator.table.sub1": "屋根面", + "simulator.table.sub2": "傾斜角", + "simulator.table.sub3": "方位角(度)", + "simulator.table.sub4": "太陽電池モジュール", + "simulator.table.sub5": "枚数", + "simulator.table.sub6": "合計", + "simulator.table.sub7": "パワーコンディショナー", + "simulator.table.sub8": "台", + "simulator.table.sub9": "予測発電量 (kWh)", + "simulator.notice.sub1": "Hanwha Japan 年間発電量", + "simulator.notice.sub2": "シミュレーション案内事項" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 4f9ab2ae..d52334bf 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -830,7 +830,7 @@ "estimate.detail.estimateType": "주문분류", "estimate.detail.roofCns": "지붕재・사양시공", "estimate.detail.remarks": "비고", - "estimate.detail.nextSubmit": "후일자료제출", + "estimate.detail.fileFlg": "후일자료제출", "estimate.detail.header.fileList1": "파일첨부", "estimate.detail.fileList.btn": "파일선택", "estimate.detail.header.fileList2": "첨부파일 목록", @@ -847,11 +847,60 @@ "estimate.detail.sepcialEstimateProductInfo.calcFormula1": "(모듈수량 * 수량)÷100", "estimate.detail.sepcialEstimateProductInfo.calcFormula2": "PKG단가(W) * PKG용량(W)", "estimate.detail.header.showPrice": "가격표시", - "estimate.detail.showPrice.btn1": "Pricing", + "estimate.detail.header.unitPrice": "정가", + "estimate.detail.showPrice.pricingBtn": "Pricing", "estimate.detail.showPrice.description1": "제품 가격 OPEN", "estimate.detail.showPrice.description2": "추가, 변경 자재", "estimate.detail.showPrice.description3": "첨부필수", "estimate.detail.showPrice.description4": "클릭하여 제품 특이사항 확인", "estimate.detail.showPrice.btn2": "제품추가", - "estimate.detail.showPrice.btn3": "제품삭제" + "estimate.detail.showPrice.btn3": "제품삭제", + "estimate.detail.itemTableHeader.col1": "Item", + "estimate.detail.itemTableHeader.col2": "품번", + "estimate.detail.itemTableHeader.col3": "형명", + "estimate.detail.itemTableHeader.col4": "수량", + "estimate.detail.itemTableHeader.col5": "단위", + "estimate.detail.itemTableHeader.col6": "단가", + "estimate.detail.itemTableHeader.col7": "금액(부가세별도)", + "estimate.detail.docPopup.title": "문서다운로드 옵션설정", + "estimate.detail.docPopup.explane": "다운로드할 문서 옵션을 선택한 후 문서 다운로드 버튼을 클릭합니다.", + "estimate.detail.docPopup.schUnitPriceFlg": "다운로드 파일", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg0": "견적가 Excel", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg1": "정가용 Excel", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg2": "견적가 PDF", + "estimate.detail.docPopup.schUnitPriceFlg.schUnitPriceFlg3": "정가용 PDF", + "estimate.detail.docPopup.schDisplayFlg": "견적제출서 표시명", + "estimate.detail.docPopup.schDisplayFlg.schDisplayFlg0": "판매점명", + "estimate.detail.docPopup.schDisplayFlg.schDisplayFlg1": "안건명", + "estimate.detail.docPopup.schWeightFlg": "가대 중량표 포함", + "estimate.detail.docPopup.schWeightFlg.schWeightFlg0": "포함", + "estimate.detail.docPopup.schWeightFlg.schWeightFlg1": "미포함", + "estimate.detail.docPopup.schDrawingFlg": "도면/시뮬레이션 파일 포함", + "estimate.detail.docPopup.schDrawingFlg.schDrawingFlg0": "포함", + "estimate.detail.docPopup.schDrawingFlg.schDrawingFlg1": "미포함", + "estimate.detail.docPopup.close": "닫기", + "estimate.detail.docPopup.docDownload": "문서 다운로드", + "estimate.detail.save.alertMsg": "저장되었습니다. 견적서에서 제품을 변경할 경우, 도면 및 회로에 반영되지 않습니다.", + "estimate.detail.save.requiredMsg": "파일첨부가 필수인 아이템이 있습니다. 파일을 첨부하거나 후일첨부를 체크해주십시오.", + "estimate.detail.reset.confirmMsg": "저장된 견적서 정보가 초기화되고, 도면정보가 반영됩니다. 정말로 초기화 하시겠습니까?", + "simulator.title.sub1": "물건번호", + "simulator.title.sub2": "작성일", + "simulator.title.sub3": "시스템 용량", + "simulator.title.sub4": "연간예측발전량", + "simulator.title.sub5": "도도부현", + "simulator.title.sub6": "일사량 관측지점", + "simulator.title.sub7": "적설조건", + "simulator.title.sub8": "풍속조건", + "simulator.title.sub9": "이하", + "simulator.table.sub1": "지붕면", + "simulator.table.sub2": "경사각", + "simulator.table.sub3": "방위각(도)", + "simulator.table.sub4": "태양전지모듈", + "simulator.table.sub5": "매수", + "simulator.table.sub6": "합계", + "simulator.table.sub7": "파워 컨디셔너", + "simulator.table.sub8": "대", + "simulator.table.sub9": "예측발전량 (kWh)", + "simulator.notice.sub1": "Hanwha Japan 연간 발전량", + "simulator.notice.sub2": "시뮬레이션 안내사항" } diff --git a/src/store/settingAtom.js b/src/store/settingAtom.js index fede515e..869cc004 100644 --- a/src/store/settingAtom.js +++ b/src/store/settingAtom.js @@ -185,6 +185,7 @@ export const corridorDimensionSelector = selector({ const settingModalFirstOptions = get(settingModalFirstOptionsState) return settingModalFirstOptions.dimensionDisplay.find((option) => option.selected) }, + dangerouslyAllowMutability: true, }) // 디스플레이 설정 - 화면 표시 diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index d84330e8..679562ec 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -1,13 +1,6 @@ import { fabric } from 'fabric' import { QLine } from '@/components/fabric/QLine' -import { - calculateIntersection, - distanceBetweenPoints, - findClosestPoint, - getDegreeByChon, - getDirectionByPoint, - isPointOnLine, -} from '@/util/canvas-util' +import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDegreeByChon, getDirectionByPoint } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' import * as turf from '@turf/turf' @@ -1307,14 +1300,19 @@ const drawRidge = (roof, canvas) => { prevRoof = index === 0 ? wallLines[wallLines.length - 1] : wallLines[index - 1] nextRoof = index === wallLines.length - 1 ? wallLines[0] : index === wallLines.length ? wallLines[1] : wallLines[index + 1] - if (prevRoof.direction !== nextRoof.direction && currentWall.length <= currentRoof.length) { - ridgeRoof.push({ index: index, roof: currentRoof, length: currentRoof.length }) + const angle1 = calculateAngle(prevRoof.startPoint, prevRoof.endPoint) + const angle2 = calculateAngle(nextRoof.startPoint, nextRoof.endPoint) + + if (Math.abs(angle1 - angle2) === 180 && currentWall.attributes.planeSize <= currentRoof.attributes.planeSize) { + ridgeRoof.push({ index: index, roof: currentRoof, length: currentRoof.attributes.planeSize }) } }) // 지붕의 길이가 짧은 순으로 정렬 ridgeRoof.sort((a, b) => a.length - b.length) + console.log('ridgeRoof', ridgeRoof) + ridgeRoof.forEach((item) => { if (getMaxRidge(roofLines.length) > roof.ridges.length) { let index = item.index, @@ -1336,28 +1334,23 @@ const drawRidge = (roof, canvas) => { let xEqualInnerLines = anotherRoof.filter((roof) => roof.x1 === roof.x2 && isInnerLine(prevRoof, currentRoof, nextRoof, roof)), //x가 같은 내부선 yEqualInnerLines = anotherRoof.filter((roof) => roof.y1 === roof.y2 && isInnerLine(prevRoof, currentRoof, nextRoof, roof)) //y가 같은 내부선 - let ridgeBaseLength = Math.round((currentRoof.length / 2) * 10) / 10, // 지붕의 기반 길이 - ridgeMaxLength = Math.min(prevRoof.length, nextRoof.length), // 지붕의 최대 길이. 이전, 다음 벽 중 짧은 길이 - ridgeAcrossLength = Math.round((ridgeMaxLength - currentRoof.length) * 10) / 10 // 맞은편 벽까지의 길이 - 지붕의 기반 길이 + let ridgeBaseLength = Math.round(currentRoof.attributes.planeSize / 2), // 지붕의 기반 길이 + ridgeMaxLength = Math.min(prevRoof.attributes.planeSize, nextRoof.attributes.planeSize), // 지붕의 최대 길이. 이전, 다음 벽 중 짧은 길이 + ridgeAcrossLength = Math.abs(Math.max(prevRoof.attributes.planeSize, nextRoof.attributes.planeSize) - currentRoof.attributes.planeSize) // 맞은편 벽까지의 길이 - 지붕의 기반 길이 let acrossRoof = anotherRoof .filter((roof) => { - if (roof.x1 === roof.x2) { - if ((nextRoof.direction === 'right' && roof.x1 > currentRoof.x1) || (nextRoof.direction === 'left' && roof.x1 < currentRoof.x1)) { - return roof - } - } - if (roof.y1 === roof.y2) { - if ((nextRoof.direction === 'top' && roof.y1 < currentRoof.y1) || (nextRoof.direction === 'bottom' && roof.y1 > currentRoof.y1)) { - return roof - } + const angle1 = calculateAngle(currentRoof.startPoint, currentRoof.endPoint) + const angle2 = calculateAngle(roof.startPoint, roof.endPoint) + if (Math.abs(angle1 - angle2) === 180) { + return roof } }) .reduce((prev, current) => { let hasBetweenRoof = false if (current.x1 === current.x2) { hasBetweenRoof = roofLines - .filter((roof) => roof !== current && roof !== currentRoof) + .filter((roof) => roof !== current) .some((line) => { let currentY2 = currentRoof.y2 if (yEqualInnerLines.length > 0) { @@ -1369,12 +1362,13 @@ const drawRidge = (roof, canvas) => { const isY2Between = (line.y2 > currentRoof.y1 && line.y2 < currentY2) || (line.y2 > currentY2 && line.y2 < currentRoof.y1) const isX1Between = (line.x1 > currentRoof.x1 && line.x1 < current.x1) || (line.x1 > currentRoof.x1 && line.x1 < current.x1) const isX2Between = (line.x2 > currentRoof.x1 && line.x2 < current.x1) || (line.x2 > currentRoof.x1 && line.x2 < current.x1) + return isY1Between && isY2Between && isX1Between && isX2Between }) } if (current.y1 === current.y2) { - hasBetweenRoof = wallLines - .filter((roof) => roof !== current && roof !== currentRoof) + hasBetweenRoof = roofLines + .filter((roof) => roof !== current) .some((line) => { let currentX2 = currentRoof.x2 if (xEqualInnerLines.length > 0) { @@ -1412,20 +1406,23 @@ const drawRidge = (roof, canvas) => { } } }, undefined) - if (acrossRoof !== undefined) { if (currentRoof.x1 === currentRoof.x2) { if (ridgeAcrossLength < Math.abs(currentRoof.x1 - acrossRoof.x1)) { - ridgeAcrossLength = Math.round((Math.round(Math.abs(currentRoof.x1 - acrossRoof.x1) * 10) / 10 - currentRoof.length) * 10) / 10 + ridgeAcrossLength = Math.round(Math.round(Math.abs(currentRoof.x1 - acrossRoof.x1) * 10) - currentRoof.attributes.planeSize) } } if (currentRoof.y1 === currentRoof.y2) { if (ridgeAcrossLength < Math.abs(currentRoof.y1 - acrossRoof.y1)) { - ridgeAcrossLength = Math.round((Math.round(Math.abs(currentRoof.y1 - acrossRoof.y1) * 10) / 10 - currentRoof.length) * 10) / 10 + ridgeAcrossLength = Math.round(Math.round(Math.abs(currentRoof.y1 - acrossRoof.y1) * 10) - currentRoof.attributes.planeSize) } } } + ridgeBaseLength = ridgeBaseLength / 10 + ridgeMaxLength = ridgeMaxLength / 10 + ridgeAcrossLength = ridgeAcrossLength / 10 + if (ridgeBaseLength > 0 && ridgeMaxLength > 0 && ridgeAcrossLength > 0) { let ridgeLength = Math.min(ridgeMaxLength, ridgeAcrossLength) if (currentRoof.x1 === currentRoof.x2) { diff --git a/yarn.lock b/yarn.lock index a167fa9d..ed2cceda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -527,6 +527,11 @@ resolved "https://registry.npmjs.org/@js-joda/core/-/core-5.6.3.tgz" integrity sha512-T1rRxzdqkEXcou0ZprN1q9yDRlvzCPLqmlNt5IIsGBzoEVgLCCYrKEwc84+TvsXuAc95VAZwtWD2zVsKPY4bcA== +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" @@ -4337,6 +4342,13 @@ chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chart.js@^4.4.6: + version "4.4.6" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.6.tgz#da39b84ca752298270d4c0519675c7659936abec" + integrity sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA== + dependencies: + "@kurkle/color" "^0.3.0" + "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.6.0" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz" @@ -5832,6 +5844,11 @@ rbush@^3.0.1: dependencies: quickselect "^2.0.0" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-color-palette@^7.2.2: version "7.2.2" resolved "https://registry.npmjs.org/react-color-palette/-/react-color-palette-7.2.2.tgz"