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/common/select/QSelectBox.jsx b/src/components/common/select/QSelectBox.jsx
index dbb3c285..0e77ccca 100644
--- a/src/components/common/select/QSelectBox.jsx
+++ b/src/components/common/select/QSelectBox.jsx
@@ -96,7 +96,7 @@ export default function QSelectBox({
title={tagTitle}
>
{selected}
-
+
{options?.length > 0 &&
options?.map((option, index) => (
- handleClickSelectOption(option)}>
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 9498b036..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,85 +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(() => {
- 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()
- }
+ changeFontByType('commonText')
}, [commonText])
useEffect(() => {
- 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()
- }
+ changeFontByType('dimensionLineText')
}, [dimensionLineText])
useEffect(() => {
- 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()
- }
+ changeFontByType('flowText')
}, [flowText])
useEffect(() => {
- 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()
- }
+ changeFontByType('lengthText')
}, [lengthText])
useEffect(() => {
- 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()
- }
+ 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
diff --git a/src/hooks/useLine.js b/src/hooks/useLine.js
index 58f87e7d..66dff769 100644
--- a/src/hooks/useLine.js
+++ b/src/hooks/useLine.js
@@ -13,6 +13,7 @@ import { basicSettingState } from '@/store/settingAtom'
import { calcLineActualSize } from '@/util/qpolygon-utils'
import { getDegreeByChon } from '@/util/canvas-util'
import { useText } from '@/hooks/useText'
+import { fontSelector } from '@/store/fontAtom'
export const useLine = () => {
const canvas = useRecoilValue(canvasState)
@@ -23,14 +24,15 @@ export const useLine = () => {
const angleUnit = useRecoilValue(showAngleUnitSelector)
const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
const globalPitch = useRecoilValue(globalPitchState)
+ const lengthText = useRecoilValue(fontSelector('lengthText'))
const { changeCorridorDimensionText } = useText()
const addLine = (points = [], options) => {
const line = new QLine(points, {
...options,
- fontSize: fontSize,
- fontFamily: fontFamily,
+ fontSize: lengthText.fontSize.value,
+ fontFamily: lengthText.fontFamily.value,
})
if (line.length < 1) {