Merge pull request 'dev' (#909) from dev into prd-deploy
Reviewed-on: #909
This commit is contained in:
commit
cab42bf612
@ -34,6 +34,10 @@ export default function QContextMenu(props) {
|
|||||||
const handleContextMenu = (e) => {
|
const handleContextMenu = (e) => {
|
||||||
// e.preventDefault() //기존 contextmenu 막고
|
// e.preventDefault() //기존 contextmenu 막고
|
||||||
|
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 복수선택(2개 이상) 시 컨텍스트 메뉴 노출 차단.
|
||||||
|
// 복수선택은 Del 키 삭제 + 마우스 드래그 이동만 지원.
|
||||||
|
if (canvas?.getActiveObjects && canvas.getActiveObjects().length > 1) return
|
||||||
|
|
||||||
if (currentObject) {
|
if (currentObject) {
|
||||||
const isArray = currentObject.hasOwnProperty('arrayData')
|
const isArray = currentObject.hasOwnProperty('arrayData')
|
||||||
if (isArray && currentObject.arrayData.length === 0) return
|
if (isArray && currentObject.arrayData.length === 0) return
|
||||||
|
|||||||
@ -49,7 +49,16 @@ export default function PassivityCircuitAllocation(props) {
|
|||||||
obj.on('mousedown', (e) => handleTargetModules(obj))
|
obj.on('mousedown', (e) => handleTargetModules(obj))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// [CIRCUIT-BOX-SELECT 2026-06-10] 手動回路割り当て 에서만 드래그 박스 복수선택 활성화 (이 화면 한정)
|
||||||
|
const prevSelection = canvas.selection
|
||||||
|
canvas.selection = true
|
||||||
|
canvas.on('selection:created', handleBoxSelect)
|
||||||
|
canvas.on('selection:updated', handleBoxSelect)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
canvas.off('selection:created', handleBoxSelect)
|
||||||
|
canvas.off('selection:updated', handleBoxSelect)
|
||||||
|
canvas.selection = prevSelection
|
||||||
canvas
|
canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
@ -57,6 +66,7 @@ export default function PassivityCircuitAllocation(props) {
|
|||||||
obj.set({ strokeWidth: 0.3 })
|
obj.set({ strokeWidth: 0.3 })
|
||||||
obj.off('mousedown')
|
obj.off('mousedown')
|
||||||
})
|
})
|
||||||
|
canvas.discardActiveObject()
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
@ -84,6 +94,28 @@ export default function PassivityCircuitAllocation(props) {
|
|||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [CIRCUIT-BOX-SELECT 2026-06-10] 드래그 박스(activeSelection) 로 잡힌 모듈을 targetModules 에 가산 병합 후 fabric 선택 해제
|
||||||
|
const handleBoxSelect = () => {
|
||||||
|
const active = canvas.getActiveObject()
|
||||||
|
if (!active || active.type !== 'activeSelection') return
|
||||||
|
const boxModules = active.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
if (boxModules.length === 0) return
|
||||||
|
|
||||||
|
setTargetModules((prev) => {
|
||||||
|
const next = Array.isArray(prev) ? [...prev] : []
|
||||||
|
boxModules.forEach((obj) => {
|
||||||
|
if (!next.includes(obj.id)) {
|
||||||
|
obj.set({ strokeWidth: 2 })
|
||||||
|
next.push(obj.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.discardActiveObject()
|
||||||
|
canvas.renderAll()
|
||||||
|
}
|
||||||
|
|
||||||
const handleCircuitNumberFix = () => {
|
const handleCircuitNumberFix = () => {
|
||||||
const pcsTpCd = selectedPcs.pcsTpCd // 실내집중형, 옥외멀티형
|
const pcsTpCd = selectedPcs.pcsTpCd // 실내집중형, 옥외멀티형
|
||||||
let uniqueCircuitNumbers = [
|
let uniqueCircuitNumbers = [
|
||||||
|
|||||||
@ -981,15 +981,11 @@ export function useModule() {
|
|||||||
|
|
||||||
// 회로가 없을때만 삭제 가능
|
// 회로가 없을때만 삭제 가능
|
||||||
const modulesRemove = () => {
|
const modulesRemove = () => {
|
||||||
const activeModule = canvas.getObjects().find((obj) => canvas.getActiveObjects()[0].id === obj.id)
|
// [MODULE-MULTI-SELECT 2026-06-10] 단일/복수 선택 모두 처리. 회로가 걸린 모듈이 하나라도 있으면 차단.
|
||||||
const modules = canvas
|
const activeModules = canvas.getActiveObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
.getObjects()
|
if (activeModules.length === 0) return
|
||||||
.filter((obj) => obj.surfaceId === activeModule.surfaceId && obj.name === POLYGON_TYPE.MODULE && activeModule.id !== obj.id)
|
|
||||||
const surface = canvas.getObjects().find((obj) => obj.id === activeModule.surfaceId && obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
if (activeModules.some((module) => module.circuit)) {
|
||||||
if (surface.isComplete) {
|
|
||||||
removeTrestleMaterials()
|
|
||||||
}
|
|
||||||
if (activeModule.circuit) {
|
|
||||||
swalFire({
|
swalFire({
|
||||||
title: getMessage('can.not.remove.module'),
|
title: getMessage('can.not.remove.module'),
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
@ -998,11 +994,26 @@ export function useModule() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
surface.set({ modules: modules })
|
const surfaceIds = [...new Set(activeModules.map((module) => module.surfaceId))]
|
||||||
canvas.remove(activeModule)
|
const affectedSurfaces = canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && surfaceIds.includes(obj.id))
|
||||||
|
|
||||||
|
if (affectedSurfaces.some((surface) => surface.isComplete)) {
|
||||||
|
removeTrestleMaterials()
|
||||||
|
}
|
||||||
|
|
||||||
|
activeModules.forEach((module) => canvas.remove(module))
|
||||||
|
|
||||||
|
affectedSurfaces.forEach((surface) => {
|
||||||
|
const remaining = canvas.getObjects().filter((obj) => obj.surfaceId === surface.id && obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
surface.set({ modules: remaining })
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.discardActiveObject()
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
setModuleStatisticsData()
|
setModuleStatisticsData()
|
||||||
recalculateAllModulesCoords(activeModule.surfaceId)
|
surfaceIds.forEach((surfaceId) => recalculateAllModulesCoords(surfaceId))
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleRoofRemove = (surfaceArray) => {
|
const moduleRoofRemove = (surfaceArray) => {
|
||||||
@ -1130,6 +1141,55 @@ export function useModule() {
|
|||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 드래그 완료 후 지붕면 이탈/겹침 검증 → 위반 시 startPos 복원, 정상이면 확정
|
||||||
|
const commitModuleDragMove = (target, startPos) => {
|
||||||
|
if (!canvas || !target) return
|
||||||
|
const isGroup = target.type === 'activeSelection'
|
||||||
|
const modules = isGroup
|
||||||
|
? target.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
: target.name === POLYGON_TYPE.MODULE
|
||||||
|
? [target]
|
||||||
|
: []
|
||||||
|
if (modules.length === 0) return
|
||||||
|
|
||||||
|
const movingIds = new Set(modules.map((module) => module.id))
|
||||||
|
const objects = getObjects()
|
||||||
|
|
||||||
|
const invalid = modules.some((module) => {
|
||||||
|
const surface = canvas.getObjects().find((obj) => obj.id === module.surfaceId && obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
||||||
|
if (surface && isOutsideSurface(module, surface)) return true
|
||||||
|
const others = canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.surfaceId === module.surfaceId && !movingIds.has(obj.id))
|
||||||
|
if (isOverlapOtherModules(module, others)) return true
|
||||||
|
if (isOverlapObjects(module, objects)) return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 위반 시 드래그 시작 위치로 복원 (그룹/단일 공통 — 둘 다 left/top 평행이동)
|
||||||
|
if (invalid && startPos) {
|
||||||
|
target.set({ left: startPos.left, top: startPos.top })
|
||||||
|
target.setCoords()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그룹은 자식 좌표가 상대값이라 discard 로 절대좌표 베이킹. 단일은 이미 절대좌표라 선택 유지.
|
||||||
|
if (isGroup) {
|
||||||
|
canvas.discardActiveObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
const surfaceIds = [...new Set(modules.map((module) => module.surfaceId))]
|
||||||
|
surfaceIds.forEach((surfaceId) => recalculateAllModulesCoords(surfaceId))
|
||||||
|
canvas.requestRenderAll()
|
||||||
|
|
||||||
|
if (invalid) {
|
||||||
|
swalFire({
|
||||||
|
title: getMessage('can.not.move.module'),
|
||||||
|
icon: 'error',
|
||||||
|
type: 'alert',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [PLAN-CORRUPT-RESTORE 2026-05-18] 손상 module 좌표 복원.
|
* [PLAN-CORRUPT-RESTORE 2026-05-18] 손상 module 좌표 복원.
|
||||||
*
|
*
|
||||||
@ -1269,6 +1329,7 @@ export function useModule() {
|
|||||||
moduleRoofRemove,
|
moduleRoofRemove,
|
||||||
alignModule,
|
alignModule,
|
||||||
recalculateAllModulesCoords,
|
recalculateAllModulesCoords,
|
||||||
|
commitModuleDragMove,
|
||||||
restoreCorruptedModules,
|
restoreCorruptedModules,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { fabric } from 'fabric'
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, currentObjectState } from '@/store/canvasAtom'
|
import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, currentObjectState, isManualModuleSetupState } from '@/store/canvasAtom'
|
||||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||||
import { fontSelector } from '@/store/fontAtom'
|
import { fontSelector } from '@/store/fontAtom'
|
||||||
import { MENU, POLYGON_TYPE } from '@/common/common'
|
import { MENU, POLYGON_TYPE } from '@/common/common'
|
||||||
import { useText } from '@/hooks/useText'
|
import { useText } from '@/hooks/useText'
|
||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
|
import { useModule } from '@/hooks/module/useModule'
|
||||||
|
|
||||||
// 캔버스에 필요한 이벤트
|
// 캔버스에 필요한 이벤트
|
||||||
export function useCanvasEvent() {
|
export function useCanvasEvent() {
|
||||||
@ -18,8 +20,12 @@ export function useCanvasEvent() {
|
|||||||
const [shouldCenter, setShouldCenter] = useState(false) // 중앙 정렬 플래그 추가
|
const [shouldCenter, setShouldCenter] = useState(false) // 중앙 정렬 플래그 추가
|
||||||
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
|
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
|
||||||
const currentMenu = useRecoilValue(currentMenuState)
|
const currentMenu = useRecoilValue(currentMenuState)
|
||||||
|
const isManualModuleSetup = useRecoilValue(isManualModuleSetupState)
|
||||||
const { changeCorridorDimensionText } = useText()
|
const { changeCorridorDimensionText } = useText()
|
||||||
const { setPolygonLinesActualSize } = usePolygon()
|
const { setPolygonLinesActualSize } = usePolygon()
|
||||||
|
const { commitModuleDragMove } = useModule()
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 모듈 복수선택 refine 재진입 가드 (rebuild 중첩 방지)
|
||||||
|
const refiningRef = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
canvas?.setZoom(canvasZoom / 100)
|
canvas?.setZoom(canvasZoom / 100)
|
||||||
@ -38,7 +44,16 @@ export function useCanvasEvent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
attachDefaultEventOnCanvas()
|
attachDefaultEventOnCanvas()
|
||||||
}, [currentMenu])
|
// [MODULE-MULTI-SELECT 2026-06-10] 모듈 기본설정 메뉴에서만 드래그 박스/복수선택 활성화.
|
||||||
|
// 그 외 메뉴는 selection:false 유지(지붕선·마루 등 다른 레이어가 드래그에 잡히는 부작용 방지).
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 手動配置 활성 중에는 드래그 모드 중지(배치 클릭과 충돌 방지).
|
||||||
|
if (canvas) {
|
||||||
|
const isModuleMenu = currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING
|
||||||
|
const enableDrag = isModuleMenu && !isManualModuleSetup
|
||||||
|
canvas.selection = enableDrag
|
||||||
|
if (!enableDrag) canvas.discardActiveObject()
|
||||||
|
}
|
||||||
|
}, [currentMenu, isManualModuleSetup])
|
||||||
|
|
||||||
// 기본적인 이벤트 필요시 추가
|
// 기본적인 이벤트 필요시 추가
|
||||||
const attachDefaultEventOnCanvas = () => {
|
const attachDefaultEventOnCanvas = () => {
|
||||||
@ -46,6 +61,7 @@ export function useCanvasEvent() {
|
|||||||
canvas?.on('object:added', objectEvent.onChange)
|
canvas?.on('object:added', objectEvent.onChange)
|
||||||
canvas?.on('object:added', objectEvent.addEvent)
|
canvas?.on('object:added', objectEvent.addEvent)
|
||||||
canvas?.on('object:modified', objectEvent.onChange)
|
canvas?.on('object:modified', objectEvent.onChange)
|
||||||
|
canvas?.on('object:modified', handleModuleSelectionMoved)
|
||||||
canvas?.on('object:removed', objectEvent.onChange)
|
canvas?.on('object:removed', objectEvent.onChange)
|
||||||
canvas?.on('selection:cleared', selectionEvent.cleared)
|
canvas?.on('selection:cleared', selectionEvent.cleared)
|
||||||
canvas?.on('selection:created', selectionEvent.created)
|
canvas?.on('selection:created', selectionEvent.created)
|
||||||
@ -207,6 +223,7 @@ export function useCanvasEvent() {
|
|||||||
})
|
})
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
}
|
}
|
||||||
|
refineModuleSelection()
|
||||||
},
|
},
|
||||||
cleared: (e) => {
|
cleared: (e) => {
|
||||||
setCurrentObject(null)
|
setCurrentObject(null)
|
||||||
@ -221,6 +238,10 @@ export function useCanvasEvent() {
|
|||||||
if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
if (obj.name === POLYGON_TYPE.MODULE && currentMenu === MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) {
|
||||||
obj.set({ strokeWidth: 0.3 })
|
obj.set({ strokeWidth: 0.3 })
|
||||||
}
|
}
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택 해제 시 모듈 이동잠금 복원(다른 메뉴에서 드래그 방지)
|
||||||
|
if (obj.name === POLYGON_TYPE.MODULE) {
|
||||||
|
obj.set({ lockMovementX: true, lockMovementY: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -239,6 +260,10 @@ export function useCanvasEvent() {
|
|||||||
//모듈 미선택시 라인 두께 변경
|
//모듈 미선택시 라인 두께 변경
|
||||||
obj.set({ strokeWidth: 0.3 })
|
obj.set({ strokeWidth: 0.3 })
|
||||||
}
|
}
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택 해제 시 모듈 이동잠금 복원(다른 메뉴에서 드래그 방지)
|
||||||
|
if (obj.name === POLYGON_TYPE.MODULE) {
|
||||||
|
obj.set({ lockMovementX: true, lockMovementY: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -256,9 +281,72 @@ export function useCanvasEvent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
refineModuleSelection()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] 선택을 모듈만으로 정제, 이동잠금 해제 (회전·스케일은 잠금 유지). refiningRef 로 재구성 중첩 차단.
|
||||||
|
const refineModuleSelection = () => {
|
||||||
|
if (!canvas) return
|
||||||
|
if (currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) return
|
||||||
|
if (refiningRef.current) return
|
||||||
|
|
||||||
|
const active = canvas.getActiveObject()
|
||||||
|
if (!active) return
|
||||||
|
|
||||||
|
// 단일 모듈 선택: 마우스 이동 가능하도록 이동잠금 해제 (회전·스케일은 잠금 유지).
|
||||||
|
if (active.type !== 'activeSelection') {
|
||||||
|
if (active.name === POLYGON_TYPE.MODULE) {
|
||||||
|
active.set({ lockMovementX: false, lockMovementY: false, lockRotation: true, lockScalingX: true, lockScalingY: true, hasControls: false })
|
||||||
|
canvas.requestRenderAll()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const objs = active.getObjects()
|
||||||
|
const modules = objs.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
|
||||||
|
if (modules.length !== objs.length) {
|
||||||
|
refiningRef.current = true
|
||||||
|
canvas.discardActiveObject()
|
||||||
|
if (modules.length > 1) {
|
||||||
|
canvas.setActiveObject(new fabric.ActiveSelection(modules, { canvas }))
|
||||||
|
} else if (modules.length === 1) {
|
||||||
|
canvas.setActiveObject(modules[0])
|
||||||
|
}
|
||||||
|
refiningRef.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalActive = canvas.getActiveObject()
|
||||||
|
if (finalActive?.type === 'activeSelection') {
|
||||||
|
finalActive.set({
|
||||||
|
lockMovementX: false,
|
||||||
|
lockMovementY: false,
|
||||||
|
lockScalingX: true,
|
||||||
|
lockScalingY: true,
|
||||||
|
lockRotation: true,
|
||||||
|
hasControls: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
canvas.requestRenderAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [MODULE-MULTI-SELECT 2026-06-10] object:modified 시 지붕면 경계 검증 → 위반이면 드래그 시작 좌표로 복원
|
||||||
|
const handleModuleSelectionMoved = (e) => {
|
||||||
|
if (currentMenu !== MENU.MODULE_CIRCUIT_SETTING.BASIC_SETTING) return
|
||||||
|
const target = e?.target
|
||||||
|
if (!target) return
|
||||||
|
|
||||||
|
const isGroup = target.type === 'activeSelection'
|
||||||
|
const isSingleModule = target.name === POLYGON_TYPE.MODULE
|
||||||
|
if (!isGroup && !isSingleModule) return
|
||||||
|
if (isGroup && !target.getObjects().some((obj) => obj.name === POLYGON_TYPE.MODULE)) return
|
||||||
|
|
||||||
|
const original = e?.transform?.original
|
||||||
|
const startPos = original ? { left: original.left, top: original.top } : null
|
||||||
|
commitModuleDragMove(target, startPos)
|
||||||
|
}
|
||||||
|
|
||||||
const removeEventOnCanvas = () => {
|
const removeEventOnCanvas = () => {
|
||||||
canvas?.off('object:added')
|
canvas?.off('object:added')
|
||||||
canvas?.off('object:modified')
|
canvas?.off('object:modified')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user