diff --git a/src/common/common.js b/src/common/common.js index fd961063..6ecc08fc 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -224,6 +224,7 @@ export const SAVE_KEY = [ 'viewportTransform', 'outerLineFix', 'adjustRoofLines', + 'northModuleYn', ] export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype] diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index b6c299d0..dbe6a9e3 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -181,8 +181,27 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { return fabric.util.transformPoint(p, matrix) }) this.points = transformedPoints - const { left, top } = this.calcOriginCoords() - this.set('pathOffset', { x: left, y: top }) + + // 바운딩 박스 재계산 (width, height 업데이트 - fill 영역 수정) + const calcDim = this._calcDimensions({}) + this.width = calcDim.width + this.height = calcDim.height + + const newPathOffset = { + x: calcDim.left + this.width / 2, + y: calcDim.top + this.height / 2, + } + this.set('pathOffset', newPathOffset) + + // 변환을 points에 적용했으므로 left, top, angle, scale 모두 리셋 (이중 변환 방지) + this.set({ + left: newPathOffset.x, + top: newPathOffset.y, + angle: 0, + scaleX: 1, + scaleY: 1, + }) + this.setCoords() this.initLines() }) 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 720845f5..a25a1a37 100644 --- a/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/step/type/PassivityCircuitAllocation.jsx @@ -234,6 +234,7 @@ export default function PassivityCircuitAllocation(props) { setSelectedPcs(tempSelectedPcs) canvas.add(moduleCircuitText) }) + const roofSurfaceList = canvas .getObjects() @@ -244,6 +245,7 @@ export default function PassivityCircuitAllocation(props) { roofSurface: surface.direction, roofSurfaceIncl: +canvas.getObjects().filter((obj) => obj.id === surface.parentId)[0].pitch, roofSurfaceNorthYn: surface.direction === 'north' ? 'Y' : 'N', + roofSurfaceNorthModuleYn: surface.northModuleYn, moduleList: surface.modules.map((module) => { return { itemId: module.moduleInfo.itemId, diff --git a/src/components/floor-plan/modal/lineTypes/Angle.jsx b/src/components/floor-plan/modal/lineTypes/Angle.jsx index 0faad2a4..1b05993f 100644 --- a/src/components/floor-plan/modal/lineTypes/Angle.jsx +++ b/src/components/floor-plan/modal/lineTypes/Angle.jsx @@ -2,11 +2,110 @@ import Image from 'next/image' import { useMessage } from '@/hooks/useMessage' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' import { CalculatorInput } from '@/components/common/input/CalcInput' +import { useEffect } from 'react' export default function Angle({ props }) { const { getMessage } = useMessage() const { angle1, setAngle1, angle1Ref, length1, setLength1, length1Ref } = props + const resetCalculatorValue = () => { + // 1. 즉시 상태를 0으로 변경 + setLength1(0); + + if (length1Ref.current) { + // 2. input에 포커스를 주어 계산기 UI가 떠있는 상태를 유지하게 함 + length1Ref.current.focus(); + length1Ref.current.value = ''; + + // 3. 약간의 시간차를 두어 계산기 UI 내부의 버튼을 찾음 + setTimeout(() => { + const acButton = document.querySelector('.keypad-btn.ac, .btn-ac') || + Array.from(document.querySelectorAll('button')).find(el => el.textContent === 'AC'); + + if (acButton) { + acButton.click(); + } else { + // 버튼을 못 찾으면 강제로 input 이벤트와 Enter/Escape 등을 시뮬레이션 + length1Ref.current.dispatchEvent(new Event('input', { bubbles: true })); + } + // 다시 포커스를 주어 키패드가 유지되도록 함 (필요시) + length1Ref.current.focus(); + }, 10); + } + } + + const resetCalculatorValue2 = () => { + // 1. 즉시 상태를 0으로 변경 + setAngle1(0); + + if (angle1Ref.current) { + // 2. input에 포커스를 주어 계산기 UI가 떠있는 상태를 유지하게 함 + angle1Ref.current.focus(); + angle1Ref.current.value = ''; + + // 3. 약간의 시간차를 두어 계산기 UI 내부의 버튼을 찾음 + setTimeout(() => { + const acButton = document.querySelector('.keypad-btn.ac, .btn-ac') || + Array.from(document.querySelectorAll('button')).find(el => el.textContent === 'AC'); + + if (acButton) { + acButton.click(); + } else { + // 버튼을 못 찾으면 강제로 input 이벤트와 Enter/Escape 등을 시뮬레이션 + angle1Ref.current.dispatchEvent(new Event('input', { bubbles: true })); + } + // 다시 포커스를 주어 키패드가 유지되도록 함 (필요시) + angle1Ref.current.focus(); + }, 10); + } + } + + // 키보드 입력 처리 + useEffect(() => { + const handleKeyDown = (e) => { + + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { + // 계산기 값이 확정된 후 초기화하기 위해 약간의 지연을 줌 + setTimeout(() => { + resetCalculatorValue(); + }, 0); + return; + } + + // 계산기 키패드가 보이는지 확인 + const keypadVisible = document.querySelector('.keypad-container') + + // 계산기 키패드가 보이면 계산기가 처리하도록 함 + if (keypadVisible) { + return + } + + // 이미 계산기 input에 포커스가 있으면 계산기가 처리하도록 함 + if (document.activeElement && document.activeElement.classList.contains('calculator-input')) { + return + } + + // 엔터키는 계산기가 숨겨진 상태에서만 페이지가 처리 + if (e.key === 'Enter') { + // 엔터키를 페이지 레벨로 전달 (useOuterLineWall.js에서 처리) + return + } + + // 숫자 키가 입력되면 계산기 input에 포커스만 주기 + if (/^[0-9+\-*\/=.]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete') { + const calcInput = document.querySelector('.calculator-input') + if (calcInput) { + // 포커스만 주고 이벤트는 preventDefault 하지 않음 + calcInput.focus() + calcInput.click() + } + } + } + // capture: true로 설정하여 다른 핸들러보다 먼저 실행 + document.addEventListener('keydown', handleKeyDown, { capture: true }) + return () => document.removeEventListener('keydown', handleKeyDown, { capture: true }) + }, []) + return ( <>
@@ -31,19 +130,33 @@ export default function Angle({ props }) { className="input-origin block" value={angle1} ref={angle1Ref} - onChange={(value) => setAngle1(value)} + onChange={(value) => {setAngle1(value) + + // Convert to number and ensure it's within -180 to 180 range + const numValue = parseInt(value, 10); + if (!isNaN(numValue)) { + const clampedValue = Math.min(180, Math.max(-180, numValue)); + setAngle1(String(clampedValue)); + } else { + setAngle1(value); + } + } + } placeholder="45" onFocus={() => (angle1Ref.current.value = '')} options={{ - allowNegative: false, - allowDecimal: true + allowNegative: true, + allowDecimal: false }} />
@@ -77,8 +190,11 @@ export default function Angle({ props }) { diff --git a/src/hooks/common/useFont.js b/src/hooks/common/useFont.js index 3db63427..45ca8200 100644 --- a/src/hooks/common/useFont.js +++ b/src/hooks/common/useFont.js @@ -1,7 +1,19 @@ import { useRecoilValue } from 'recoil' import { canvasState } from '@/store/canvasAtom' import { fontSelector } from '@/store/fontAtom' -import { useEffect } from 'react' +import { useCallback, useEffect } from 'react' + +/** 폰트 타입별 캔버스 오브젝트 이름 매핑 */ +const FONT_TYPE_TO_OBJ_NAME = { + commonText: 'commonText', + dimensionLineText: 'dimensionLineText', + flowText: 'flowText', + lengthText: 'lengthText', + circuitNumberText: 'circuitNumber', +} + +/** 캔버스 오브젝트 이름 → 폰트 타입 역매핑 */ +const OBJ_NAME_TO_FONT_TYPE = Object.fromEntries(Object.entries(FONT_TYPE_TO_OBJ_NAME).map(([k, v]) => [v, k])) export function useFont() { const canvas = useRecoilValue(canvasState) @@ -11,96 +23,98 @@ export function useFont() { const lengthText = useRecoilValue(fontSelector('lengthText')) const circuitNumberText = useRecoilValue(fontSelector('circuitNumberText')) + /** 폰트 타입별 설정 매핑 */ + const fontSettings = { + commonText, + dimensionLineText, + flowText, + lengthText, + circuitNumberText, + } + + /** + * 타입별 폰트 설정을 캔버스 오브젝트에 적용하는 공통 함수 + * @param {string} type - 폰트 타입 (commonText, dimensionLineText, flowText, lengthText, circuitNumberText) + * @param {number} delay - 적용 지연 시간 (ms), 기본값 200 + */ + const changeFontByType = useCallback( + (type, delay = 200) => { + const fontSetting = fontSettings[type] + const objName = FONT_TYPE_TO_OBJ_NAME[type] + + if (!fontSetting || !objName) { + console.warn(`Invalid font type: ${type}`) + return + } + + setTimeout(() => { + if (canvas && fontSetting.fontWeight?.value) { + const textObjs = canvas.getObjects().filter((obj) => obj.name === objName) + textObjs.forEach((obj) => { + obj.set({ + fontFamily: fontSetting.fontFamily.value, + fontWeight: fontSetting.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', + fontStyle: fontSetting.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', + fontSize: fontSetting.fontSize.value, + fill: fontSetting.fontColor.value, + }) + }) + canvas.renderAll() + } + }, delay) + }, + [canvas, fontSettings], + ) + + const changeAllFonts = () => { + changeFontByType('commonText') + changeFontByType('dimensionLineText') + changeFontByType('flowText') + changeFontByType('lengthText') + changeFontByType('circuitNumberText') + } + + /** 각 폰트 타입별 useEffect */ useEffect(() => { - setTimeout(() => { - if (canvas && commonText.fontWeight.value) { - const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'commonText') - textObjs.forEach((obj) => { - obj.set({ - fontFamily: commonText.fontFamily.value, - fontWeight: commonText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: commonText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: commonText.fontSize.value, - fill: commonText.fontColor.value, - }) - }) - canvas.renderAll() - }}, 200) + changeFontByType('commonText') }, [commonText]) useEffect(() => { - setTimeout(() => { - if (canvas && dimensionLineText.fontWeight.value) { - const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'dimensionLineText') - textObjs.forEach((obj) => { - obj.set({ - fontFamily: dimensionLineText.fontFamily.value, - fontWeight: dimensionLineText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: dimensionLineText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: dimensionLineText.fontSize.value, - fill: dimensionLineText.fontColor.value, - }) - }) - canvas.renderAll() - } - }, 200) - + changeFontByType('dimensionLineText') }, [dimensionLineText]) useEffect(() => { - setTimeout(() => { - if (canvas && flowText.fontWeight.value) { - const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'flowText') - textObjs.forEach((obj) => { - obj.set({ - fontFamily: flowText.fontFamily.value, - fontWeight: flowText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: flowText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: flowText.fontSize.value, - fill: flowText.fontColor.value, - }) - }) - canvas.renderAll() - } - }, 200) - + changeFontByType('flowText') }, [flowText]) useEffect(() => { - setTimeout(() => { - if (canvas && lengthText.fontWeight.value) { - const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'lengthText') - textObjs.forEach((obj) => { - obj.set({ - fontFamily: lengthText.fontFamily.value, - fontWeight: lengthText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: lengthText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: lengthText.fontSize.value, - fill: lengthText.fontColor.value, - }) - }) - canvas.renderAll() - } - }, 200) + changeFontByType('lengthText') }, [lengthText]) useEffect(() => { - setTimeout(() => { - if (canvas && circuitNumberText.fontWeight.value) { - const textObjs = canvas?.getObjects().filter((obj) => obj.name === 'circuitNumber') - textObjs.forEach((obj) => { - obj.set({ - fontFamily: circuitNumberText.fontFamily.value, - fontWeight: circuitNumberText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', - fontStyle: circuitNumberText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', - fontSize: circuitNumberText.fontSize.value, - fill: circuitNumberText.fontColor.value, - }) - }) - canvas.renderAll() - } - }, 200) + changeFontByType('circuitNumberText') }, [circuitNumberText]) - return {} + /** 캔버스에 텍스트 오브젝트 추가 시 자동으로 폰트 적용 */ + useEffect(() => { + if (!canvas) return + + const handleObjectAdded = (e) => { + const obj = e.target + if (!obj?.name) return + + const fontType = OBJ_NAME_TO_FONT_TYPE[obj.name] + if (fontType) { + changeFontByType(fontType, 0) + } + } + + canvas.on('object:added', handleObjectAdded) + + return () => { + canvas.off('object:added', handleObjectAdded) + } + }, [canvas, changeFontByType]) + + return { changeFontByType, changeAllFonts } } diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 9e4415bd..658c8c22 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -830,7 +830,7 @@ export const useTrestle = () => { return -surfaceCompass } - let resultAzimuth = moduleCompass + let resultAzimuth = parseInt(moduleCompass, 10) switch (direction) { case 'south': { diff --git a/src/hooks/roofcover/useOuterLineWall.js b/src/hooks/roofcover/useOuterLineWall.js index dd304865..850a761a 100644 --- a/src/hooks/roofcover/useOuterLineWall.js +++ b/src/hooks/roofcover/useOuterLineWall.js @@ -812,7 +812,7 @@ export function useOuterLineWall(id, propertiesId) { if (points.length === 0) { return } - enterCheck(e) + // enterCheck(e) const key = e.key switch (key) { case 'Enter': { diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index 629aae6e..3dfea465 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -815,7 +815,7 @@ export function usePlacementShapeDrawing(id) { if (points.length === 0) { return } - enterCheck(e) + // enterCheck(e) const key = e.key switch (key) { case 'Enter': { diff --git a/src/hooks/useCirCuitTrestle.js b/src/hooks/useCirCuitTrestle.js index 0c332ec8..5bfab3ce 100644 --- a/src/hooks/useCirCuitTrestle.js +++ b/src/hooks/useCirCuitTrestle.js @@ -99,6 +99,12 @@ export function useCircuitTrestle(executeEffect = false) { // 지붕면 목록 const getRoofSurfaceList = () => { const roofSurfaceList = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) + + roofSurfaceList.forEach((roofSurface) => { + const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.surfaceId === roofSurface.id) + roofSurface.northModuleYn = modules.every((module) => module.moduleInfo.northModuleYn === 'Y') ? 'Y' : 'N' + }) + roofSurfaceList.sort((a, b) => a.left - b.left || b.top - a.top) const result = roofSurfaceList @@ -119,6 +125,7 @@ export function useCircuitTrestle(executeEffect = false) { } }), roofSurfaceNorthYn: obj.direction === 'north' ? 'Y' : 'N', + roofSurfaceNorthModuleYn: obj.northModuleYn, } }) .filter((surface) => surface.moduleList.length > 0) @@ -139,11 +146,14 @@ export function useCircuitTrestle(executeEffect = false) { let remaining = [...arr] while (remaining.length > 0) { - const { roofSurface, roofSurfaceIncl } = remaining[0] - const key = `${roofSurface}|${roofSurfaceIncl}` + const { roofSurface, roofSurfaceIncl, roofSurfaceNorthModuleYn } = remaining[0] + const key = `${roofSurface}|${roofSurfaceIncl}|${roofSurfaceNorthModuleYn}` // 해당 그룹 추출 - const group = remaining.filter((item) => item.roofSurface === roofSurface && item.roofSurfaceIncl === roofSurfaceIncl) + const group = remaining.filter( + (item) => + item.roofSurface === roofSurface && item.roofSurfaceIncl === roofSurfaceIncl && item.roofSurfaceNorthModuleYn === roofSurfaceNorthModuleYn, + ) // 이미 처리했는지 체크 후 저장 if (!seen.has(key)) { @@ -152,7 +162,14 @@ export function useCircuitTrestle(executeEffect = false) { } // remaining에서 제거 - remaining = remaining.filter((item) => !(item.roofSurface === roofSurface && item.roofSurfaceIncl === roofSurfaceIncl)) + remaining = remaining.filter( + (item) => + !( + item.roofSurface === roofSurface && + item.roofSurfaceIncl === roofSurfaceIncl && + item.roofSurfaceNorthModuleYn === roofSurfaceNorthModuleYn + ), + ) } return result