From bf681dccba2a314d2fda6b7fa2b86e3412e004d5 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Tue, 2 Dec 2025 11:05:03 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EC=A4=8C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.js | 3 +- src/components/floor-plan/CanvasFrame.jsx | 12 +++++- src/components/floor-plan/CanvasMenu.jsx | 25 +++++++----- src/hooks/common/useCommonUtils.js | 46 ++++++++++++++++++++++- src/hooks/useCanvasEvent.js | 32 ++++++++++++++-- src/util/canvas-util.js | 6 +-- 6 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/common/common.js b/src/common/common.js index 4220e5f5..60a57c76 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -219,7 +219,8 @@ export const SAVE_KEY = [ 'originWidth', 'originHeight', 'skeletonLines', - 'skeleton' + 'skeleton', + 'viewportTransform', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 4b718c05..63dc523a 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useRef } from 'react' -import { useRecoilValue, useResetRecoilState } from 'recoil' +import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import QContextMenu from '@/components/common/context-menu/QContextMenu' import PanelBatchStatistics from '@/components/floor-plan/modal/panelBatch/PanelBatchStatistics' @@ -11,7 +11,7 @@ import { useCanvas } from '@/hooks/useCanvas' import { usePlan } from '@/hooks/usePlan' import { useContextMenu } from '@/hooks/useContextMenu' import { useCanvasConfigInitialize } from '@/hooks/common/useCanvasConfigInitialize' -import { currentMenuState } from '@/store/canvasAtom' +import { canvasZoomState, currentMenuState } from '@/store/canvasAtom' import { totalDisplaySelector } from '@/store/settingAtom' import { POLYGON_TYPE } from '@/common/common' import { FloorPlanContext } from '@/app/floor-plan/FloorPlanProvider' @@ -50,6 +50,7 @@ export default function CanvasFrame() { const resetSeriesState = useResetRecoilState(seriesState) const resetModelsState = useResetRecoilState(modelsState) const resetCompasDeg = useResetRecoilState(compasDegAtom) + const [zoom, setCanvasZoom] = useRecoilState(canvasZoomState) const resetSelectedModelsState = useResetRecoilState(selectedModelsState) const resetPcsCheckState = useResetRecoilState(pcsCheckState) const { handleModuleSelectionTotal } = useCanvasPopupStatusController() @@ -67,6 +68,13 @@ export default function CanvasFrame() { canvasLoadInit() //config된 상태로 캔버스 객체를 그린다 canvas?.renderAll() // 캔버스를 다시 그립니다. + if (canvas.viewportTransform) { + if (canvas.viewportTransform[0] !== 1) { + setCanvasZoom(Number((canvas.viewportTransform[0] * 100).toFixed(0))) + } + } + canvas.originViewPortTransform = canvas.viewportTransform + if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE).length > 0) { setTimeout(() => { setSelectedMenu('module') diff --git a/src/components/floor-plan/CanvasMenu.jsx b/src/components/floor-plan/CanvasMenu.jsx index 6ddd94c7..97d878a0 100644 --- a/src/components/floor-plan/CanvasMenu.jsx +++ b/src/components/floor-plan/CanvasMenu.jsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil' @@ -25,17 +25,18 @@ import { useCommonUtils } from '@/hooks/common/useCommonUtils' import useMenu from '@/hooks/common/useMenu' import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController' import { useAxios } from '@/hooks/useAxios' -import { canvasSettingState, canvasState, canvasZoomState, currentMenuState, verticalHorizontalModeState, currentCanvasPlanState } from '@/store/canvasAtom' +import { + canvasSettingState, + canvasState, + canvasZoomState, + currentCanvasPlanState, + currentMenuState, + verticalHorizontalModeState, +} from '@/store/canvasAtom' import { sessionStore } from '@/store/commonAtom' import { outerLinePointsState } from '@/store/outerLineAtom' import { appMessageStore, globalLocaleStore } from '@/store/localeAtom' -import { - addedRoofsState, - basicSettingState, - corridorDimensionSelector, - selectedRoofMaterialSelector, - settingModalFirstOptionsState, -} from '@/store/settingAtom' +import { addedRoofsState, basicSettingState, selectedRoofMaterialSelector, settingModalFirstOptionsState } from '@/store/settingAtom' import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom' import { commonUtilsState } from '@/store/commonUtilsAtom' import { menusState } from '@/store/menuAtom' @@ -51,6 +52,7 @@ import { QcastContext } from '@/app/QcastProvider' import { useRoofFn } from '@/hooks/common/useRoofFn' import { usePolygon } from '@/hooks/usePolygon' import { useTrestle } from '@/hooks/module/useTrestle' + export default function CanvasMenu(props) { const [currentCanvasPlan, setCurrentCanvasPlan] = useRecoilState(currentCanvasPlanState) const { selectedMenu, setSelectedMenu } = props @@ -515,7 +517,10 @@ export default function CanvasMenu(props) { if (createUser === 'T01' && sessionState.storeId !== 'T01') { setAllButtonStyles('none') } else { - setEstimateContextState({ tempFlg: estimateRecoilState.tempFlg, lockFlg: estimateRecoilState.lockFlg }) + setEstimateContextState({ + tempFlg: estimateRecoilState.tempFlg, + lockFlg: estimateRecoilState.lockFlg, + }) handleButtonStyles(estimateRecoilState.tempFlg, estimateRecoilState.lockFlg, estimateContextState.docNo) } } diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index 7c53f98c..c878bafd 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -31,8 +31,11 @@ export function useCommonUtils() { useEffect(() => { commonTextMode() if (commonUtils.dimension) { + generateTempGrid() commonDimensionMode() return + } else { + removeTempGrid() } if (commonUtils.distance) { commonDistanceMode() @@ -655,7 +658,7 @@ export function useCommonUtils() { if (obj.name === 'roof') { clonedObj.setCoords() clonedObj.fire('modified') - clonedObj.fire('polygonMoved') + // clonedObj.fire('polygonMoved') clonedObj.set({ direction: obj.direction, directionText: obj.directionText, @@ -905,6 +908,45 @@ export function useCommonUtils() { } } + const generateTempGrid = () => { + if (!canvas) return + + const objects = canvas.getObjects().filter((obj) => ['QPolygon'].includes(obj.type)) + const gridLines = [] + + objects.forEach((obj) => { + const lines = obj.lines + + lines.forEach((line) => { + const gridLine = new fabric.Line([line.x1, line.y1, line.x2, line.y2], { + stroke: 'gray', + strokeWidth: 1, + selectable: false, + evented: false, + opacity: 0.5, + name: 'tempGrid', + direction: line.x1 === line.x2 ? 'vertical' : 'horizontal', + visible: false, + }) + gridLines.push(gridLine) + }) + }) + + gridLines.forEach((line) => { + canvas.add(line) + }) + + canvas.renderAll() + } + + const removeTempGrid = () => { + if (!canvas) return + + const tempGrids = canvas.getObjects().filter((obj) => obj.name === 'tempGrid' && !obj.visible) + tempGrids.forEach((grid) => canvas.remove(grid)) + canvas.renderAll() + } + return { commonFunctions, dimensionSettings, @@ -916,5 +958,7 @@ export function useCommonUtils() { editText, changeDimensionExtendLine, deleteOuterLineObject, + generateTempGrid, + removeTempGrid, } } diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index d991a2cf..cc4ac608 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -402,7 +402,8 @@ export function useCanvasEvent() { } } else { zoom = canvasZoom - 10 - if (zoom < 10) { //50%->10% + if (zoom < 10) { + //50%->10% return } } @@ -412,8 +413,33 @@ export function useCanvasEvent() { const handleZoomClear = () => { setCanvasZoom(100) - canvas.set({ zoom: 1 }) - canvas.viewportTransform = [1, 0, 0, 1, 0, 0] + + zoomToAllObjects() + canvas.renderAll() + } + + const zoomToAllObjects = () => { + const objects = canvas.getObjects().filter((obj) => obj.visible) + if (objects.length === 0) return + + let minX = Infinity, + minY = Infinity + let maxX = -Infinity, + maxY = -Infinity + + objects.forEach((obj) => { + const bounds = obj.getBoundingRect() + minX = Math.min(minX, bounds.left) + minY = Math.min(minY, bounds.top) + maxX = Math.max(maxX, bounds.left + bounds.width) + maxY = Math.max(maxY, bounds.top + bounds.height) + }) + + const centerX = (minX + maxX) / 2 + const centerY = (minY + maxY) / 2 + const centerPoint = new fabric.Point(centerX, centerY) + + canvas.zoomToPoint(centerPoint, 1) canvas.renderAll() } diff --git a/src/util/canvas-util.js b/src/util/canvas-util.js index 243936da..b2ee57db 100644 --- a/src/util/canvas-util.js +++ b/src/util/canvas-util.js @@ -269,7 +269,7 @@ export const getDegreeByChon = (chon) => { * @returns {number} */ export const getChonByDegree = (degree) => { - return Number((Math.tan((degree * Math.PI) / 180) * 10).toFixed(1)) + return Number((Math.tan((degree * Math.PI) / 180) * 10).toFixed(2)) } /** @@ -1036,11 +1036,11 @@ export const getDegreeInOrientation = (degree) => { { min: -51, max: -37, value: -45 }, { min: -36, max: -22, value: -30 }, { min: -21, max: -7, value: -15 }, - { min: -6, max: 0, value: 0 } + { min: -6, max: 0, value: 0 }, ] // 해당 범위에 맞는 값 찾기 - const range = degreeRanges.find(range => degree >= range.min && degree <= range.max) + const range = degreeRanges.find((range) => degree >= range.min && degree <= range.max) return range ? range.value : degree } From eb315cae642bcc230028448838442bbf633c9d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=B0=BD=EC=88=98?= Date: Thu, 4 Dec 2025 10:51:40 +0900 Subject: [PATCH 2/5] =?UTF-8?q?style:=20=ED=9A=8C=EB=A1=9C=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=9D=EC=97=85=20=EC=B4=9D=20=ED=9A=8C=EB=A1=9C?= =?UTF-8?q?=EC=88=98=20select-box=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/circuitTrestle/step/StepUp.jsx | 8 +++++++- src/styles/_reset.scss | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx index b0222aa3..5b956cc2 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/StepUp.jsx @@ -649,7 +649,13 @@ export default function StepUp(props) { style={{ cursor: allocationType === 'auto' ? 'pointer' : 'default' }} > {item.serQty} - {item.paralQty} + + {/* 2025.12.04 select 추가 */} + + + {/* {item.paralQty} */} ) })} diff --git a/src/styles/_reset.scss b/src/styles/_reset.scss index c0c4a2cb..472243ed 100644 --- a/src/styles/_reset.scss +++ b/src/styles/_reset.scss @@ -460,7 +460,11 @@ button{ } } - +.table-select{ + height: 20px; + color: #fff !important; + font-size: 11px !important; +} // input .form-input{ label{ From 800453ff6b1c158bd45f6014598f9f1b1735ebfe Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Thu, 4 Dec 2025 14:28:30 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=ED=98=BC=ED=95=A9=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=ED=9A=8C=EB=A1=9C=20=EB=8C=80=EC=9D=91=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../circuitTrestle/CircuitTrestleSetting.jsx | 29 +++++- .../step/type/PassivityCircuitAllocation.jsx | 94 ++++++++++++++----- src/hooks/useCirCuitTrestle.js | 27 +++++- src/locales/ja.json | 1 + src/locales/ko.json | 1 + 5 files changed, 124 insertions(+), 28 deletions(-) diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 3827e116..f86a7ead 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -227,11 +227,33 @@ export default function CircuitTrestleSetting({ id }) { 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(), + pcsItemList: getPcsItemList(isMultiModule), } // 파워컨디셔너 추천 목록 조회 @@ -292,12 +314,12 @@ export default function CircuitTrestleSetting({ id }) { }) } else { // 회로 구성 가능 여부 체크 - getPcsVoltageChk({ ...params, pcsItemList: getSelectedPcsItemList() }).then((res) => { + getPcsVoltageChk({ ...params, pcsItemList: getSelectedPcsItemList(isMultiModule) }).then((res) => { if (res.resultCode === 'S') { // 회로 구성 가능 여부 체크 통과 시 승압설정 정보 조회 getPcsVoltageStepUpList({ ...params, - pcsItemList: getSelectedPcsItemList(), + pcsItemList: getSelectedPcsItemList(isMultiModule), }).then((res) => { if (res?.result.resultCode === 'S' && res?.data) { setTabNum(2) @@ -523,6 +545,7 @@ export default function CircuitTrestleSetting({ id }) { obj.circuit = null obj.pcsItemId = null obj.circuitNumber = null + obj.pcs = null }) setSelectedModels( JSON.parse(JSON.stringify(selectedModels)).map((model) => { diff --git a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx index 049efe10..22b0dd16 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx @@ -9,7 +9,7 @@ import { moduleStatisticsState } from '@/store/circuitTrestleAtom' import { fontSelector } from '@/store/fontAtom' import { selectedModuleState } from '@/store/selectedModuleOptions' import { circuitNumDisplaySelector } from '@/store/settingAtom' -import { useContext, useEffect, useState } from 'react' +import { useContext, useEffect, useRef, useState } from 'react' import { useRecoilValue } from 'recoil' import { normalizeDigits } from '@/util/input-utils' @@ -32,6 +32,7 @@ export default function PassivityCircuitAllocation(props) { const { header, rows, footer } = useRecoilValue(moduleStatisticsState) const [circuitNumber, setCircuitNumber] = useState(1) const [targetModules, setTargetModules] = useState([]) + const targetModulesRef = useRef([]) const { getPcsManualConfChk } = useMasterController() const isDisplayCircuitNumber = useRecoilValue(circuitNumDisplaySelector) const { setModuleStatisticsData } = useCircuitTrestle() @@ -59,6 +60,10 @@ export default function PassivityCircuitAllocation(props) { } }, []) + useEffect(() => { + targetModulesRef.current = targetModules + }, [targetModules]) + const handleTargetModules = (obj) => { if (!Array.isArray(targetModules)) { setTargetModules([]) @@ -79,6 +84,7 @@ export default function PassivityCircuitAllocation(props) { } const handleCircuitNumberFix = () => { + const pcsTpCd = selectedPcs.pcsTpCd // 실내집중형, 옥외멀티형 let uniqueCircuitNumbers = [ ...new Set( canvas @@ -91,13 +97,13 @@ export default function PassivityCircuitAllocation(props) { const surfaceList = targetModules.map((module) => { return canvas.getObjects().filter((obj) => obj.id === canvas.getObjects().filter((obj) => obj.id === module)[0].surfaceId)[0] }) + let surfaceType = {} + + surfaceList.forEach((surface) => { + surfaceType[`${surface.direction}-${surface.roofMaterial.pitch}`] = surface + }) if (surfaceList.length > 1) { - let surfaceType = {} - - surfaceList.forEach((surface) => { - surfaceType[`${surface.direction}-${surface.roofMaterial.pitch}`] = surface - }) if (Object.keys(surfaceType).length > 1) { swalFire({ text: getMessage('module.circuit.fix.not.same.roof.error'), @@ -107,6 +113,7 @@ export default function PassivityCircuitAllocation(props) { return } } + if (!circuitNumber || circuitNumber === 0) { swalFire({ text: getMessage('module.circuit.minimun.error'), @@ -114,31 +121,65 @@ export default function PassivityCircuitAllocation(props) { icon: 'warning', }) return - } else if (targetModules.length === 0) { + } + + if (targetModules.length === 0) { swalFire({ text: getMessage('module.not.found'), type: 'alert', icon: 'warning', }) return - } else if (selectedModels.length > 1) { - let result = false + } - uniqueCircuitNumbers.forEach((number) => { - if ( - number.split('-')[1] === circuitNumber + ')' && - number.split('-')[0] !== '(' + (selectedModels.findIndex((model) => model.id === selectedPcs.id) + 1) - ) { - result = true - } - }) - if (result) { - swalFire({ - text: getMessage('module.already.exist.error'), - type: 'alert', - icon: 'warning', + switch (pcsTpCd) { + case 'INDFCS': { + const originHaveThisPcsModules = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.pcs && obj.pcs.id === selectedPcs.id) + // 이미 해당 pcs로 설치된 모듈의 surface의 방향을 구한다. + const originSurfaceList = canvas + .getObjects() + .filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE && originHaveThisPcsModules.map((obj) => obj.surfaceId).includes(obj.id)) + + originSurfaceList.concat(originSurfaceList).forEach((surface) => { + surfaceType[`${surface.direction}-${surface.roofMaterial.pitch}`] = surface }) - return + + if (surfaceList.length > 1) { + if (Object.keys(surfaceType).length > 1) { + swalFire({ + text: getMessage('module.circuit.fix.not.same.roof.error'), + type: 'alert', + icon: 'warning', + }) + return + } + } + + break + } + case 'OUTDMULTI': { + if (selectedModels.length > 1) { + let result = false + + uniqueCircuitNumbers.forEach((number) => { + if ( + number.split('-')[1] === circuitNumber + ')' && + number.split('-')[0] !== '(' + (selectedModels.findIndex((model) => model.id === selectedPcs.id) + 1) + ) { + result = true + } + }) + if (result) { + swalFire({ + text: getMessage('module.already.exist.error'), + type: 'alert', + icon: 'warning', + }) + return + } + } } } @@ -189,6 +230,7 @@ export default function PassivityCircuitAllocation(props) { roofSurfaceId: surface.id, roofSurface: surface.direction, roofSurfaceIncl: +canvas.getObjects().filter((obj) => obj.id === surface.parentId)[0].pitch, + roofSurfaceNorthYn: surface.direction === 'north' ? 'Y' : 'N', moduleList: surface.modules.map((module) => { return { itemId: module.moduleInfo.itemId, @@ -270,6 +312,12 @@ export default function PassivityCircuitAllocation(props) { return } + targetModules.forEach((module) => { + const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE) + const targetModule = modules.find((obj) => obj.id === module) + targetModule.pcs = selectedPcs + }) + setTargetModules([]) setCircuitNumber(+circuitNumber + 1) setModuleStatisticsData() diff --git a/src/hooks/useCirCuitTrestle.js b/src/hooks/useCirCuitTrestle.js index a35d3c8c..83bb0b84 100644 --- a/src/hooks/useCirCuitTrestle.js +++ b/src/hooks/useCirCuitTrestle.js @@ -42,7 +42,18 @@ export function useCircuitTrestle(executeEffect = false) { } } // PCS 아이템 목록 - const getPcsItemList = () => { + const getPcsItemList = (isMultiModule = false) => { + if (isMultiModule) { + return models + .filter((model) => model.pcsTpCd !== 'INDFCS') + .map((model) => { + return { + itemId: model.itemId, + pcsMkrCd: model.pcsMkrCd, + pcsSerCd: model.pcsSerCd, + } + }) + } return models.map((model) => { return { itemId: model.itemId, @@ -53,7 +64,18 @@ export function useCircuitTrestle(executeEffect = false) { } // 선택된 PCS 아이템 목록 - const getSelectedPcsItemList = () => { + const getSelectedPcsItemList = (isMultiModule = false) => { + if (isMultiModule) { + return selectedModels + .filter((model) => model.pcsTpCd !== 'INDFCS') + .map((model) => { + return { + itemId: model.itemId, + pcsMkrCd: model.pcsMkrCd, + pcsSerCd: model.pcsSerCd, + } + }) + } return selectedModels.map((model) => { return { itemId: model.itemId, @@ -95,6 +117,7 @@ export function useCircuitTrestle(executeEffect = false) { uniqueId: module.id ? module.id : null, } }), + roofSurfaceNorthYn: obj.direction === 'north' ? 'Y' : 'N', } }) .filter((surface) => surface.moduleList.length > 0) diff --git a/src/locales/ja.json b/src/locales/ja.json index db27c2ef..e738ebaa 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1089,6 +1089,7 @@ "module.circuit.minimun.error": "回路番号は1以上の数値を入力してください。", "module.already.exist.error": "回路番号が同じで異なるパワーコンディショナのモジュールがあります。 別の回路番号を設定してください。", "module.circuit.fix.not.same.roof.error": "異なる屋根面のモジュールが選択されています。 モジュールの選択をや直してください。", + "module.circuit.indoor.focused.error": "混合モジュールと屋内集中PCSを組み合わせる場合は、手動回路割り当てのみ対応可能です。", "construction.length.difference": "屋根面工法をすべて選択してください。", "menu.validation.canvas.roof": "パネルを配置するには、屋根面を入力する必要があります。", "batch.object.outside.roof": "オブジェクトは屋根に設置する必要があります。", diff --git a/src/locales/ko.json b/src/locales/ko.json index 4f6601cd..bed23c44 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1089,6 +1089,7 @@ "module.circuit.minimun.error": "회로번호는 1 이상입력해주세요.", "module.already.exist.error": "회로번호가 같은 다른 파워 컨디셔너 모듈이 있습니다. 다른 회로번호를 설정하십시오.", "module.circuit.fix.not.same.roof.error": "다른 지붕면의 모듈이 선택되어 있습니다. 모듈 선택을 다시 하세요.", + "module.circuit.indoor.focused.error": "혼합 모듈과 실내 집중형 PCS를 조합하는 경우, 수동 회로 할당만 가능합니다.", "construction.length.difference": "지붕면 공법을 모두 선택하십시오.", "menu.validation.canvas.roof": "패널을 배치하려면 지붕면을 입력해야 합니다.", "batch.object.outside.roof": "오브젝트는 지붕내에 설치해야 합니다.", From eee74edf9e9a45e534970af14fbdfc84e3935518 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Thu, 4 Dec 2025 17:13:34 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=EC=99=B8=EB=B2=BD=EC=84=A0=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/setting01/FirstOption.jsx | 19 +++++++- .../roofcover/useRoofAllocationSetting.js | 45 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/components/floor-plan/modal/setting01/FirstOption.jsx b/src/components/floor-plan/modal/setting01/FirstOption.jsx index 51a52cdb..ef2d8e0b 100644 --- a/src/components/floor-plan/modal/setting01/FirstOption.jsx +++ b/src/components/floor-plan/modal/setting01/FirstOption.jsx @@ -1,8 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import { useMessage } from '@/hooks/useMessage' import { POLYGON_TYPE } from '@/common/common' import { useEvent } from '@/hooks/useEvent' import { useRoofFn } from '@/hooks/common/useRoofFn' +import { outlineDisplaySelector } from '@/store/settingAtom' +import { useRecoilValue } from 'recoil' export default function FirstOption(props) { const { getMessage } = useMessage() @@ -11,6 +13,7 @@ export default function FirstOption(props) { const { option1, option2, dimensionDisplay } = settingModalFirstOptions const { initEvent } = useEvent() const { setSurfaceShapePattern } = useRoofFn() + const outlineDisplay = useRecoilValue(outlineDisplaySelector) // 데이터를 최초 한 번만 조회 useEffect(() => { @@ -18,6 +21,13 @@ export default function FirstOption(props) { setSettingsDataSave({ ...settingsData }) }, []) + useEffect(() => { + const outline = canvas.getObjects().filter((obj) => obj.name === 'originRoofOuterLine') + outline.forEach((obj) => { + obj.visible = outlineDisplay + }) + }, [outlineDisplay]) + const onClickOption = async (item) => { let dimensionDisplay = settingModalFirstOptions?.dimensionDisplay let option1 = settingModalFirstOptions?.option1 @@ -58,7 +68,12 @@ export default function FirstOption(props) { // setSettingModalFirstOptions({ ...settingModalFirstOptions, option1: [...options] }) } - setSettingsData({ ...settingsData, option1: [...option1], option2: [...option2], dimensionDisplay: [...dimensionDisplay] }) + setSettingsData({ + ...settingsData, + option1: [...option1], + option2: [...option2], + dimensionDisplay: [...dimensionDisplay], + }) } // useEffect(() => { diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 799e3a72..8839516f 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -9,6 +9,7 @@ import { basicSettingState, correntObjectNoState, corridorDimensionSelector, + outlineDisplaySelector, roofDisplaySelector, roofMaterialsSelector, selectedRoofMaterialSelector, @@ -59,9 +60,11 @@ export function useRoofAllocationSetting(id) { const { saveCanvas } = usePlan() const [roofsStore, setRoofsStore] = useRecoilState(roofsState) const [moduleSelectionData, setModuleSelectionData] = useRecoilState(moduleSelectionDataState) + const outerLinePoints = useRecoilValue(outerLinePointsState) const resetPoints = useResetRecoilState(outerLinePointsState) const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector) const { changeCorridorDimensionText } = useText() + const outlineDisplay = useRecoilValue(outlineDisplaySelector) useEffect(() => { /** 배치면 초기설정에서 선택한 지붕재 배열 설정 */ @@ -303,11 +306,53 @@ export function useRoofAllocationSetting(id) { addPopup(popupId, 1, ) } else { apply() + //기존 지붕 선은 남겨둔다. + drawOriginRoofLine() resetPoints() + basicSettingSave() } } + const drawOriginRoofLine = () => { + // outerLinePoints 배열을 이용하여 빨간색 Line 객체들 생성 + if (outerLinePoints && outerLinePoints.length > 1) { + // 연속된 점들을 연결하여 라인 생성 + for (let i = 0; i < outerLinePoints.length - 1; i++) { + const point1 = outerLinePoints[i] + const point2 = outerLinePoints[i + 1] + + const line = new fabric.Line([point1.x, point1.y, point2.x, point2.y], { + stroke: 'black', + strokeDashArray: [5, 2], + strokeWidth: 1, + selectable: false, + name: 'originRoofOuterLine', + visible: outlineDisplay, + }) + + canvas.add(line) + } + + // 마지막 점과 첫 점을 연결하여 폐곡선 만들기 + if (outerLinePoints.length > 2) { + const lastPoint = outerLinePoints[outerLinePoints.length - 1] + const firstPoint = outerLinePoints[0] + + const closingLine = new fabric.Line([lastPoint.x, lastPoint.y, firstPoint.x, firstPoint.y], { + stroke: 'red', + strokeWidth: 2, + selectable: false, + name: 'originRoofOuterLine', + }) + + canvas.add(closingLine) + } + + canvas.renderAll() + } + } + /** * 지붕재 오른쪽 마우스 클릭 후 단일로 지붕재 변경 필요한 경우 */ From 28d26511de36f6d8e41cf2370f5a7de3d051c7aa Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 5 Dec 2025 15:36:56 +0900 Subject: [PATCH 5/5] =?UTF-8?q?=EB=B3=B5=EC=82=AC=20=ED=9B=84=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useCommonUtils.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/hooks/common/useCommonUtils.js b/src/hooks/common/useCommonUtils.js index c878bafd..9fe5a221 100644 --- a/src/hooks/common/useCommonUtils.js +++ b/src/hooks/common/useCommonUtils.js @@ -648,6 +648,7 @@ export function useCommonUtils() { lockMovementY: true, name: obj.name, editable: false, + selectable: true, // 복사된 객체 선택 가능하도록 설정 id: uuidv4(), //복사된 객체라 새로 따준다 }) @@ -656,19 +657,25 @@ export function useCommonUtils() { //배치면일 경우 if (obj.name === 'roof') { - clonedObj.setCoords() - clonedObj.fire('modified') - // clonedObj.fire('polygonMoved') + clonedObj.canvas = canvas // canvas 참조 설정 clonedObj.set({ direction: obj.direction, directionText: obj.directionText, roofMaterial: obj.roofMaterial, + stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정 + selectable: true, // 선택 가능하도록 설정 + evented: true, // 마우스 이벤트를 받을 수 있도록 설정 + isFixed: false, // containsPoint에서 특별 처리 방지 }) obj.lines.forEach((line, index) => { clonedObj.lines[index].set({ attributes: line.attributes }) }) + clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset) + clonedObj.fire('modified') + clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트 + canvas.setActiveObject(clonedObj) canvas.renderAll() addLengthText(clonedObj) //수치 추가 drawDirectionArrow(clonedObj) //방향 화살표 추가