From b9efc4aa4770bffa4a4b4dd91552839cb8149271 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 29 Dec 2025 16:56:39 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=8B=9C=20validation=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../circuitTrestle/CircuitTrestleSetting.jsx | 171 +++++++++++++++++- src/locales/ja.json | 1 + src/locales/ko.json | 1 + 3 files changed, 169 insertions(+), 4 deletions(-) diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index f86a7ead..32e20ad4 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -5,12 +5,13 @@ 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 } from 'recoil' +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil' import { GlobalDataContext } from '@/app/GlobalDataProvider' -import { POLYGON_TYPE } from '@/common/common' +import { POLYGON_TYPE, MENU } from '@/common/common' import { useSwal } from '@/hooks/useSwal' -import { canvasState, canvasZoomState } from '@/store/canvasAtom' +import { canvasState, canvasZoomState, currentMenuState } from '@/store/canvasAtom' import { useTrestle } from '@/hooks/module/useTrestle' import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions' @@ -29,11 +30,12 @@ const ALLOCATION_TYPE = { } export default function CircuitTrestleSetting({ id }) { const { getMessage } = useMessage() - const { closePopup } = usePopup() + const { closePopup, addPopup } = usePopup() const { apply, setViewCircuitNumberTexts, getEstimateData, clear: clearTrestle, setAllModuleSurfaceIsComplete } = useTrestle() const { swalFire } = useSwal() const { saveEstimate } = useEstimate() 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) @@ -106,6 +108,167 @@ export default function CircuitTrestleSetting({ id }) { } }, []) + // 모듈이 설치된 경우 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 + 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 + return { + x, + y, + width: Math.floor(width), + height: Math.floor(height), + area: Math.floor(width) * Math.floor(height), + } + }) + + // 방향별 설정 + 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 + 1 + } + + // 정렬된 모듈 순회 + 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) { + 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) { + 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, ) + }, + }) + return + } + } + }, []) + const capture = async (type) => { beforeCapture(type) diff --git a/src/locales/ja.json b/src/locales/ja.json index e738ebaa..7fb83f4e 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -134,6 +134,7 @@ "modal.module.basic.settting.module.error10": "棟側の配置領域の値を{0} mm以上に変更してください。\n(屋根材: {1})", "modal.module.basic.settting.module.error11": "ケラバ側の配置領域の値を{0} mm以上に変更してください。\n(屋根材: {1})", "modal.module.basic.settting.module.error12": "施工方法を選択してください。\n(屋根材: {0})", + "module.size.validation.rack.error": "モジュール配置が不正です。 正しく配置し直してください。", "modal.module.basic.setting.module.placement": "モジュールの配置", "modal.module.basic.setting.module.placement.select.fitting.type": "設置形態を選択してください。", "modal.module.basic.setting.module.placement.waterfowl.arrangement": "千鳥配置", diff --git a/src/locales/ko.json b/src/locales/ko.json index bed23c44..4d237fd6 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -134,6 +134,7 @@ "modal.module.basic.settting.module.error10": "용마루쪽 값은 {0}mm 이상이어야 합니다.\n(지붕재: {1})", "modal.module.basic.settting.module.error11": "케라바쪽 값은 {0}mm 이상이어야 합니다.\n(지붕재: {1})", "modal.module.basic.settting.module.error12": "시공법을 선택해주세요.\n(지붕재: {0})", + "module.size.validation.rack.error": "모듈 배치가 잘못되었습니다. 올바르게 다시 배치해 주세요.", "modal.module.basic.setting.module.placement": "모듈 배치", "modal.module.basic.setting.module.placement.select.fitting.type": "설치형태를 선택합니다.", "modal.module.basic.setting.module.placement.waterfowl.arrangement": "물떼새 배치",