ysCha 38d04918a2 [모듈배치] PCS 진입 시 랙 크기순서 검증 오판 수정 — 모듈 면적을 스펙 치수로 산출
작업내용 :
- validateModuleSizeForRack 내 centerPoints area 산출을
  캔버스 측정치(Math.round(width×height))에서
  모듈 스펙(longAxis×shortAxis, moduleInfo 경유)으로 변경
- 같은 모듈이라도 캔버스 반올림으로 width 가 73/74 로 갈려
  area 가 161 만큼 벌어져 랙 크기순서 위반 오판 → PCS 진입 즉시 에러 유발
- 스펙 치수는 동일 제품이면 항상 동일하므로 diff=0 → 오판 제거
- gap 계산(인접 판정)용 width/height 는 캔버스 측정치 유지 (기하 정확도 보존)
2026-07-02 17:52:00 +09:00

1114 lines
38 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import WithDraggable from '@/components/common/draggable/WithDraggable'
import { useContext, useEffect, useRef, useState } from 'react'
import PowerConditionalSelect from '@/components/floor-plan/modal/circuitTrestle/step/PowerConditionalSelect'
import StepUp from '@/components/floor-plan/modal/circuitTrestle/step/StepUp'
import { useMessage } from '@/hooks/useMessage'
import { usePopup } from '@/hooks/usePopup'
import PassivityCircuitAllocation from './step/type/PassivityCircuitAllocation'
import BasicSetting from '@/components/floor-plan/modal/basic/BasicSetting'
import { useMasterController } from '@/hooks/common/useMasterController'
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { GlobalDataContext } from '@/app/GlobalDataProvider'
import { MENU, POLYGON_TYPE } from '@/common/common'
import { useSwal } from '@/hooks/useSwal'
import { canvasState, canvasZoomState, currentMenuState } from '@/store/canvasAtom'
import { useTrestle } from '@/hooks/module/useTrestle'
import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions'
import { v4 as uuidv4 } from 'uuid'
import { useEstimate } from '@/hooks/useEstimate'
import { useUndoRedo } from '@/hooks/useUndoRedo'
import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle'
import { useImgLoader } from '@/hooks/floorPlan/useImgLoader'
import { QcastContext } from '@/app/QcastProvider'
import { fontSelector } from '@/store/fontAtom'
import { fabric } from 'fabric'
const ALLOCATION_TYPE = {
AUTO: 'auto',
PASSIVITY: 'passivity',
}
export default function CircuitTrestleSetting({ id }) {
const { getMessage } = useMessage()
const { closePopup, addPopup } = usePopup()
const { apply, setViewCircuitNumberTexts, getEstimateData, clear: clearTrestle, setAllModuleSurfaceIsComplete } = useTrestle()
const { swalFire } = useSwal()
const { saveEstimate } = useEstimate()
const { clearStacks } = useUndoRedo()
const canvas = useRecoilValue(canvasState)
const setCurrentMenu = useSetRecoilState(currentMenuState)
const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState)
const [tabNum, setTabNum] = useState(1)
const [allocationType, setAllocationType] = useState(ALLOCATION_TYPE.AUTO)
const [circuitAllocationType, setCircuitAllocationType] = useState(1)
const { managementState, setManagementState } = useContext(GlobalDataContext)
const selectedModules = useRecoilValue(selectedModuleState)
const { getPcsAutoRecommendList, getPcsVoltageChk, getPcsVoltageStepUpList, getPcsManualConfChk } = useMasterController()
const flowText = useRecoilValue(fontSelector('flowText'))
const lengthText = useRecoilValue(fontSelector('lengthText'))
const circuitNumberText = useRecoilValue(fontSelector('circuitNumberText'))
// 회로할당(승합설정)에서 선택된 값들을 저장할 상태 추가
const [selectedStepUpValues, setSelectedStepUpValues] = useState({})
const [getStepUpSelections, setGetStepUpSelections] = useState(null)
// const { trigger: setCircuitData } = useCanvasPopupStatusController(4)
// const [stepUpListData, setStepUpListData] = useRecoilState(stepUpListDataState)
const [stepUpListData, setStepUpListData] = useState([])
const [seletedMainOption, setSeletedMainOption] = useState(null)
const [seletedSubOption, setSeletedSubOption] = useState(null)
const { setModuleStatisticsData } = useCircuitTrestle()
const { handleCanvasToPng } = useImgLoader()
const moduleSelectionData = useRecoilValue(moduleSelectionDataState)
const { setIsGlobalLoading } = useContext(QcastContext)
const originCanvasViewPortTransform = useRef([])
const [isFold, setIsFold] = useState(false)
const [showHiddenBasicSetting, setShowHiddenBasicSetting] = useState(true)
const {
makers,
setMakers,
selectedMaker,
setSelectedMaker,
series,
setSeries,
models,
setModels,
selectedModels,
setSelectedModels,
pcsCheck,
setPcsCheck,
getOptYn,
getPcsItemList,
getSelectedPcsItemList,
getUseModuleItemList,
getRoofSurfaceList,
getModuleList,
removeNotAllocationModules,
resetCircuits,
} = useCircuitTrestle()
// const { trigger: moduleSelectedDataTrigger } = useCanvasPopupStatusController(2)
useEffect(() => {
originCanvasViewPortTransform.current = [...canvas.viewportTransform]
if (!managementState) {
}
// setCircuitData({
// makers,
// selectedMaker,
// series,
// models,
// selectedModels,
// pcsCheck,
// })
return () => {
const moduleSetupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
if (moduleSetupSurfaces.some((surface) => !surface.isComplete)) {
resetCircuits()
}
}
}, [])
// 모듈/가대설정 팝업을 순간적으로 열었다가 닫아 초기화 로직 실행
useEffect(() => {
const timer = setTimeout(() => {
setShowHiddenBasicSetting(false)
}, 300)
return () => clearTimeout(timer)
}, [])
// 모듈이 설치된 경우 rack설치 여부에 따라 설치 된 모듈 아래에 작은 모듈이 설치되어 있을 경우는 모듈 설치 메뉴 reopen
useEffect(() => {
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
if (modules.length === 0) {
return
}
/**
* 랙 설치 시 모듈 크기 검증
* - 남쪽: 아래쪽 모듈이 위쪽 모듈보다 커야 함
* - 북쪽: 위쪽 모듈이 아래쪽 모듈보다 커야 함
* - 동쪽: 오른쪽 모듈이 왼쪽 모듈보다 커야 함
* - 서쪽: 왼쪽 모듈이 오른쪽 모듈보다 커야 함
*/
const validateModuleSizeForRack = (surface) => {
const { modules, direction, trestleDetail } = surface
if (!trestleDetail) {
return true // 상세 정보 없음
}
const { rackYn, moduleIntvlHor, moduleIntvlVer } = trestleDetail
if (rackYn === 'N' || !modules || modules.length < 2) {
return true // 검증 통과
}
// 모듈 중심점 및 크기 정보 계산
const centerPoints = modules.map((module) => {
const { x, y } = module.getCenterPoint()
const { width, height } = module
// [RACK-AREA-SPEC 2026-07-02] 면적은 캔버스 측정치(Math.round(width/height)) 대신 모듈 스펙(longAxis×shortAxis)으로 산출.
// 측정치는 좌표 반올림으로 같은 모듈도 width 가 73/74 로 갈려 area 가 161 만큼 벌어져(=가짜 크기차) 랙 순서 위반 오판을 유발한다.
// 스펙 치수는 배치·회전과 무관해 동일 제품이면 항상 area 동일 → 반올림 잔차 제거. (gap 계산용 width/height 는 그대로 유지)
const specLong = Number(module.longAxis ?? module.moduleInfo?.longAxis ?? 0) / 10
const specShort = Number(module.shortAxis ?? module.moduleInfo?.shortAxis ?? 0) / 10
return {
x,
y,
width: Math.round(width),
height: Math.round(height),
area: specLong * specShort,
}
})
// 방향별 설정
const isVertical = direction === 'south' || direction === 'north'
const primaryInterval = isVertical ? moduleIntvlVer : moduleIntvlHor
const secondaryInterval = isVertical ? moduleIntvlHor : moduleIntvlVer
// 정렬 함수: 큰 모듈이 있어야 할 위치부터 시작
const getSortFn = () => {
switch (direction) {
case 'south':
return (a, b) => b.y - a.y // 아래쪽(y 큼)부터
case 'north':
return (a, b) => a.y - b.y // 위쪽(y 작음)부터
case 'east':
return (a, b) => b.x - a.x // 오른쪽(x 큼)부터
case 'west':
return (a, b) => a.x - b.x // 왼쪽(x 작음)부터
default:
return () => 0
}
}
// 타겟 모듈 찾기 (현재 모듈보다 작아야 할 위치의 모듈)
const findTargetModules = (current, margin) => {
return centerPoints.filter((cp) => {
const sameAxis = isVertical ? Math.abs(cp.x - current.x) < margin : Math.abs(cp.y - current.y) < margin
const targetDirection =
direction === 'south'
? cp.y < current.y
: direction === 'north'
? cp.y > current.y
: direction === 'east'
? cp.x < current.x
: cp.x > current.x
return sameAxis && targetDirection
})
}
// 가장 가까운 타겟 모듈 찾기
const getClosestTarget = (filtered) => {
if (filtered.length === 0) return null
return filtered.reduce((closest, cp) => {
switch (direction) {
case 'south':
return cp.y > closest.y ? cp : closest
case 'north':
return cp.y < closest.y ? cp : closest
case 'east':
return cp.x > closest.x ? cp : closest
case 'west':
return cp.x < closest.x ? cp : closest
default:
return closest
}
})
}
// 두 모듈 간 간격 계산
const getGap = (current, target) => {
if (isVertical) {
return direction === 'south'
? current.y - current.height / 2 - (target.y + target.height / 2)
: target.y - target.height / 2 - (current.y + current.height / 2)
} else {
return direction === 'east'
? current.x - current.width / 2 - (target.x + target.width / 2)
: target.x - target.width / 2 - (current.x + current.width / 2)
}
}
// 인접 모듈 여부 확인
const isAdjacent = (current, target) => {
const gap = getGap(current, target)
return gap >= 0 && gap <= primaryInterval + 2
}
// 정렬된 모듈 순회
const sortedPoints = [...centerPoints].sort(getSortFn())
for (const current of sortedPoints) {
// 1. 일반 배치: 같은 라인에서 인접 모듈 검사
const directTargets = findTargetModules(current, secondaryInterval)
const closestTarget = getClosestTarget(directTargets)
if (closestTarget && isAdjacent(current, closestTarget) && closestTarget.area - current.area > 1) {
return false // 검증 실패
}
// 2. 물떼새 배치: 반 오프셋 위치의 인접 모듈 검사
const size = isVertical ? current.width : current.height
const halfOffset = (size + secondaryInterval) / 2
for (const sign of [-1, 1]) {
const offsetValue = sign * halfOffset
const findHalfTarget = centerPoints.filter((cp) => {
const offsetAxis = isVertical
? Math.abs(cp.x - (current.x + offsetValue)) <= primaryInterval
: Math.abs(cp.y - (current.y + offsetValue)) <= primaryInterval
const targetDirection =
direction === 'south'
? cp.y < current.y
: direction === 'north'
? cp.y > current.y
: direction === 'east'
? cp.x < current.x
: cp.x > current.x
return offsetAxis && targetDirection
})
const closestHalf = getClosestTarget(findHalfTarget)
if (closestHalf && isAdjacent(current, closestHalf) && closestHalf.area - current.area > 1) {
return false // 검증 실패
}
}
}
return true // 검증 통과
}
// 모든 설치면에 대해 검증 수행
const moduleSetupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
for (const surface of moduleSetupSurfaces) {
if (!validateModuleSizeForRack(surface)) {
swalFire({
text: getMessage('module.size.validation.rack.error'),
icon: 'error',
confirmFn: () => {
// 현재 팝업 닫기
closePopup(id)
// 메뉴 하이라이트 변경
setCurrentMenu(MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING)
// 모듈/가대설정 팝업 열기
const newPopupId = uuidv4()
clearTrestle()
setAllModuleSurfaceIsComplete(false)
addPopup(newPopupId, 1, <BasicSetting id={newPopupId} />)
},
})
return
}
}
}, [])
const capture = async (type) => {
beforeCapture(type)
await handleCanvasToPng(type)
afterCapture(type)
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, 1000)
})
}
// 캡쳐 전 처리
const beforeCapture = (type) => {
setCanvasZoom(100)
canvas.set({ zoom: 1 })
// roof 객체들을 찾아서 중앙점 계산
const roofs = canvas.getObjects().filter((obj) => obj.name === 'roof' && !obj.wall)
if (roofs.length > 0) {
// 모든 roof의 x, y 좌표를 수집
const allPoints = []
roofs.forEach((roof) => {
if (roof.getCurrentPoints()) {
roof.getCurrentPoints().forEach((point) => {
allPoints.push({ x: point.x, y: point.y })
})
}
})
if (allPoints.length > 0) {
// 모든 점들의 중앙값 계산
const minX = Math.min(...allPoints.map((p) => p.x))
const maxX = Math.max(...allPoints.map((p) => p.x))
const minY = Math.min(...allPoints.map((p) => p.y))
const maxY = Math.max(...allPoints.map((p) => p.y))
const centerX = (minX + maxX) / 2
const centerY = (minY + maxY) / 2
// 캔버스 중앙으로 이동하기 위한 오프셋 계산
const canvasWidth = canvas.getWidth()
const canvasHeight = canvas.getHeight()
const offsetX = canvasWidth / 2 - centerX
const offsetY = canvasHeight / 2 - centerY
canvas.viewportTransform = [1, 0, 0, 1, offsetX, offsetY]
} else {
canvas.viewportTransform = [1, 0, 0, 1, 0, 0]
}
} else {
canvas.viewportTransform = [1, 0, 0, 1, 0, 0]
}
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
const circuitNumberTexts = canvas.getObjects().filter((obj) => obj.name === 'circuitNumber')
if (type === 2) {
modules.forEach((module) => {
module.set({ originColor: module.fill, fill: null, strokeWidth: 2 })
})
circuitNumberTexts.forEach((text) => {
text.set({ visible: false })
})
}
let x, y
x = canvas.width / 2
y = canvas.height / 2
canvas.zoomToPoint(new fabric.Point(x, y), 0.4)
// changeFontSize('lengthText', '28')
// changeFontSize('circuitNumber', '28')
// changeFontSize('flowText', '28')
canvas.renderAll()
}
// 캡쳐 후 처리
const afterCapture = (type) => {
if (originCanvasViewPortTransform.current[0] !== 1) {
setCanvasZoom(Number((originCanvasViewPortTransform.current[0] * 100).toFixed(0)))
}
canvas.viewportTransform = [...originCanvasViewPortTransform.current]
canvas.renderAll()
changeFontSize('lengthText', lengthText.fontSize.value)
changeFontSize('circuitNumber', circuitNumberText.fontSize.value)
changeFontSize('flowText', flowText.fontSize.value)
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
const circuitNumberTexts = canvas.getObjects().filter((obj) => obj.name === 'circuitNumber')
if (type === 2) {
modules.forEach((module) => {
module.set({
fill: module.originColor,
strokeWidth: 0.3,
})
})
circuitNumberTexts.forEach((text) => {
text.set({ visible: true })
})
}
canvas.renderAll()
}
// 수동할당 시 모듈 삭제
// 시리즈중 자동으로 추천 PCS 정보 조회
const onAutoRecommend = () => {
if (series.filter((s) => s.selected).length === 0) {
swalFire({
title: getMessage('modal.circuit.trestle.setting.circuit.allocation.passivity.all.power.conditional.validation.error02'),
type: 'alert',
})
return
}
const isMultiModule = selectedModules.itemList.length > 1
let isAllIndfcs = false
if (isMultiModule) {
//INDFCS 실내집중, OUTDMULTI 옥외멀티
// 1. 모듈이 혼합형일 경우 선택한 pcs가 실내집중인 경우 alert
if (selectedModels.length > 0) {
isAllIndfcs = selectedModels.every((model) => model.pcsTpCd === 'INDFCS')
} else {
isAllIndfcs = models.every((model) => model.pcsTpCd === 'INDFCS')
}
}
if (isAllIndfcs) {
swalFire({
title: getMessage('module.circuit.indoor.focused.error'),
type: 'alert',
})
return
}
const params = {
...getOptYn(),
useModuleItemList: getUseModuleItemList(),
roofSurfaceList: getRoofSurfaceList(),
pcsItemList: getPcsItemList(isMultiModule),
}
// 파워컨디셔너 추천 목록 조회
if (selectedModels.length === 0) {
// 시리즈중 자동으로 추천 PCS 정보 조회
getPcsAutoRecommendList(params).then((res) => {
if (res.data?.pcsItemList) {
let pcsItemList = []
let pcsObj = {}
models.forEach((model) => {
pcsObj[`${model.pcsSerCd}_${model.itemId}`] = model
})
res.data?.pcsItemList.forEach((item) => {
const key = `${item.pcsSerCd}_${item.itemId}`
if (pcsObj[key]) {
pcsItemList.push({
...pcsObj[key],
isUsed: false,
id: uuidv4(),
})
}
})
// 회로 구성 가능 여부 체크 요청 파라미터
const pcsVoltageChkParams = {
...getOptYn(),
useModuleItemList: getUseModuleItemList(),
roofSurfaceList: getRoofSurfaceList(),
pcsItemList: pcsItemList,
}
// 추천 목록 선택
setSelectedModels(pcsItemList)
// 회로 구성 가능 여부 체크
getPcsVoltageChk(pcsVoltageChkParams).then((res) => {
if (res.resultCode === 'S') {
setTabNum(2)
setAllModuleSurfaceIsComplete(false)
} else {
swalFire({
title: res.resultMsg,
type: 'alert',
})
}
})
} else {
// 데이터가 없는 경우 오류 메시지 확인 필요
if (res.result.resultCode === 'E') {
swalFire({
title: res.result.resultMsg,
type: 'alert',
})
} else {
swalFire({
title: '파워컨디셔너를 추가해 주세요.',
type: 'alert',
})
}
}
})
} else {
// 회로 구성 가능 여부 체크
getPcsVoltageChk({ ...params, pcsItemList: getSelectedPcsItemList(isMultiModule) }).then((res) => {
if (res.resultCode === 'S') {
// 회로 구성 가능 여부 체크 통과 시 승압설정 정보 조회
getPcsVoltageStepUpList({
...params,
pcsItemList: getSelectedPcsItemList(isMultiModule),
}).then((res) => {
if (res?.result.resultCode === 'S' && res?.data) {
setTabNum(2)
setAllModuleSurfaceIsComplete(false)
} else {
swalFire({ text: getMessage('common.message.send.error') })
}
})
} else {
swalFire({
title: res.resultMsg,
type: 'alert',
})
}
})
}
}
// 자동할당 버튼 클릭 시
const onAutoAllocation = () => {
let moduleStdQty = 0
let moduleMaxQty = 0
const selectedModels = models.filter((m) => m.selected)
// 시리즈중 자동으로 추천 PCS 정보 조회
if (selectedModels.length === 0) {
onAutoRecommend()
} else {
// 모듈 최소 매수
moduleStdQty = selectedModels.reduce((acc, model) => {
return acc + parseInt(model.moduleStdQty)
}, 0)
moduleMaxQty = selectedModels.reduce((acc, model) => {
return acc + parseInt(model.moduleMaxQty)
}, 0)
}
// const target = pcsCheck.max ? moduleMaxQty : moduleStdQty
// const placementModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
// if (placementModules.length > target) {
// swalFire({
// title: '배치가능 매수를 초과합니다. 파워컨디셔너를 다시 선택해 주세요.',
// type: 'alert',
// })
// return
// }
// setAllocationType(ALLOCATION_TYPE.AUTO)
// setTabNum(2)
}
// 수동할당 버튼 클릭 시
const onPassivityAllocation = () => {
if (selectedModels.length === 0) {
const params = {
...getOptYn(),
useModuleItemList: getUseModuleItemList(),
roofSurfaceList: getRoofSurfaceList().map((surface) => {
return {
...surface,
moduleList: surface.moduleList.map((module) => {
return {
itemId: module.itemId,
uniqueId: module.uniqueId,
}
}),
}
}),
pcsItemList: getPcsItemList(),
}
// 파워컨디셔너 추천 목록 조회
getPcsAutoRecommendList(params).then((res) => {
if (res.data?.pcsItemList) {
let pcsItemList = []
let pcsObj = {}
models.forEach((model) => {
pcsObj[`${model.pcsSerCd}_${model.itemId}`] = model
})
res.data?.pcsItemList.forEach((item) => {
const key = `${item.pcsSerCd}_${item.itemId}`
if (pcsObj[key]) {
pcsItemList.push({
...pcsObj[key],
isUsed: false,
id: uuidv4(),
})
}
})
const pcsVoltageChkParams = {
...getOptYn(),
useModuleItemList: getUseModuleItemList(),
roofSurfaceList: getRoofSurfaceList(),
pcsItemList: pcsItemList.map((item) => {
return {
itemId: item.itemId,
pcsMkrCd: item.pcsMkrCd,
pcsSerCd: item.pcsSerCd,
pcsSerParallelYn: item.pcsSerParallelYn,
}
}),
}
setSelectedModels(pcsItemList)
getPcsVoltageChk(pcsVoltageChkParams).then((res) => {
setAllocationType(ALLOCATION_TYPE.PASSIVITY)
setAllModuleSurfaceIsComplete(false)
clearTrestle()
})
} else {
swalFire({
title: res.result.resultMsg,
type: 'alert',
confirmFn: () => {
return
},
})
return
}
})
} else {
const moduleStdQty = selectedModels.reduce((acc, model) => {
return acc + parseInt(model.moduleStdQty)
}, 0)
const moduleMaxQty = selectedModels.reduce((acc, model) => {
return acc + parseInt(model.moduleMaxQty)
}, 0)
const target = pcsCheck.max ? moduleMaxQty : moduleStdQty
const placementModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
let moduleAmount = placementModules.reduce((acc, module) => {
if (moduleSelectionData.module.itemList.length === 1 || module.moduleInfo.itemId === moduleSelectionData.module.itemList[0].itemId) {
return acc + 1
} else {
return acc + 0.66
}
}, 0)
if (moduleAmount > target) {
swalFire({
title: getMessage('modal.circuit.trestle.setting.circuit.allocation.passivity.all.power.conditional.validation.error01'),
type: 'alert',
})
return
}
setAllocationType(ALLOCATION_TYPE.PASSIVITY)
clearTrestle()
}
}
// StepUp에서 선택된 값들을 처리하는 함수 수정
const handleStepUpValuesSelected = (selectionData) => {
const { gooodsNo } = selectionData
setSelectedStepUpValues((prev) => ({
...prev,
[gooodsNo]: selectionData,
}))
}
// StepUp 컴포넌트 초기화 핸들러
const handleStepUpInitialize = (getCurrentSelections) => {
setGetStepUpSelections(() => getCurrentSelections)
}
// 회로할당(승압설정) 저장 버튼 클릭 시
const onApply = async () => {
setAllModuleSurfaceIsComplete(false)
setIsGlobalLoading(true)
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
.map((obj) => {
obj.pcses = getStepUpListData()
})
// 캡처 실패 시 에러 팝업 후 중단 (다시 회로구성 가능)
try {
await capture(1)
} catch (e) {
console.log('🚀 ~ capture(1) error:', e)
setIsGlobalLoading(false)
swalFire({
text: getMessage('common.message.send.error'),
icon: 'error',
})
return
}
//회로할당 저장 시 result=null인 경우에도 회로번호 텍스트 표시 유지 처리
// 회로할당 저장 클릭 시
// 가대 및 지지금구 설치
// apply()
// apply() 결과를 체크하여 null이면 함수 종료
const applyResult = apply()
if (!applyResult) {
setIsGlobalLoading(false)
return
}
const result = await getEstimateData()
if (result) {
await capture(2)
// 견적서 저장
await saveEstimate(result)
} else {
setIsGlobalLoading(false)
}
clearStacks()
// removeNotAllocationModules()
}
const changeFontSize = (name, size) => {
const textObjs = canvas?.getObjects().filter((obj) => obj.name === name)
textObjs.forEach((obj) => {
obj.set({
fontSize: size,
})
})
canvas.renderAll()
}
// 이전 버튼 클릭 시
const onClickPrev = () => {
// setAllocationType(ALLOCATION_TYPE.AUTO)
swalFire({
text: getMessage('modal.circuit.trestle.setting.circuit.allocation.passivity.all.power.conditional.reset.info'),
type: 'alert',
icon: 'warning',
confirmFn: () => {
const circuitModules = canvas.getObjects().filter((obj) => obj.name === 'module')
canvas.remove(...circuitModules.map((module) => module.circuit))
circuitModules.forEach((obj) => {
obj.circuit = null
obj.pcsItemId = null
obj.circuitNumber = null
obj.pcs = null
})
setSelectedModels(
JSON.parse(JSON.stringify(selectedModels)).map((model) => {
model.isUsed = false
return model
}),
)
if (allocationType === ALLOCATION_TYPE.PASSIVITY) {
setAllocationType(ALLOCATION_TYPE.AUTO)
setTabNum(1)
} else {
setTabNum(1)
}
clearTrestle()
canvas.renderAll()
setModuleStatisticsData()
},
})
}
// 파워컨디셔너 컴포넌트 속성
const powerConditionalSelectProps = {
tabNum,
setTabNum,
makers,
setMakers,
selectedMaker,
setSelectedMaker,
series,
setSeries,
models,
setModels,
selectedModels,
setSelectedModels,
managementState,
getUseModuleItemList,
}
// 수동할당 컴포넌트 속성
const passivityProps = {
tabNum,
setTabNum,
pcsCheck,
selectedModels,
setSelectedModels,
getOptYn,
getUseModuleItemList,
getRoofSurfaceList,
}
// 승압설정 컴포넌트 속성
const stepUpProps = {
tabNum,
setTabNum,
models,
setModels,
allocationType,
circuitAllocationType,
setCircuitAllocationType,
selectedMaker,
selectedModels,
setSelectedModels,
getSelectedPcsItemList,
getOptYn, // 옵션 Y/N
getUseModuleItemList, // 사용된 모듈아이템 List
getRoofSurfaceList, // 지붕면 목록
getPcsItemList, // PCS 아이템 목록
onValuesSelected: handleStepUpValuesSelected, // 선택된 값들을 처리하는 함수
stepUpListData,
setStepUpListData,
seletedMainOption,
setSeletedMainOption,
seletedSubOption,
setSeletedSubOption,
getModuleList,
setModuleStatisticsData,
}
// 승압설정 목록 조회
const getStepUpListData = () => {
// PCS 인스턴스별 실제 담당 모듈 타입 집합 수집 (Cross Mix DC3 접속함 분리용)
const pcsModuleTpCds = {}
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.circuit && obj.pcsItemId)
.forEach((module) => {
const pcsInstanceId = module.circuit?.circuitInfo?.id
const tpCd = module.moduleInfo?.moduleTpCd
if (!pcsInstanceId || !tpCd) return
if (!pcsModuleTpCds[pcsInstanceId]) pcsModuleTpCds[pcsInstanceId] = new Set()
pcsModuleTpCds[pcsInstanceId].add(tpCd)
})
const pcs = []
stepUpListData[0].pcsItemList.map((item, index) => {
const targetTpCds = pcsModuleTpCds[selectedModels[index]?.id]
return item.serQtyList
.filter((serQty) => serQty.selected && serQty.paralQty > 0)
.forEach((serQty) => {
pcs.push({
pcsMkrCd: item.pcsMkrCd,
pcsSerCd: item.pcsSerCd,
pcsItemId: item.itemId,
pscOptCd: getPcsOptCd(index),
paralQty: serQty.paralQty,
// 접속함: PCS가 실제 담당하는 모듈 타입으로 필터 후 itemId 기준 dedupe
// (Cross Mix 시 다른 모듈 계열의 conn 제외 / 단일·동계열 Mix는 기존과 동일 결과)
connections: item.connList?.length
? [
...new Map(
item.connList
.filter((conn) => !targetTpCds || !conn.moduleTpCd || targetTpCds.has(conn.moduleTpCd))
.map((conn) => [
conn.itemId,
{ connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt, moduleTpCd: conn.moduleTpCd },
]),
).values(),
]
: [],
})
// return {
// pcsMkrCd: item.pcsMkrCd,
// pcsSerCd: item.pcsSerCd,
// pcsItemId: item.itemId,
// pscOptCd: getPcsOptCd(index),
// paralQty: serQty.paralQty,
// connections: item.connList?.length
// ? [
// {
// connItemId: item.connList[0].itemId,
// },
// ]
// : [],
// }
})
})
return pcs
}
const getPcsOptCd = (index) => {
if (selectedModels.some((model) => model.pcsSerParallelYn === 'Y')) {
if (selectedModels.length > 1) {
if (index === 0) {
return seletedMainOption.code
} else {
return seletedSubOption.code
}
} else {
return seletedMainOption.code
}
} else {
return seletedMainOption.code
}
}
const handleStepUp = () => {
handlePassivityAllocationCkeck()
// const notAllocationModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && !obj.circuit)
// if (notAllocationModules.length > 0) {
// swalFire({
// title: getMessage('not.allocation.exist.module'),
// type: 'alert',
// })
// return
// } else {
// passivityCircuitAllocationRef.current.onApply()
// setTabNum(2)
// }
}
// 닫기 버튼 클릭 시 처리하는 함수 추가
const handleClose = () => {
// // 회로 번호 텍스트 제거
if (
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
.some((surface) => !surface.isComplete)
) {
canvas.remove(...canvas.getObjects().filter((obj) => obj.name === 'circuitNumber'))
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
.forEach((obj) => {
obj.circuit = null
obj.pcsItemId = null
obj.circuitNumber = null
})
canvas.renderAll()
}
closePopup(id)
}
const handlePassivityAllocationCkeck = () => {
let pcsCount = {}
let result = {}
const notAllocationModules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && !obj.circuit)
if (notAllocationModules.length > 0) {
swalFire({
text: getMessage('not.allocation.exist.module'),
type: 'alert',
icon: 'warning',
})
return
}
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
.forEach((module) => {
const circuitNumber = module.circuitNumber.replace(/[()]/g, '')
pcsCount[circuitNumber] = (pcsCount[circuitNumber] || 0) + 1
})
for (const key in pcsCount) {
const firstPart = key.split('-')[0] // '-' 기준으로 첫 번째 부분을 추출
const value = pcsCount[key]
// 그룹이 없으면 초기화
if (!result[firstPart]) {
result[firstPart] = { maxValue: value, count: 1 }
} else {
// 이미 그룹이 있으면 큰 값으로 갱신, count는 증가
result[firstPart].maxValue = Math.max(result[firstPart].maxValue, value)
result[firstPart].count += 1
}
}
const params = {
...getOptYn(),
useModuleItemList: getUseModuleItemList(),
roofSurfaceList: getRoofSurfaceList(),
pcsItemList: selectedModels.map((model, index) => {
return {
pcsMkrCd: model.pcsMkrCd,
pcsSerCd: model.pcsSerCd,
itemId: model.itemId,
itemNm: model.itemNm,
goodsNo: model.goodsNo,
pcsSerParallelYn: model.pcsSerParallelYn,
serQtyList: [
{
serQty: result[(index + 1).toString()] ? result[(index + 1).toString()].maxValue : 0,
paralQty: result[(index + 1).toString()] ? result[(index + 1).toString()].count : 0,
rmdYn: 'Y',
usePossYn: 'Y',
roofSurfaceList: canvas
.getObjects()
.filter((obj) => POLYGON_TYPE.MODULE_SETUP_SURFACE === obj.name && obj?.modules.length > 0)
.map((surface) => {
return {
roofSurfaceId: surface.id,
roofSurface: surface.direction,
roofSurfaceIncl: +canvas.getObjects().filter((obj) => obj.id === surface.parentId)[0].pitch,
moduleList: surface.modules.map((module) => {
return {
itemId: module.moduleInfo.itemId,
circuit: module.circuitNumber,
pcsItemId: module.pcsItemId,
}
}),
}
}),
},
],
}
}),
}
getPcsManualConfChk(params).then((res) => {
if (res?.resultCode === 'E') {
swalFire({
text: res.resultMsg,
type: 'alert',
icon: 'warning',
})
return
} else {
setTabNum(2)
setAllModuleSurfaceIsComplete(false)
}
})
}
return (
<>
{showHiddenBasicSetting && (
<div style={{ display: 'none' }}>
<BasicSetting id={`hidden-basic-${id}`} />
</div>
)}
<WithDraggable isShow={true} pos={{ x: 50, y: 230 }} className="l-2">
<WithDraggable.Header
title={getMessage('modal.circuit.trestle.setting')}
onClose={() => handleClose()}
isFold={isFold}
onFold={() => setIsFold(!isFold)}
/>
<WithDraggable.Body>
<div style={{ display: !(tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY) && isFold ? 'none' : 'block' }}>
<div style={{ display: tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && isFold ? 'none' : 'block' }}>
<div className="roof-module-tab">
<div className={`module-tab-bx act`}>{getMessage('modal.circuit.trestle.setting.power.conditional.select')}</div>
<span className={`tab-arr ${tabNum === 2 ? 'act' : ''}`}></span>
<div className={`module-tab-bx ${tabNum === 2 ? 'act' : ''}`}>
{getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')})
</div>
</div>
</div>
{tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && <PowerConditionalSelect {...powerConditionalSelectProps} />}
{tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && (
<PassivityCircuitAllocation {...passivityProps} isFold={isFold} />
)}
{tabNum === 2 && <StepUp {...stepUpProps} onInitialize={handleStepUpInitialize} />}
</div>
{tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && (
<div className="grid-btn-wrap">
<button className="btn-frame modal mr5 act" onClick={() => onAutoRecommend()}>
{getMessage('modal.circuit.trestle.setting.circuit.allocation.auto')}
</button>
<button className="btn-frame modal act" onClick={() => onPassivityAllocation()}>
{getMessage('modal.circuit.trestle.setting.circuit.allocation.passivity')}
</button>
</div>
)}
{tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && (
<div className="grid-btn-wrap">
<button className="btn-frame modal mr5 " onClick={() => onClickPrev()}>
{getMessage('modal.common.prev')}
</button>
<button className="btn-frame modal act" onClick={() => handleStepUp()}>
{getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')})
</button>
</div>
)}
{tabNum === 2 && (
<div className="grid-btn-wrap">
<button className="btn-frame modal mr5" onClick={() => onClickPrev()}>
{getMessage('modal.common.prev')}
</button>
{/* <button className="btn-frame modal act" onClick={() => apply()}> */}
<button className="btn-frame modal act" onClick={onApply}>
{getMessage('modal.common.save')}({getMessage('modal.circuit.trestle.setting.circuit.allocation')})
</button>
</div>
)}
</WithDraggable.Body>
</WithDraggable>
</>
)
}