Merge pull request 'dev' (#556) from dev into dev-deploy

Reviewed-on: #556
This commit is contained in:
ysCha 2026-01-07 17:53:14 +09:00
commit 41d5bcd22f
9 changed files with 262 additions and 93 deletions

View File

@ -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]

View File

@ -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()
})

View File

@ -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,

View File

@ -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 (
<>
<div className="outline-wrap">
@ -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
}}
/>
</div>
<button
className="reset-btn"
onClick={(e) => {
setAngle1(0)
e.preventDefault();
e.stopPropagation();
resetCalculatorValue2()
}}
></button>
</div>
@ -77,8 +190,11 @@ export default function Angle({ props }) {
</div>
<button
className="reset-btn"
onClick={() => {
setLength1(0)
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
resetCalculatorValue()
}}
></button>
</div>

View File

@ -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 }
}

View File

@ -830,7 +830,7 @@ export const useTrestle = () => {
return -surfaceCompass
}
let resultAzimuth = moduleCompass
let resultAzimuth = parseInt(moduleCompass, 10)
switch (direction) {
case 'south': {

View File

@ -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': {

View File

@ -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': {

View File

@ -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