450 lines
16 KiB
JavaScript
450 lines
16 KiB
JavaScript
'use client'
|
|
|
|
import 'chart.js/auto'
|
|
import { Bar } from 'react-chartjs-2'
|
|
import dayjs from 'dayjs'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
import { useEffect, useState, useRef, useContext } from 'react'
|
|
import { useRecoilState } from 'recoil'
|
|
import { FloorPlanContext } from '@/app/floor-plan/FloorPlanProvider'
|
|
import { pwrGnrSimTypeState } from '@/store/simulatorAtom'
|
|
|
|
import { useAxios } from '@/hooks/useAxios'
|
|
import { useMessage } from '@/hooks/useMessage'
|
|
import { useCanvasMenu } from '@/hooks/common/useCanvasMenu'
|
|
|
|
import { convertNumberToPriceDecimal } from '@/util/common-utils'
|
|
import { usePlan } from '@/hooks/usePlan'
|
|
import { usePopup } from '@/hooks/usePopup'
|
|
// import { useSearchParams } from 'next/navigation'
|
|
|
|
export default function Simulator() {
|
|
const { floorPlanState } = useContext(FloorPlanContext)
|
|
const { objectNo, pid } = floorPlanState
|
|
|
|
// const searchParams = useSearchParams()
|
|
// const objectNo = searchParams.get('objectNo')
|
|
// const pid = searchParams.get('pid')
|
|
const { selectedPlan } = usePlan()
|
|
|
|
const chartRef = useRef(null)
|
|
|
|
// 캔버스 메뉴 넘버 셋팅
|
|
const { setMenuNumber } = useCanvasMenu()
|
|
|
|
useEffect(() => {
|
|
setMenuNumber(6)
|
|
}, [])
|
|
|
|
const { get } = useAxios()
|
|
const { getMessage } = useMessage()
|
|
|
|
// 차트 관련
|
|
const [chartData, setChartData] = useState([])
|
|
|
|
const { closeAll } = usePopup()
|
|
|
|
useEffect(() => {
|
|
closeAll()
|
|
}, [])
|
|
|
|
const data = {
|
|
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
|
datasets: [
|
|
{
|
|
label: 'kWh',
|
|
data: chartData.slice(0, 12).map((item) => {
|
|
const num = Number(item.replaceAll(',', ''))
|
|
return isNaN(num) ? 0 : num
|
|
}),
|
|
|
|
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(() => {
|
|
console.log('🚀 ~ useEffect ~ selectedPlan:', selectedPlan)
|
|
/* 초기화 작업 */
|
|
setChartData([])
|
|
setObjectDetail({})
|
|
setModuleInfoList([])
|
|
setPcsInfoList([])
|
|
setHatsudenryouAll([])
|
|
setHatsudenryouAllSnow([])
|
|
setHatsudenryouPeakcutAll([])
|
|
setHatsudenryouPeakcutAllSnow([])
|
|
|
|
if (objectNo) {
|
|
fetchObjectDetail(objectNo)
|
|
fetchSimulatorNotice()
|
|
setPwrGnrSimType('D')
|
|
setPwrRecoil({ ...pwrRecoil, type: 'D' })
|
|
}
|
|
}, [objectNo, pid, selectedPlan])
|
|
|
|
// 물건 상세 정보 조회
|
|
const [objectDetail, setObjectDetail] = useState({})
|
|
|
|
// 모듈배치정보 조회
|
|
const [moduleInfoList, setModuleInfoList] = useState([])
|
|
|
|
// 파워컨디셔너 조회
|
|
const [pcsInfoList, setPcsInfoList] = useState([])
|
|
|
|
// 타입별 list 조회
|
|
const [hatsudenryouAll, setHatsudenryouAll] = useState([])
|
|
const [hatsudenryouAllSnow, setHatsudenryouAllSnow] = useState([])
|
|
const [hatsudenryouPeakcutAll, setHatsudenryouPeakcutAll] = useState([])
|
|
const [hatsudenryouPeakcutAllSnow, setHatsudenryouPeakcutAllSnow] = useState([])
|
|
|
|
const fetchObjectDetail = async (objectNo) => {
|
|
const apiUrl = `/api/pwrGnrSimulation/calculations?objectNo=${objectNo}&planNo=${pid}`
|
|
|
|
const resultData = await get({ url: apiUrl })
|
|
if (resultData) {
|
|
setObjectDetail(resultData)
|
|
if (resultData.hatsudenryouAll) {
|
|
setHatsudenryouAll(resultData.hatsudenryouAll)
|
|
}
|
|
if (resultData.hatsudenryouAllSnow) {
|
|
setHatsudenryouAllSnow(resultData.hatsudenryouAllSnow)
|
|
}
|
|
if (resultData.hatsudenryouPeakcutAll) {
|
|
setHatsudenryouPeakcutAll(resultData.hatsudenryouPeakcutAll)
|
|
}
|
|
if (resultData.hatsudenryouPeakcutAllSnow) {
|
|
setHatsudenryouPeakcutAllSnow(resultData.hatsudenryouPeakcutAllSnow)
|
|
setChartData(resultData.hatsudenryouPeakcutAllSnow)
|
|
}
|
|
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', '<br/>'))
|
|
} else {
|
|
setContent(getMessage('common.message.no.data'))
|
|
}
|
|
})
|
|
}
|
|
|
|
// 차트 데이터 변경 시, list type 셋팅
|
|
const [pwrGnrSimType, setPwrGnrSimType] = useState('D')
|
|
const [pwrRecoil, setPwrRecoil] = useRecoilState(pwrGnrSimTypeState)
|
|
|
|
const handleChartChangeData = (type) => {
|
|
setPwrRecoil({ ...pwrRecoil, type: type })
|
|
switch (type) {
|
|
case 'A':
|
|
setChartData(hatsudenryouAll)
|
|
break
|
|
case 'B':
|
|
setChartData(hatsudenryouAllSnow)
|
|
break
|
|
case 'C':
|
|
setChartData(hatsudenryouPeakcutAll)
|
|
break
|
|
case 'D':
|
|
setChartData(hatsudenryouPeakcutAllSnow)
|
|
break
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="sub-content estimate">
|
|
<div className="sub-content-inner">
|
|
<div className="sub-content-box">
|
|
<div className="sub-table-box">
|
|
<div className="estimate-list-wrap">
|
|
{/* 물건번호 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub1')}</div>
|
|
<div className="estimate-name">
|
|
{objectDetail.objectNo}
|
|
{`${objectDetail.planNo ? `(Plan No: ${objectDetail.planNo})` : ''}`}
|
|
</div>
|
|
</div>
|
|
{/* 작성일 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub2')}</div>
|
|
<div className="estimate-name">
|
|
{objectDetail.drawingEstimateCreateDate ? `${dayjs(objectDetail.drawingEstimateCreateDate).format('YYYY.MM.DD')}` : ''}
|
|
</div>
|
|
</div>
|
|
{/* 시스템용량 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub3')}</div>
|
|
<div className="estimate-name">{objectDetail.capacity ? `${convertNumberToPriceDecimal(objectDetail.capacity)} kW` : ''}</div>
|
|
</div>
|
|
{/* 연간예측발전량 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub4')}</div>
|
|
<div className="estimate-name">{chartData[chartData.length - 1]}</div>
|
|
</div>
|
|
</div>
|
|
<div className="estimate-list-wrap">
|
|
{/* 도도부현 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub5')}</div>
|
|
<div className="estimate-name">{objectDetail.prefName}</div>
|
|
</div>
|
|
{/* 일사량 관측지점 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub6')}</div>
|
|
<div className="estimate-name">{objectDetail.areaName}</div>
|
|
</div>
|
|
{/* 적설조건 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub7')}</div>
|
|
<div className="estimate-name">{objectDetail.snowfall} cm</div>
|
|
</div>
|
|
{/* 풍속조건 */}
|
|
<div className="estimate-box">
|
|
<div className="estimate-tit">{getMessage('simulator.title.sub8')}</div>
|
|
<div className="estimate-name">{objectDetail.standardWindSpeedId}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="sub-content-box">
|
|
<div className="chart-wrap">
|
|
<div className="chart-inner">
|
|
<div className="sub-table-box">
|
|
<div className="chart-box">
|
|
{/* chart */}
|
|
<div className="select-wrap">
|
|
<select
|
|
style={{ width: '30%' }}
|
|
className="select-light"
|
|
value={pwrGnrSimType}
|
|
defaultValue={`D`}
|
|
onChange={(e) => {
|
|
handleChartChangeData(e.target.value)
|
|
setPwrGnrSimType(e.target.value)
|
|
}}
|
|
>
|
|
<option value={`A`}>積雪考慮なし(ピークカットなし発電量)</option>
|
|
<option value={`B`}>積雪考慮なし(ピークカットあり発電量)</option>
|
|
<option value={`C`}>積雪考慮あり(ピークカットなし発電量)</option>
|
|
<option value={`D`}>積雪考慮あり(ピークカットあり発電量)</option>
|
|
</select>
|
|
</div>
|
|
<Bar ref={chartRef} data={data} options={options} />
|
|
</div>
|
|
<div className="table-box-title-wrap">
|
|
<div className="title-wrap">
|
|
<h3>{getMessage('simulator.table.sub9')} </h3>
|
|
</div>
|
|
</div>
|
|
{/* 예측발전량 */}
|
|
<div className="chart-month-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>1月</th>
|
|
<th>2月</th>
|
|
<th>3月</th>
|
|
<th>4月</th>
|
|
<th>5月</th>
|
|
<th>6月</th>
|
|
<th>7月</th>
|
|
<th>8月</th>
|
|
<th>9月</th>
|
|
<th>10月</th>
|
|
<th>11月</th>
|
|
<th>12月</th>
|
|
<th>{getMessage('simulator.table.sub6')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{chartData.length > 0 ? (
|
|
<tr>
|
|
{chartData.map((data) => (
|
|
<td key={uuidv4()}>{data}</td>
|
|
))}
|
|
</tr>
|
|
) : (
|
|
<tr>
|
|
<td colSpan={13}>{getMessage('common.message.no.data')}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="chart-table-wrap">
|
|
<div className="sub-table-box">
|
|
<div className="module-table ">
|
|
<table className="small">
|
|
<thead>
|
|
<tr>
|
|
<th>{getMessage('simulator.table.sub1')}</th>
|
|
<th>{getMessage('simulator.table.sub2')}</th>
|
|
<th>{getMessage('simulator.table.sub3')}</th>
|
|
<th>{getMessage('simulator.table.sub4')}</th>
|
|
<th>{getMessage('simulator.table.sub5')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{moduleInfoList.length > 0 ? (
|
|
moduleInfoList.map((moduleInfo) => {
|
|
return (
|
|
<>
|
|
<tr key={moduleInfo.itemId}>
|
|
{/* 지붕면 */}
|
|
<td>{moduleInfo.roofSurface}</td>
|
|
{/* 경사각 */}
|
|
<td>{convertNumberToPriceDecimal(moduleInfo.slopeAngle)}{moduleInfo.classType == 0 ? "寸":"º"}</td>
|
|
{/* 방위각(도) */}
|
|
<td>{convertNumberToPriceDecimal(moduleInfo.azimuth)}</td>
|
|
{/* 태양전지모듈 */}
|
|
<td>
|
|
<div className="overflow-lab">{moduleInfo.itemNo}</div>
|
|
</td>
|
|
{/* 매수 */}
|
|
<td>{convertNumberToPriceDecimal(moduleInfo.amount)}</td>
|
|
</tr>
|
|
</>
|
|
)
|
|
})
|
|
) : (
|
|
<tr>
|
|
<td colSpan={5}>{getMessage('common.message.no.data')}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
{moduleInfoList.length > 0 && (
|
|
<div className="module-total">
|
|
<div className="total-title">{getMessage('simulator.table.sub6')}</div>
|
|
<div className="total-num">
|
|
{moduleInfoList.reduce((acc, moduleInfo) => convertNumberToPriceDecimal(Number(acc) + Number(moduleInfo.amount)), 0)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="sub-table-box">
|
|
<div className="module-table ">
|
|
<table className="big">
|
|
<thead>
|
|
<tr>
|
|
<th>{getMessage('simulator.table.sub7')}</th>
|
|
<th>{getMessage('simulator.table.sub8')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{pcsInfoList.length > 0 ? (
|
|
pcsInfoList.map((pcsInfo) => {
|
|
return (
|
|
<>
|
|
<tr key={pcsInfo.itemId}>
|
|
{/* 파워컨디셔너 */}
|
|
<td className="al-l">
|
|
<div className="overflow-lab">{pcsInfo.itemNo}</div>
|
|
</td>
|
|
{/* 대 */}
|
|
<td>{convertNumberToPriceDecimal(pcsInfo.amount)}</td>
|
|
</tr>
|
|
</>
|
|
)
|
|
})
|
|
) : (
|
|
<tr>
|
|
<td colSpan={2}>{getMessage('common.message.no.data')}</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="sub-content-box">
|
|
<div className="sub-table-box">
|
|
<div className="simulation-guide-wrap">
|
|
<div className="simulation-tit-wrap">
|
|
<span>
|
|
{getMessage('simulator.notice.sub1')}
|
|
<br />
|
|
{getMessage('simulator.notice.sub2')}
|
|
</span>
|
|
</div>
|
|
{/* 시뮬레이션 안내사항 */}
|
|
<div
|
|
className="simulation-guide-box"
|
|
dangerouslySetInnerHTML={{
|
|
__html: content,
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|