From bff666914c8d74f8ab90e7ada80eb37c06bde775 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 7 Jan 2026 10:07:31 +0900 Subject: [PATCH 1/6] =?UTF-8?q?font=20=EC=84=A4=EC=A0=95=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=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/useFont.js | 170 +++++++++++++++++++----------------- 1 file changed, 92 insertions(+), 78 deletions(-) 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 } } -- 2.47.2 From 6872c6bb16fe1d78002f1754b55992ad41f8c5bb Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 7 Jan 2026 10:12:25 +0900 Subject: [PATCH 2/6] =?UTF-8?q?=EB=B0=B0=EC=B9=98=EB=A9=B4=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0,=20=EC=99=B8=EB=B2=BD=EC=84=A0=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EA=B8=B0=20=EA=B0=81=EB=8F=84=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/roofcover/useOuterLineWall.js | 2 +- src/hooks/surface/usePlacementShapeDrawing.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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': { -- 2.47.2 From 5ac023d72d5ae3f08dfe91724f8ee22b05336cc6 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 7 Jan 2026 10:35:24 +0900 Subject: [PATCH 3/6] =?UTF-8?q?[HANASYS=20DESIGN]=20=ED=9A=8C=EC=A0=84=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EC=9D=B4=EC=9A=A9=ED=96=88?= =?UTF-8?q?=EC=9D=84=20=EB=95=8C=EC=9D=98=20=EB=B6=88=EB=9F=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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() }) -- 2.47.2 From c499653e798f0d02fda912c2e8273306cf4c282d Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 7 Jan 2026 16:59:30 +0900 Subject: [PATCH 4/6] =?UTF-8?q?=EB=B6=81=EB=A9=B4=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=84=A4=EC=B9=98=20=EC=97=AC=EB=B6=80=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20=EB=B6=81=EB=A9=B4=20=EB=AA=A8=EB=93=88=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=20=EC=8B=9C=20=EB=8F=99=EB=A9=B4=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.js | 1 + .../step/type/PassivityCircuitAllocation.jsx | 2 ++ src/hooks/useCirCuitTrestle.js | 25 ++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) 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/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/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 -- 2.47.2 From 47de1ef61d7d2b7b75a41ed5196c83dbe6441e54 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 7 Jan 2026 17:49:37 +0900 Subject: [PATCH 5/6] =?UTF-8?q?azimuth:=20string=20->=20int=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useTrestle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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': { -- 2.47.2 From a5dc5caaf34660eb2c143d69d556751787c1a799 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 7 Jan 2026 17:51:11 +0900 Subject: [PATCH 6/6] =?UTF-8?q?=EA=B0=81=EB=8F=84=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/lineTypes/Angle.jsx | 128 +++++++++++++++++- 1 file changed, 122 insertions(+), 6 deletions(-) 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 }) { -- 2.47.2