Merge branch 'dev' into feature/design-remake
This commit is contained in:
commit
340c7669af
@ -216,6 +216,8 @@ export const SAVE_KEY = [
|
||||
'isMultipleOf45',
|
||||
'from',
|
||||
'originColor',
|
||||
'originWidth',
|
||||
'originHeight',
|
||||
]
|
||||
|
||||
export const OBJECT_PROTOTYPE = [fabric.Line.prototype, fabric.Polygon.prototype, fabric.Triangle.prototype, fabric.Group.prototype]
|
||||
|
||||
@ -2,433 +2,439 @@ import React, { useState, useRef, useEffect, forwardRef } from 'react'
|
||||
import { createCalculator } from '@/util/calc-utils'
|
||||
import '@/styles/calc.scss'
|
||||
|
||||
export const CalculatorInput = forwardRef(({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder}, ref) => {
|
||||
const [showKeypad, setShowKeypad] = useState(false)
|
||||
const [displayValue, setDisplayValue] = useState(value || '0')
|
||||
const [hasOperation, setHasOperation] = useState(false)
|
||||
const calculatorRef = useRef(createCalculator(options))
|
||||
const containerRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
export const CalculatorInput = forwardRef(
|
||||
({ value, onChange, label, options = {}, id, className = 'calculator-input', readOnly = false, placeholder }, ref) => {
|
||||
const [showKeypad, setShowKeypad] = useState(false)
|
||||
const [displayValue, setDisplayValue] = useState(value || '0')
|
||||
const [hasOperation, setHasOperation] = useState(false)
|
||||
const calculatorRef = useRef(createCalculator(options))
|
||||
const containerRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
// 외부 ref와 내부 ref를 동기화
|
||||
useEffect(() => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(inputRef.current)
|
||||
} else {
|
||||
ref.current = inputRef.current
|
||||
}
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
// Sync displayValue with value prop
|
||||
useEffect(() => {
|
||||
setDisplayValue(value || '0')
|
||||
}, [value])
|
||||
|
||||
// 클릭 외부 감지
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setShowKeypad(false)
|
||||
if (hasOperation) {
|
||||
// If there's an operation in progress, compute the result when losing focus
|
||||
handleCompute()
|
||||
// 외부 ref와 내부 ref를 동기화
|
||||
useEffect(() => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(inputRef.current)
|
||||
} else {
|
||||
ref.current = inputRef.current
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [value, onChange, hasOperation])
|
||||
// Sync displayValue with value prop
|
||||
useEffect(() => {
|
||||
setDisplayValue(value || '0')
|
||||
}, [value])
|
||||
|
||||
// 숫자 입력 처리 함수 수정
|
||||
const handleNumber = (num) => {
|
||||
const calculator = calculatorRef.current
|
||||
let newDisplayValue = ''
|
||||
|
||||
if (hasOperation) {
|
||||
// 연산자 이후 숫자 입력 시
|
||||
if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) {
|
||||
calculator.currentOperand = num.toString()
|
||||
calculator.shouldResetDisplay = false
|
||||
} else {
|
||||
calculator.currentOperand = (calculator.currentOperand || '') + num
|
||||
// 클릭 외부 감지
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
||||
setShowKeypad(false)
|
||||
if (hasOperation) {
|
||||
// If there's an operation in progress, compute the result when losing focus
|
||||
handleCompute()
|
||||
}
|
||||
}
|
||||
}
|
||||
newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
} else {
|
||||
// 첫 번째 숫자 입력 시
|
||||
if (displayValue === '0' || calculator.shouldResetDisplay) {
|
||||
calculator.currentOperand = num.toString()
|
||||
calculator.shouldResetDisplay = false
|
||||
newDisplayValue = calculator.currentOperand
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [value, onChange, hasOperation])
|
||||
|
||||
// 숫자 입력 처리 함수 수정
|
||||
const handleNumber = (num) => {
|
||||
const calculator = calculatorRef.current
|
||||
let newDisplayValue = ''
|
||||
|
||||
if (hasOperation) {
|
||||
// 연산자 이후 숫자 입력 시
|
||||
if (calculator.currentOperand === '0' || calculator.shouldResetDisplay) {
|
||||
calculator.currentOperand = num.toString()
|
||||
calculator.shouldResetDisplay = false
|
||||
} else {
|
||||
calculator.currentOperand = (calculator.currentOperand || '') + num
|
||||
}
|
||||
newDisplayValue = calculator.previousOperand + calculator.operation + calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(calculator.currentOperand)
|
||||
}
|
||||
} else {
|
||||
calculator.currentOperand = (calculator.currentOperand || '') + num
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(newDisplayValue)
|
||||
// 첫 번째 숫자 입력 시
|
||||
if (displayValue === '0' || calculator.shouldResetDisplay) {
|
||||
calculator.currentOperand = num.toString()
|
||||
calculator.shouldResetDisplay = false
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(calculator.currentOperand)
|
||||
}
|
||||
} else {
|
||||
calculator.currentOperand = (calculator.currentOperand || '') + num
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(newDisplayValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = newDisplayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// 텍스트 끝으로 스크롤
|
||||
inputRef.current.scrollLeft = inputRef.current.scrollWidth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 포커스와 커서 위치 설정 (새로운 값의 길이로 설정)
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = newDisplayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
// 연산자 처리 함수 수정
|
||||
const handleOperation = (operation) => {
|
||||
const calculator = calculatorRef.current
|
||||
let newDisplayValue = ''
|
||||
|
||||
// 연산자 처리 함수 수정
|
||||
const handleOperation = (operation) => {
|
||||
const calculator = calculatorRef.current
|
||||
let newDisplayValue = ''
|
||||
|
||||
// 현재 입력된 값이 없으면 이전 값 사용 (연속 연산 시)
|
||||
if (!calculator.currentOperand && calculator.previousOperand) {
|
||||
calculator.operation = operation
|
||||
newDisplayValue = calculator.previousOperand + operation
|
||||
setDisplayValue(newDisplayValue)
|
||||
setHasOperation(true)
|
||||
} else if (hasOperation) {
|
||||
// 이미 연산자가 있는 경우, 계산 실행
|
||||
const result = calculator.compute()
|
||||
if (result !== undefined) {
|
||||
calculator.previousOperand = result.toString()
|
||||
// 현재 입력된 값이 없으면 이전 값 사용 (연속 연산 시)
|
||||
if (!calculator.currentOperand && calculator.previousOperand) {
|
||||
calculator.operation = operation
|
||||
newDisplayValue = calculator.previousOperand + operation
|
||||
setDisplayValue(newDisplayValue)
|
||||
setHasOperation(true)
|
||||
} else if (hasOperation) {
|
||||
// 이미 연산자가 있는 경우, 계산 실행
|
||||
const result = calculator.compute()
|
||||
if (result !== undefined) {
|
||||
calculator.previousOperand = result.toString()
|
||||
calculator.operation = operation
|
||||
calculator.currentOperand = ''
|
||||
newDisplayValue = calculator.previousOperand + operation
|
||||
setDisplayValue(newDisplayValue)
|
||||
}
|
||||
} else {
|
||||
// 첫 번째 연산자 입력 시
|
||||
calculator.previousOperand = calculator.currentOperand || '0'
|
||||
calculator.operation = operation
|
||||
calculator.currentOperand = ''
|
||||
setHasOperation(true)
|
||||
newDisplayValue = calculator.previousOperand + operation
|
||||
setDisplayValue(newDisplayValue)
|
||||
}
|
||||
} else {
|
||||
// 첫 번째 연산자 입력 시
|
||||
calculator.previousOperand = calculator.currentOperand || '0'
|
||||
calculator.operation = operation
|
||||
calculator.currentOperand = ''
|
||||
setHasOperation(true)
|
||||
newDisplayValue = calculator.previousOperand + operation
|
||||
setDisplayValue(newDisplayValue)
|
||||
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = newDisplayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// 텍스트 끝으로 스크롤
|
||||
inputRef.current.scrollLeft = inputRef.current.scrollWidth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 포커스와 커서 위치 설정 (새로운 값의 길이로 설정)
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = newDisplayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// AC 버튼 클릭 핸들러
|
||||
const handleClear = () => {
|
||||
const calculator = calculatorRef.current
|
||||
const newValue = calculator.clear()
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(false)
|
||||
onChange(displayValue)
|
||||
// 포커스와 커서 위치 설정
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = displayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// Ensure focus is maintained
|
||||
inputRef.current.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 계산 실행 함수 수정
|
||||
const handleCompute = (fromEnterKey = false) => {
|
||||
const calculator = calculatorRef.current
|
||||
if (!hasOperation || !calculator.currentOperand) return
|
||||
|
||||
const result = calculator.compute()
|
||||
if (result !== undefined) {
|
||||
const resultStr = result.toString()
|
||||
setDisplayValue(resultStr)
|
||||
setHasOperation(false)
|
||||
// Only call onChange with the final result
|
||||
onChange(resultStr)
|
||||
|
||||
// 엔터키로 호출된 경우 포커스 설정하지 않음
|
||||
if (!fromEnterKey) {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = resultStr.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEL 버튼 클릭 핸들러
|
||||
const handleDelete = () => {
|
||||
const calculator = calculatorRef.current
|
||||
const newValue = calculator.deleteNumber()
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(!!calculator.operation)
|
||||
onChange(displayValue)
|
||||
// 포커스와 커서 위치 설정
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = displayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// Ensure focus is maintained
|
||||
inputRef.current.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// input의 onChange 이벤트 처리 - 허용된 계산기 입력만 처리
|
||||
const handleInputChange = (e) => {
|
||||
if (readOnly) return
|
||||
|
||||
const inputValue = e.target.value
|
||||
|
||||
// 허용된 문자만 필터링 (숫자, 연산자, 소수점)
|
||||
const filteredValue = inputValue.replace(/[^0-9+\-×÷.]/g, '')
|
||||
|
||||
// 연산자 연속 입력 방지
|
||||
const lastChar = filteredValue[filteredValue.length - 1]
|
||||
const prevChar = filteredValue[filteredValue.length - 2]
|
||||
|
||||
if (['+', '×', '÷'].includes(lastChar) && ['+', '-', '×', '÷', '.'].includes(prevChar)) {
|
||||
// 연산자나 소수점이 연속으로 입력된 경우 이전 문자 유지
|
||||
return
|
||||
}
|
||||
|
||||
// 소수점 중복 입력 방지
|
||||
const parts = filteredValue.split(/[+\-×÷]/)
|
||||
if (parts[parts.length - 1].split('.').length > 2) {
|
||||
// 한 숫자에 소수점이 2개 이상인 경우
|
||||
return
|
||||
}
|
||||
|
||||
setDisplayValue(filteredValue)
|
||||
|
||||
// 계산기 상태 업데이트
|
||||
if (filteredValue !== displayValue) {
|
||||
// AC 버튼 클릭 핸들러
|
||||
const handleClear = () => {
|
||||
const calculator = calculatorRef.current
|
||||
const hasOperation = /[+\-×÷]/.test(filteredValue)
|
||||
const newValue = calculator.clear()
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(false)
|
||||
onChange(displayValue)
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = displayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// 텍스트 끝으로 스크롤
|
||||
inputRef.current.scrollLeft = inputRef.current.scrollWidth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (hasOperation) {
|
||||
const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
|
||||
calculator.previousOperand = operand1 || ''
|
||||
calculator.operation = operator || ''
|
||||
calculator.currentOperand = operand2 || ''
|
||||
setHasOperation(true)
|
||||
} else {
|
||||
calculator.currentOperand = filteredValue
|
||||
// 계산 실행 함수 수정
|
||||
const handleCompute = (fromEnterKey = false) => {
|
||||
const calculator = calculatorRef.current
|
||||
if (!hasOperation || !calculator.currentOperand) return
|
||||
|
||||
const result = calculator.compute()
|
||||
if (result !== undefined) {
|
||||
const resultStr = result.toString()
|
||||
setDisplayValue(resultStr)
|
||||
setHasOperation(false)
|
||||
// Only call onChange with the final result
|
||||
onChange(resultStr)
|
||||
|
||||
// 엔터키로 호출된 경우 포커스 설정하지 않음
|
||||
if (!fromEnterKey) {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = resultStr.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DEL 버튼 클릭 핸들러
|
||||
const handleDelete = () => {
|
||||
const calculator = calculatorRef.current
|
||||
const newValue = calculator.deleteNumber()
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(!!calculator.operation)
|
||||
onChange(displayValue)
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
const len = displayValue.length
|
||||
inputRef.current.setSelectionRange(len, len)
|
||||
// 텍스트 끝으로 스크롤
|
||||
inputRef.current.scrollLeft = inputRef.current.scrollWidth
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// input의 onChange 이벤트 처리 - 허용된 계산기 입력만 처리
|
||||
const handleInputChange = (e) => {
|
||||
if (readOnly) return
|
||||
|
||||
const inputValue = e.target.value
|
||||
|
||||
// 허용된 문자만 필터링 (숫자, 연산자, 소수점)
|
||||
const filteredValue = inputValue.replace(/[^0-9+\-×÷.]/g, '')
|
||||
|
||||
// 연산자 연속 입력 방지
|
||||
const lastChar = filteredValue[filteredValue.length - 1]
|
||||
const prevChar = filteredValue[filteredValue.length - 2]
|
||||
|
||||
if (['+', '×', '÷'].includes(lastChar) && ['+', '-', '×', '÷', '.'].includes(prevChar)) {
|
||||
// 연산자나 소수점이 연속으로 입력된 경우 이전 문자 유지
|
||||
return
|
||||
}
|
||||
|
||||
onChange(filteredValue)
|
||||
}
|
||||
}
|
||||
// 소수점 중복 입력 방지
|
||||
const parts = filteredValue.split(/[+\-×÷]/)
|
||||
if (parts[parts.length - 1].split('.').length > 2) {
|
||||
// 한 숫자에 소수점이 2개 이상인 경우
|
||||
return
|
||||
}
|
||||
|
||||
setDisplayValue(filteredValue)
|
||||
|
||||
// 계산기 상태 업데이트
|
||||
if (filteredValue !== displayValue) {
|
||||
const calculator = calculatorRef.current
|
||||
const hasOperation = /[+\-×÷]/.test(filteredValue)
|
||||
|
||||
if (hasOperation) {
|
||||
const [operand1, operator, operand2] = filteredValue.split(/([+\-×÷])/)
|
||||
calculator.previousOperand = operand1 || ''
|
||||
calculator.operation = operator || ''
|
||||
calculator.currentOperand = operand2 || ''
|
||||
setHasOperation(true)
|
||||
} else {
|
||||
calculator.currentOperand = filteredValue
|
||||
setHasOperation(false)
|
||||
}
|
||||
|
||||
onChange(filteredValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 키패드 토글 함수
|
||||
const toggleKeypad = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const newShowKeypad = !showKeypad
|
||||
setShowKeypad(newShowKeypad)
|
||||
|
||||
// Show keypad 시에만 포커스 유지
|
||||
if (newShowKeypad) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 키보드 이벤트 처리 수정
|
||||
const handleKeyDown = (e) => {
|
||||
if (readOnly) return
|
||||
|
||||
// Tab 키는 계산기 숨기고 기본 동작 허용
|
||||
if (e.key === 'Tab') {
|
||||
setShowKeypad(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 모든 방향키는 기본 동작 허용
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
setShowKeypad(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 계산기 허용 키들이 입력되면 키패드 표시 (엔터키 제외)
|
||||
if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
|
||||
setShowKeypad(true)
|
||||
}
|
||||
|
||||
// 키패드가 숨겨진 상태에서 엔터키는 페이지로 전달
|
||||
if (!showKeypad && e.key === 'Enter') {
|
||||
return
|
||||
}
|
||||
|
||||
// 키패드 토글 함수
|
||||
const toggleKeypad = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const newShowKeypad = !showKeypad
|
||||
setShowKeypad(newShowKeypad)
|
||||
const calculator = calculatorRef.current
|
||||
const { allowDecimal } = options
|
||||
|
||||
// Show keypad 시에만 포커스 유지
|
||||
if (newShowKeypad) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
if (e.key === '.') {
|
||||
// allowDecimal이 false이면 소수점 입력 무시
|
||||
if (!allowDecimal) return
|
||||
|
||||
// 키보드 이벤트 처리 수정
|
||||
const handleKeyDown = (e) => {
|
||||
if (readOnly) return
|
||||
// 소수점 입력 처리
|
||||
const currentValue = displayValue.toString()
|
||||
const parts = currentValue.split(/[+\-×÷]/)
|
||||
const lastPart = parts[parts.length - 1]
|
||||
|
||||
// Tab 키는 계산기 숨기고 기본 동작 허용
|
||||
if (e.key === 'Tab') {
|
||||
setShowKeypad(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 모든 방향키는 기본 동작 허용
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
setShowKeypad(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 계산기 허용 키들이 입력되면 키패드 표시 (엔터키 제외)
|
||||
if (/^[0-9+\-×÷.=]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete' || e.key === '*' || e.key === '/') {
|
||||
setShowKeypad(true)
|
||||
}
|
||||
|
||||
// 키패드가 숨겨진 상태에서 엔터키는 페이지로 전달
|
||||
if (!showKeypad && e.key === 'Enter') {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const calculator = calculatorRef.current
|
||||
const { allowDecimal } = options
|
||||
|
||||
if (e.key === '.') {
|
||||
// allowDecimal이 false이면 소수점 입력 무시
|
||||
if (!allowDecimal) return
|
||||
|
||||
// 소수점 입력 처리
|
||||
const currentValue = displayValue.toString()
|
||||
const parts = currentValue.split(/[+\-×÷]/)
|
||||
const lastPart = parts[parts.length - 1]
|
||||
|
||||
// 이미 소수점이 있으면 무시
|
||||
if (!lastPart.includes('.')) {
|
||||
// 이미 소수점이 있으면 무시
|
||||
if (!lastPart.includes('.')) {
|
||||
handleNumber(e.key)
|
||||
}
|
||||
} else if (/^[0-9]$/.test(e.key)) {
|
||||
handleNumber(e.key)
|
||||
}
|
||||
} else if (/^[0-9]$/.test(e.key)) {
|
||||
handleNumber(e.key)
|
||||
} else {
|
||||
switch (e.key) {
|
||||
case '+':
|
||||
case '-':
|
||||
case '*':
|
||||
case '/':
|
||||
const opMap = { '*': '×', '/': '÷' }
|
||||
handleOperation(opMap[e.key] || e.key)
|
||||
break
|
||||
case 'Enter':
|
||||
case '=':
|
||||
if (showKeypad) {
|
||||
// 키패드가 보이는 상태에서 엔터키: 계산 후 키패드만 숨김
|
||||
handleCompute(true) // 엔터키로 호출됨을 표시
|
||||
} else {
|
||||
switch (e.key) {
|
||||
case '+':
|
||||
case '-':
|
||||
case '*':
|
||||
case '/':
|
||||
const opMap = { '*': '×', '/': '÷' }
|
||||
handleOperation(opMap[e.key] || e.key)
|
||||
break
|
||||
case 'Enter':
|
||||
case '=':
|
||||
if (showKeypad) {
|
||||
// 키패드가 보이는 상태에서 엔터키: 계산 후 키패드만 숨김
|
||||
handleCompute(true) // 엔터키로 호출됨을 표시
|
||||
setShowKeypad(false)
|
||||
}
|
||||
// 키패드가 숨겨진 상태에서 엔터키: 페이지로 전달 (preventDefault 하지 않음)
|
||||
break
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
handleDelete()
|
||||
break
|
||||
|
||||
case 'Escape':
|
||||
handleClear()
|
||||
setShowKeypad(false)
|
||||
}
|
||||
// 키패드가 숨겨진 상태에서 엔터키: 페이지로 전달 (preventDefault 하지 않음)
|
||||
break
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
handleDelete()
|
||||
break
|
||||
break
|
||||
|
||||
case 'Escape':
|
||||
handleClear()
|
||||
setShowKeypad(false)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="calculator-input-wrapper">
|
||||
{label && (
|
||||
<label htmlFor={id} className="calculator-label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
id={id}
|
||||
value={displayValue}
|
||||
readOnly={readOnly}
|
||||
className={className}
|
||||
onClick={() => !readOnly && setShowKeypad(true)}
|
||||
onFocus={() => !readOnly && setShowKeypad(true)}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={readOnly ? -1 : 0}
|
||||
placeholder={placeholder}
|
||||
autoComplete={'off'}
|
||||
/>
|
||||
return (
|
||||
<div ref={containerRef} className="calculator-input-wrapper">
|
||||
{label && (
|
||||
<label htmlFor={id} className="calculator-label">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
id={id}
|
||||
value={displayValue}
|
||||
readOnly={readOnly}
|
||||
className={className}
|
||||
onClick={() => !readOnly && setShowKeypad(true)}
|
||||
onFocus={() => !readOnly && setShowKeypad(true)}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={readOnly ? -1 : 0}
|
||||
placeholder={placeholder}
|
||||
autoComplete={'off'}
|
||||
/>
|
||||
|
||||
{showKeypad && !readOnly && (
|
||||
<div className="keypad-container">
|
||||
<div className="keypad-grid">
|
||||
<button
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.clear()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
handleClear()
|
||||
setHasOperation(false)
|
||||
}}
|
||||
className="btn-clear"
|
||||
>
|
||||
AC
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.deleteNumber()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
//setHasOperation(!!calculatorRef.current.operation)
|
||||
handleDelete()
|
||||
}}
|
||||
className="btn-delete"
|
||||
>
|
||||
DEL
|
||||
</button>
|
||||
<button onClick={() => handleOperation('÷')} className="btn-operator">
|
||||
÷
|
||||
</button>
|
||||
|
||||
<button onClick={() => handleOperation('×')} className="btn-operator">
|
||||
×
|
||||
</button>
|
||||
<button onClick={() => handleOperation('-')} className="btn-operator">
|
||||
-
|
||||
</button>
|
||||
<button onClick={() => handleOperation('+')} className="btn-operator">
|
||||
+
|
||||
</button>
|
||||
|
||||
{/* 숫자 버튼 */}
|
||||
{[1,2,3,4,5,6,7,8,9].map((num) => (
|
||||
<button key={num} onClick={() => handleNumber(num)} className="btn-number">
|
||||
{num}
|
||||
{showKeypad && !readOnly && (
|
||||
<div className="keypad-container">
|
||||
<div className="keypad-grid">
|
||||
<button
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.clear()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
handleClear()
|
||||
setHasOperation(false)
|
||||
}}
|
||||
className="btn-clear"
|
||||
>
|
||||
AC
|
||||
</button>
|
||||
))}
|
||||
{/* 0 버튼 */}
|
||||
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = calculatorRef.current.appendNumber('.')
|
||||
onChange(newValue)
|
||||
}}
|
||||
className="btn-number"
|
||||
>
|
||||
.
|
||||
</button>
|
||||
{/* = 버튼 */}
|
||||
<button onClick={() => handleCompute(false)} className="btn-equals">
|
||||
=
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.deleteNumber()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
//setHasOperation(!!calculatorRef.current.operation)
|
||||
handleDelete()
|
||||
}}
|
||||
className="btn-delete"
|
||||
>
|
||||
DEL
|
||||
</button>
|
||||
<button onClick={() => handleOperation('÷')} className="btn-operator">
|
||||
÷
|
||||
</button>
|
||||
|
||||
<button onClick={() => handleOperation('×')} className="btn-operator">
|
||||
×
|
||||
</button>
|
||||
<button onClick={() => handleOperation('-')} className="btn-operator">
|
||||
-
|
||||
</button>
|
||||
<button onClick={() => handleOperation('+')} className="btn-operator">
|
||||
+
|
||||
</button>
|
||||
|
||||
{/* 숫자 버튼 */}
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
|
||||
<button key={num} onClick={() => handleNumber(num)} className="btn-number">
|
||||
{num}
|
||||
</button>
|
||||
))}
|
||||
{/* 0 버튼 */}
|
||||
<button onClick={() => handleNumber('0')} className="btn-number btn-zero">
|
||||
0
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = calculatorRef.current.appendNumber('.')
|
||||
onChange(newValue)
|
||||
}}
|
||||
className="btn-number"
|
||||
>
|
||||
.
|
||||
</button>
|
||||
{/* = 버튼 */}
|
||||
<button onClick={() => handleCompute(false)} className="btn-equals">
|
||||
=
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
CalculatorInput.displayName = 'CalculatorInput'
|
||||
|
||||
@ -87,6 +87,10 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.initLines()
|
||||
this.init()
|
||||
this.setShape()
|
||||
const originWidth = this.originWidth ?? this.width
|
||||
const originHeight = this.originHeight ?? this.height
|
||||
this.originWidth = this.angle === 90 || this.angle === 270 ? originHeight : originWidth
|
||||
this.originHeight = this.angle === 90 || this.angle === 270 ? originWidth : originHeight
|
||||
},
|
||||
|
||||
setShape() {
|
||||
@ -126,11 +130,13 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
this.on('moving', () => {
|
||||
this.initLines()
|
||||
this.addLengthText()
|
||||
this.setCoords()
|
||||
})
|
||||
|
||||
this.on('modified', (e) => {
|
||||
this.initLines()
|
||||
this.addLengthText()
|
||||
this.setCoords()
|
||||
})
|
||||
|
||||
this.on('selected', () => {
|
||||
@ -733,7 +739,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
},
|
||||
|
||||
inPolygonImproved(point) {
|
||||
const vertices = this.points
|
||||
const vertices = this.getCurrentPoints()
|
||||
let inside = false
|
||||
const testX = Number(point.x.toFixed(this.toFixed))
|
||||
const testY = Number(point.y.toFixed(this.toFixed))
|
||||
@ -745,7 +751,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
const yj = Number(vertices[j].y.toFixed(this.toFixed))
|
||||
|
||||
// 점이 정점 위에 있는지 확인
|
||||
if (Math.abs(xi - testX) < 0.01 && Math.abs(yi - testY) < 0.01) {
|
||||
if (Math.abs(xi - testX) <= 0.01 && Math.abs(yi - testY) <= 0.01) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -196,6 +196,14 @@ export default function CanvasMenu(props) {
|
||||
text: getMessage('module.delete.confirm'),
|
||||
type: 'confirm',
|
||||
confirmFn: () => {
|
||||
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
|
||||
roofs.forEach((roof) => {
|
||||
roof.set({
|
||||
stroke: 'black',
|
||||
strokeWidth: 3,
|
||||
})
|
||||
})
|
||||
|
||||
//해당 메뉴 이동시 배치면 삭제
|
||||
|
||||
setAllModuleSurfaceIsComplete(false)
|
||||
|
||||
@ -7,16 +7,21 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import QSelectBox from '@/components/common/select/QSelectBox'
|
||||
import { roofsState } from '@/store/roofAtom'
|
||||
import { useModuleBasicSetting } from '@/hooks/module/useModuleBasicSetting'
|
||||
import { useCommonCode } from '@/hooks/common/useCommonCode'
|
||||
import Swal from 'sweetalert2'
|
||||
import { normalizeDecimal} from '@/util/input-utils'
|
||||
|
||||
export const Orientation = forwardRef((props, ref) => {
|
||||
const { getMessage } = useMessage()
|
||||
const { findCommonCode } = useCommonCode()
|
||||
const [hasAnglePassivity, setHasAnglePassivity] = useState(false)
|
||||
const basicSetting = useRecoilValue(basicSettingState)
|
||||
const [addedRoofs, setAddedRoofs] = useRecoilState(addedRoofsState) //지붕재 선택
|
||||
const [roofsStore, setRoofsStore] = useRecoilState(roofsState)
|
||||
const [roofTab, setRoofTab] = useState(0) //지붕재 탭
|
||||
const [selectedModuleSeries, setSelectedModuleSeries] = useState(null)
|
||||
const [moduleSeriesList, setModuleSeriesList] = useState([])
|
||||
const [filteredModuleList, setFilteredModuleList] = useState([])
|
||||
const {
|
||||
roofs,
|
||||
setRoofs,
|
||||
@ -66,8 +71,13 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
],
|
||||
}
|
||||
|
||||
const allOption = {
|
||||
moduleSerCd: 'ALL',
|
||||
moduleSerNm: getMessage("board.sub.total") || 'ALL'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (basicSetting.roofSizeSet == '3') {
|
||||
if (basicSetting.roofSizeSet === '3') {
|
||||
restoreModuleInstArea()
|
||||
}
|
||||
}, [])
|
||||
@ -80,9 +90,22 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModules) {
|
||||
setSelectedModules(moduleList.find((module) => module.itemId === selectedModules.itemId))
|
||||
const foundModule = moduleList.find((module) => module.itemId === selectedModules.itemId)
|
||||
if (foundModule) {
|
||||
setSelectedModules(foundModule)
|
||||
|
||||
// 선택된 모듈의 시리즈로 업데이트 (시리즈 목록이 있는 경우에만)
|
||||
if (moduleSeriesList.length > 0 && foundModule.moduleSerCd) {
|
||||
const currentSeries = moduleSeriesList.find(series => series.moduleSerCd === foundModule.moduleSerCd)
|
||||
if (currentSeries && (!selectedModuleSeries || selectedModuleSeries.moduleSerCd !== currentSeries.moduleSerCd)) {
|
||||
setSelectedModuleSeries(currentSeries)
|
||||
}
|
||||
}else{
|
||||
setSelectedModuleSeries(allOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedModules])
|
||||
}, [selectedModules, moduleList, moduleSeriesList])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSurfaceType) {
|
||||
@ -164,7 +187,7 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
title: getMessage('module.not.found'),
|
||||
icon: 'warning',
|
||||
})
|
||||
return
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -206,6 +229,41 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const handleChangeModuleSeries = (e) => {
|
||||
resetRoofs()
|
||||
setSelectedModuleSeries(e)
|
||||
|
||||
// 선택된 시리즈에 맞는 모듈 목록 필터링 및 첫 번째 모듈 선택
|
||||
if (e && moduleList.length > 0) {
|
||||
let filtered
|
||||
|
||||
if (e.moduleSerCd === 'ALL') {
|
||||
// "전체" 선택 시 모든 모듈 표시
|
||||
filtered = moduleList
|
||||
} else {
|
||||
// 특정 시리즈 선택 시 해당 시리즈 모듈만 표시
|
||||
//filtered = moduleList.filter(module => module.moduleSerCd === e.moduleSerCd)
|
||||
filtered = moduleList.filter(module => module && module.moduleSerCd && module.moduleSerCd === e.moduleSerCd)
|
||||
}
|
||||
|
||||
setFilteredModuleList(filtered)
|
||||
|
||||
// 필터링된 목록의 첫 번째 모듈을 자동 선택
|
||||
if (filtered.length > 0) {
|
||||
const firstModule = filtered[0]
|
||||
setSelectedModules(firstModule)
|
||||
// 상위 컴포넌트의 handleChangeModule 호출
|
||||
if (handleChangeModule) {
|
||||
handleChangeModule(firstModule)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 모듈 리스트가 비어있는 경우
|
||||
setFilteredModuleList([])
|
||||
setSelectedModules(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangeModule = (e) => {
|
||||
resetRoofs()
|
||||
setSelectedModules(e)
|
||||
@ -264,12 +322,71 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
setRoofsStore(newRoofs)
|
||||
}
|
||||
|
||||
// 모듈시리즈 목록 생성 및 commonCode와 매핑
|
||||
useEffect(() => {
|
||||
// 컴포넌트가 마운트될 때 selectedModules가 없으면 handleChangeModule 호출
|
||||
if (moduleList.length > 0 && (!selectedModules || !selectedModules.itemId)) {
|
||||
handleChangeModule(moduleList[0]);
|
||||
if (moduleList.length > 0 && moduleSeriesList.length === 0) {
|
||||
const moduleSeriesCodes = findCommonCode(207100) || []
|
||||
|
||||
// moduleList에서 고유한 moduleSerCd 추출
|
||||
const uniqueSeriesCd = [...new Set(moduleList.map(module => module.moduleSerCd).filter(Boolean))]
|
||||
|
||||
if (uniqueSeriesCd.length > 0) {
|
||||
// moduleSerCd와 commonCode를 매핑하여 기본 moduleSeriesList 생성
|
||||
const mappedSeries = uniqueSeriesCd.map(serCd => {
|
||||
const matchedCode = moduleSeriesCodes.find(code => code.clCode === serCd)
|
||||
return {
|
||||
moduleSerCd: serCd,
|
||||
moduleSerNm: matchedCode ? matchedCode.clCodeNm : serCd
|
||||
}
|
||||
})
|
||||
|
||||
// "전체" 옵션을 맨 앞에 추가
|
||||
const seriesList = [allOption, ...mappedSeries]
|
||||
setModuleSeriesList(seriesList)
|
||||
|
||||
// 현재 선택된 모듈이 있으면 해당 모듈의 시리즈를 찾아서 선택
|
||||
if (selectedModules && selectedModules.moduleSerCd) {
|
||||
const currentSeries = seriesList.find(series => series.moduleSerCd === selectedModules.moduleSerCd)
|
||||
if (currentSeries) {
|
||||
setSelectedModuleSeries(currentSeries)
|
||||
} else {
|
||||
setSelectedModuleSeries(allOption)
|
||||
// "ALL"이 선택되면 자동으로 모듈 필터링 및 선택 실행
|
||||
setTimeout(() => handleChangeModuleSeries(allOption), 0)
|
||||
}
|
||||
} else {
|
||||
// 선택된 모듈이 없으면 "전체"를 기본 선택
|
||||
setSelectedModuleSeries(allOption)
|
||||
// "ALL"이 선택되면 자동으로 모듈 필터링 및 선택 실행
|
||||
setTimeout(() => handleChangeModuleSeries(allOption), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [moduleList]); // 빈 의존성 배열로 마운트 시 한 번만 실행
|
||||
}, [moduleList, selectedModules])
|
||||
|
||||
// 초기 로딩 시에만 필터링된 모듈 목록 설정
|
||||
useEffect(() => {
|
||||
if (moduleList.length > 0 && filteredModuleList.length === 0 && selectedModuleSeries) {
|
||||
let filtered
|
||||
|
||||
if (selectedModuleSeries.moduleSerCd === 'ALL') {
|
||||
// "전체" 선택 시 모든 모듈 표시
|
||||
filtered = moduleList
|
||||
} else {
|
||||
// 특정 시리즈 선택 시 해당 시리즈 모듈만 표시
|
||||
filtered = moduleList.filter(module => module.moduleSerCd === selectedModuleSeries.moduleSerCd)
|
||||
}
|
||||
|
||||
setFilteredModuleList(filtered)
|
||||
|
||||
if (filtered.length > 0 && !selectedModules) {
|
||||
setSelectedModules(filtered[0])
|
||||
}
|
||||
} else if (moduleList.length === 0 && filteredModuleList.length === 0 && selectedModuleSeries) {
|
||||
// 모듈 리스트가 비어있는 경우 빈 배열로 설정
|
||||
setFilteredModuleList([])
|
||||
}
|
||||
}, [moduleList, selectedModuleSeries]);
|
||||
return (
|
||||
<>
|
||||
<div className="properties-setting-wrap">
|
||||
@ -336,16 +453,32 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
<div className="compas-table-wrap">
|
||||
<div className="compas-table-box mb10">
|
||||
<div className="outline-form mb10">
|
||||
<span>{getMessage('modal.module.basic.setting.module.setting')}</span>
|
||||
<span>{getMessage('modal.module.basic.setting.module.series.setting')}</span>
|
||||
<div className="grid-select">
|
||||
{moduleList && (
|
||||
<div className="grid-select">
|
||||
<QSelectBox
|
||||
options={moduleList}
|
||||
options={moduleSeriesList.length > 0 ? moduleSeriesList : [allOption]}
|
||||
value={selectedModuleSeries}
|
||||
targetKey={'moduleSerCd'}
|
||||
sourceKey={'moduleSerCd'}
|
||||
showKey={'moduleSerNm'}
|
||||
onChange={(e) => handleChangeModuleSeries(e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="outline-form mb10">
|
||||
<span>{getMessage('modal.module.basic.setting.module.setting2')}</span>
|
||||
<div className="grid-select">
|
||||
{filteredModuleList && (
|
||||
<QSelectBox
|
||||
options={filteredModuleList}
|
||||
value={selectedModules}
|
||||
targetKey={'itemId'}
|
||||
sourceKey={'itemId'}
|
||||
showKey={'itemNm'}
|
||||
onChange={(e) => handleChangeModule(e)}
|
||||
showFirstOptionWhenEmpty = {true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -396,7 +529,7 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{basicSetting && basicSetting.roofSizeSet == '3' && (
|
||||
{basicSetting && basicSetting.roofSizeSet === '3' && (
|
||||
<div className="outline-form mt15">
|
||||
<span>{getMessage('modal.module.basic.setting.module.placement.area')}</span>
|
||||
<div className="input-grid mr10" style={{ width: '60px' }}>
|
||||
@ -407,7 +540,7 @@ export const Orientation = forwardRef((props, ref) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{basicSetting && basicSetting.roofSizeSet != '3' && (
|
||||
{basicSetting && basicSetting.roofSizeSet !== '3' && (
|
||||
<div className="compas-table-box">
|
||||
<div className="compas-grid-table">
|
||||
<div className="outline-form">
|
||||
|
||||
@ -18,6 +18,12 @@ const Trestle = forwardRef((props, ref) => {
|
||||
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
|
||||
const pitchText = useRecoilValue(pitchTextSelector)
|
||||
const [selectedRoof, setSelectedRoof] = useState(null)
|
||||
const [isAutoSelecting, setIsAutoSelecting] = useState(false) // 자동 선택 중인지 상태
|
||||
const [autoSelectTimeout, setAutoSelectTimeout] = useState(null) // 타임아웃 참조
|
||||
const autoSelectTimeoutRef = useRef(null)
|
||||
|
||||
// 공통 타임아웃 설정 (밀리초)
|
||||
const AUTO_SELECT_TIMEOUT = 500 // API 호출 완료 대기 시간
|
||||
const {
|
||||
trestleState,
|
||||
trestleDetail,
|
||||
@ -60,23 +66,27 @@ const Trestle = forwardRef((props, ref) => {
|
||||
const [flag, setFlag] = useState(false)
|
||||
const tempModuleSelectionData = useRef(null)
|
||||
const [autoSelectStep, setAutoSelectStep] = useState(null) // 'raftBase', 'trestle', 'constMthd', 'roofBase', 'construction'
|
||||
|
||||
const prevHajebichiRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (roofs && !selectedRoof) {
|
||||
if (roofs && roofs.length > 0 && !selectedRoof) {
|
||||
console.log("roofs:::::", roofs.length)
|
||||
setLengthBase(roofs[0].length);
|
||||
setSelectedRoof(roofs[0])
|
||||
}
|
||||
if (selectedRoof && selectedRoof.lenAuth === "C") {
|
||||
onChangeLength(selectedRoof.length);
|
||||
}
|
||||
if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth)) {
|
||||
}else if (selectedRoof && ["C", "R"].includes(selectedRoof.raftAuth) && roofs && roofs.length > 0) {
|
||||
onChangeRaftBase(roofs[0]);
|
||||
}else if (selectedRoof && ["C", "R"].includes(selectedRoof.roofPchAuth) && roofs && roofs.length > 0 &&
|
||||
roofs[0].hajebichi !== prevHajebichiRef.current ) {
|
||||
prevHajebichiRef.current = roofs[0].hajebichi;
|
||||
onChangeHajebichi(roofs[0].hajebichi);
|
||||
}
|
||||
|
||||
//모듈 설치 영역 복구
|
||||
restoreModuleInstArea()
|
||||
}, [roofs])
|
||||
}, [roofs, selectedRoof]) // selectedRoof 추가
|
||||
|
||||
useEffect(() => {
|
||||
if (flag && moduleSelectionData) {
|
||||
@ -161,7 +171,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (constructionList.length > 0) {
|
||||
const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState?.construction?.constTp)
|
||||
const existingConstruction = constructionList.find((construction) => construction.constTp === trestleState.constTp)
|
||||
if (existingConstruction) {
|
||||
setSelectedConstruction(existingConstruction)
|
||||
} else if (autoSelectStep === 'construction') {
|
||||
@ -252,7 +262,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
// 다음 단계(가대메이커) 자동 선택 설정 - 지연 실행
|
||||
setTimeout(() => {
|
||||
setAutoSelectStep('trestle')
|
||||
}, 500) // API 호출 완료를 위한 더 긴 지연
|
||||
}, AUTO_SELECT_TIMEOUT) // API 호출 완료를 위한 더 긴 지연
|
||||
}
|
||||
|
||||
const onChangeHajebichi = (e) => {
|
||||
@ -274,7 +284,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
roof: {
|
||||
moduleTpCd: selectedModules.itemTp ?? '',
|
||||
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
hajebichi: e,
|
||||
},
|
||||
})
|
||||
@ -282,7 +292,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
// 다음 단계(가대메이커) 자동 선택 설정 - 지연 실행
|
||||
setTimeout(() => {
|
||||
setAutoSelectStep('trestle')
|
||||
}, 500)
|
||||
}, AUTO_SELECT_TIMEOUT)
|
||||
}
|
||||
|
||||
const onChangeTrestleMaker = (e) => {
|
||||
@ -297,7 +307,8 @@ const Trestle = forwardRef((props, ref) => {
|
||||
roof: {
|
||||
moduleTpCd: selectedModules.itemTp ?? '',
|
||||
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
|
||||
trestleMkrCd: e.trestleMkrCd,
|
||||
},
|
||||
})
|
||||
@ -305,7 +316,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
// API 호출 완료 후 다음 단계(공법) 자동 선택 설정
|
||||
setTimeout(() => {
|
||||
setAutoSelectStep('constMthd')
|
||||
}, 300)
|
||||
}, AUTO_SELECT_TIMEOUT)
|
||||
}
|
||||
|
||||
const onChangeConstMthd = (e) => {
|
||||
@ -319,16 +330,28 @@ const Trestle = forwardRef((props, ref) => {
|
||||
roof: {
|
||||
moduleTpCd: selectedModules.itemTp ?? '',
|
||||
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
|
||||
trestleMkrCd: selectedTrestle?.trestleMkrCd,
|
||||
constMthdCd: e.constMthdCd,
|
||||
},
|
||||
})
|
||||
|
||||
// 기존 타임아웃 취소
|
||||
if (autoSelectTimeoutRef.current) {
|
||||
clearTimeout(autoSelectTimeoutRef.current)
|
||||
}
|
||||
|
||||
// 자동 선택 중 상태 활성화
|
||||
setIsAutoSelecting(true)
|
||||
|
||||
// API 호출 완료 후 다음 단계(지붕밑바탕) 자동 선택 설정
|
||||
setTimeout(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setAutoSelectStep('roofBase')
|
||||
}, 300)
|
||||
setIsAutoSelecting(false)
|
||||
}, AUTO_SELECT_TIMEOUT)
|
||||
|
||||
autoSelectTimeoutRef.current = timeoutId
|
||||
}
|
||||
|
||||
const onChangeRoofBase = (e) => {
|
||||
@ -340,7 +363,8 @@ const Trestle = forwardRef((props, ref) => {
|
||||
roof: {
|
||||
moduleTpCd: selectedModules.itemTp ?? '',
|
||||
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
|
||||
trestleMkrCd: selectedTrestle?.trestleMkrCd,
|
||||
constMthdCd: selectedConstMthd?.constMthdCd,
|
||||
roofBaseCd: e.roofBaseCd,
|
||||
@ -356,7 +380,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
// API 호출 완료 후 다음 단계(construction) 자동 선택 설정
|
||||
setTimeout(() => {
|
||||
setAutoSelectStep('construction')
|
||||
}, 300)
|
||||
}, AUTO_SELECT_TIMEOUT)
|
||||
}
|
||||
|
||||
const handleConstruction = (index) => {
|
||||
@ -366,7 +390,8 @@ const Trestle = forwardRef((props, ref) => {
|
||||
roof: {
|
||||
moduleTpCd: selectedModules.itemTp ?? '',
|
||||
roofMatlCd: selectedRoof?.roofMatlCd ?? '',
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi,
|
||||
trestleMkrCd: selectedTrestle.trestleMkrCd,
|
||||
constMthdCd: selectedConstMthd.constMthdCd,
|
||||
roofBaseCd: selectedRoofBase.roofBaseCd,
|
||||
@ -403,7 +428,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
ridgeMargin,
|
||||
kerabaMargin,
|
||||
roofIndex: selectedRoof.index,
|
||||
raft: selectedRaftBase?.clCode,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.roofBaseCd,
|
||||
trestle: {
|
||||
hajebichi: hajebichi,
|
||||
length: lengthBase,
|
||||
@ -440,8 +465,8 @@ const Trestle = forwardRef((props, ref) => {
|
||||
ridgeMargin,
|
||||
kerabaMargin,
|
||||
roofIndex: roof.index,
|
||||
raft: selectedRaftBase?.clCode,
|
||||
hajebichi: hajebichi,
|
||||
raft: selectedRaftBase?.clCode ?? selectedRoof?.raft ?? '',
|
||||
//hajebichi: selectedRaftBase?.hajebichi ?? selectedRoof?.hajebichi ?? 0,
|
||||
trestle: {
|
||||
length: lengthBase,
|
||||
hajebichi: hajebichi,
|
||||
@ -451,7 +476,8 @@ const Trestle = forwardRef((props, ref) => {
|
||||
...selectedRoofBase,
|
||||
},
|
||||
construction: {
|
||||
...constructionList.find((data) => data.constTp === trestleState.constTp),
|
||||
//...constructionList.find((data) => newAddedRoofs[index].construction.constTp === data.constTp),
|
||||
...constructionList.find((data) => trestleState.constTp === data.constTp),
|
||||
cvrYn,
|
||||
snowGdPossYn,
|
||||
cvrChecked,
|
||||
@ -525,7 +551,7 @@ const Trestle = forwardRef((props, ref) => {
|
||||
}
|
||||
|
||||
if (['C', 'R'].includes(roof.roofPchAuth)) {
|
||||
if (!roof?.roofPchBase) {
|
||||
if (!roof?.hajebichi) {
|
||||
Swal.fire({
|
||||
title: getMessage('modal.module.basic.settting.module.error7', [roof.nameJp]), // 하제비치를 입력해주세요.
|
||||
icon: 'warning',
|
||||
|
||||
@ -12,7 +12,7 @@ import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||
|
||||
export default function SizeSetting(props) {
|
||||
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
|
||||
const [settingTarget, setSettingTarget] = useState(1)
|
||||
const [settingTarget, setSettingTarget] = useState(props.side || 1)
|
||||
const { id, pos = contextPopupPosition, target } = props
|
||||
const { getMessage } = useMessage()
|
||||
const { closePopup } = usePopup()
|
||||
@ -47,11 +47,11 @@ export default function SizeSetting(props) {
|
||||
<div className="size-option-top">
|
||||
<div className="size-option-wrap">
|
||||
<div className="size-option mb5">
|
||||
<input type="text" className="input-origin mr5" value={target?.width.toFixed(0) * 10} readOnly={true} />
|
||||
<input type="text" className="input-origin mr5" value={(target?.originWidth * 10).toFixed(0)} readOnly={true} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
<div className="size-option">
|
||||
<input type="text" className="input-origin mr5" defaultValue={target?.width.toFixed(0) * 10} ref={widthRef} />
|
||||
<input type="text" className="input-origin mr5" defaultValue={(target?.originWidth * 10).toFixed(0)} ref={widthRef} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -60,11 +60,11 @@ export default function SizeSetting(props) {
|
||||
<div className="size-option-side">
|
||||
<div className="size-option-wrap">
|
||||
<div className="size-option mb5">
|
||||
<input type="text" className="input-origin mr5" value={target?.height.toFixed(0) * 10} readOnly={true} />
|
||||
<input type="text" className="input-origin mr5" value={(target?.originHeight * 10).toFixed(0)} readOnly={true} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
<div className="size-option">
|
||||
<input type="text" className="input-origin mr5" defaultValue={target?.height.toFixed(0) * 10} ref={heightRef} />
|
||||
<input type="text" className="input-origin mr5" defaultValue={(target?.originHeight * 10).toFixed(0)} ref={heightRef} />
|
||||
<span className="normal-font">mm</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -86,7 +86,7 @@ export default function ContextRoofAllocationSetting(props) {
|
||||
return (
|
||||
<div className="grid-option-box" key={index}>
|
||||
<div className="d-check-radio pop no-text">
|
||||
<input type="radio" name="radio01" checked={roof.selected && 'checked'} readOnly={true} />
|
||||
<input type="radio" name="radio01" checked={!!roof.selected} readOnly={true} />
|
||||
<label
|
||||
htmlFor="ra01"
|
||||
onClick={(e) => {
|
||||
@ -189,7 +189,7 @@ export default function ContextRoofAllocationSetting(props) {
|
||||
<input
|
||||
type="text"
|
||||
className="input-origin block"
|
||||
value={roof.hajebichi === '' ? '0' : roof.hajebichi}
|
||||
value={roof.hajebichi ?? ''}
|
||||
readOnly={roof.roofPchAuth === 'R'}
|
||||
onChange={(e) => {
|
||||
e.target.value = normalizeDigits(e.target.value)
|
||||
@ -211,8 +211,7 @@ export default function ContextRoofAllocationSetting(props) {
|
||||
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
||||
handleChangePitch(e, index)
|
||||
}}
|
||||
value={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
|
||||
defaultValue={currentAngleType === 'slope' ? (roof.pitch || '0') : (roof.angle || '0')}
|
||||
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<span className="absol">{pitchText}</span>
|
||||
|
||||
@ -86,7 +86,7 @@ export default function RoofAllocationSetting(props) {
|
||||
return (
|
||||
<div className="grid-option-box" key={index}>
|
||||
<div className="d-check-radio pop no-text">
|
||||
<input type="radio" name="radio01" checked={roof.selected} readOnly />
|
||||
<input type="radio" name="radio01" checked={!!roof.selected} readOnly />
|
||||
<label
|
||||
htmlFor="ra01"
|
||||
onClick={(e) => {
|
||||
@ -194,7 +194,7 @@ export default function RoofAllocationSetting(props) {
|
||||
e.target.value = normalizeDigits(e.target.value)
|
||||
handleChangeInput(e, 'hajebichi', index)
|
||||
}}
|
||||
value={parseInt(roof.hajebichi)}
|
||||
value={roof.hajebichi ?? ''}
|
||||
readOnly={roof.roofPchAuth === 'R'}
|
||||
/>
|
||||
</div>
|
||||
@ -212,7 +212,7 @@ export default function RoofAllocationSetting(props) {
|
||||
e.target.value = normalizeDecimalLimit(e.target.value, 2)
|
||||
handleChangePitch(e, index)
|
||||
}}
|
||||
value={currentAngleType === 'slope' ? roof.pitch : roof.angle}
|
||||
value={currentAngleType === 'slope' ? (roof.pitch ?? '') : (roof.angle ?? '')}
|
||||
/>
|
||||
</div>
|
||||
<span className="absol">{pitchText}</span>
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from '@/store/canvasAtom'
|
||||
|
||||
import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util'
|
||||
import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가
|
||||
import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom'
|
||||
import offsetPolygon, { calculateAngle, createLinesFromPolygon } from '@/util/qpolygon-utils'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
@ -265,14 +266,14 @@ export function useModuleBasicSetting(tabNum) {
|
||||
batchObjects.forEach((obj) => {
|
||||
//도머일때
|
||||
if (obj.name === BATCH_TYPE.TRIANGLE_DORMER || obj.name === BATCH_TYPE.PENTAGON_DORMER) {
|
||||
const groupPoints = obj.groupPoints
|
||||
const groupPoints = obj.getCurrentPoints()
|
||||
const offsetObjects = offsetPolygon(groupPoints, 10)
|
||||
const dormerOffset = new QPolygon(offsetObjects, batchObjectOptions)
|
||||
dormerOffset.setViewLengthText(false)
|
||||
canvas.add(dormerOffset) //모듈설치면 만들기
|
||||
} else {
|
||||
//개구, 그림자일때
|
||||
const points = obj.points
|
||||
const points = obj.getCurrentPoints()
|
||||
const offsetObjects = offsetPolygon(points, 10)
|
||||
const offset = new QPolygon(offsetObjects, batchObjectOptions)
|
||||
offset.setViewLengthText(false)
|
||||
@ -319,7 +320,7 @@ export function useModuleBasicSetting(tabNum) {
|
||||
const margin = moduleSelectionData.common.margin ? moduleSelectionData.common.margin : 200
|
||||
|
||||
//육지붕일때는 그냥 하드코딩
|
||||
offsetPoints = offsetPolygon(roof.points, -Number(margin) / 10) //육지붕일때
|
||||
offsetPoints = offsetPolygon(roof.getCurrentPoints(), -Number(margin) / 10) //육지붕일때
|
||||
} else {
|
||||
//육지붕이 아닐때
|
||||
if (allPointsOutside) {
|
||||
|
||||
@ -675,6 +675,8 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
})
|
||||
|
||||
objectGroup.recalculateGroupPoints()
|
||||
|
||||
isDown = false
|
||||
initEvent()
|
||||
// dbClickEvent()
|
||||
@ -1426,7 +1428,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
|
||||
//그림자는 아무데나 설치 할 수 있게 해달라고 함
|
||||
if (obj.name === BATCH_TYPE.OPENING) {
|
||||
const turfObject = pointsToTurfPolygon(obj.points)
|
||||
const turfObject = pointsToTurfPolygon(obj.getCurrentPoints())
|
||||
|
||||
if (turf.booleanWithin(turfObject, turfSurface)) {
|
||||
obj.set({
|
||||
@ -1459,7 +1461,7 @@ export function useObjectBatch({ isHidden, setIsHidden }) {
|
||||
const calcLeft = obj.left - originLeft
|
||||
const calcTop = obj.top - originTop
|
||||
|
||||
const currentDormerPoints = obj.groupPoints.map((item) => {
|
||||
const currentDormerPoints = obj.getCurrentPoints().map((item) => {
|
||||
return {
|
||||
x: item.x + calcLeft,
|
||||
y: item.y + calcTop,
|
||||
|
||||
@ -174,22 +174,32 @@ export function useRoofAllocationSetting(id) {
|
||||
})
|
||||
}
|
||||
|
||||
const firstRes = Array.isArray(res) && res.length > 0 ? res[0] : null
|
||||
|
||||
setBasicSetting({
|
||||
...basicSetting,
|
||||
planNo: res[0].planNo,
|
||||
roofSizeSet: res[0].roofSizeSet,
|
||||
roofAngleSet: res[0].roofAngleSet,
|
||||
planNo: firstRes?.planNo ?? planNo,
|
||||
roofSizeSet: firstRes?.roofSizeSet ?? 0,
|
||||
roofAngleSet: firstRes?.roofAngleSet ?? 0,
|
||||
roofsData: roofsArray,
|
||||
selectedRoofMaterial: selectRoofs.find((roof) => roof.selected),
|
||||
})
|
||||
|
||||
setBasicInfo({
|
||||
planNo: '' + res[0].planNo,
|
||||
roofSizeSet: '' + res[0].roofSizeSet,
|
||||
roofAngleSet: '' + res[0].roofAngleSet,
|
||||
planNo: '' + (firstRes?.planNo ?? planNo),
|
||||
roofSizeSet: '' + (firstRes?.roofSizeSet ?? 0),
|
||||
roofAngleSet: '' + (firstRes?.roofAngleSet ?? 0),
|
||||
})
|
||||
//데이터 동기화
|
||||
setCurrentRoofList(selectRoofs)
|
||||
// 데이터 동기화: 렌더링용 필드 기본값 보정
|
||||
const normalizedRoofs = selectRoofs.map((roof) => ({
|
||||
...roof,
|
||||
width: roof.width ?? '',
|
||||
length: roof.length ?? '',
|
||||
hajebichi: roof.hajebichi ?? '',
|
||||
pitch: roof.pitch ?? '',
|
||||
angle: roof.angle ?? '',
|
||||
}))
|
||||
setCurrentRoofList(normalizedRoofs)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Data fetching error:', error)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil'
|
||||
import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom'
|
||||
import { canvasSettingState, canvasState, currentCanvasPlanState, currentObjectState, globalPitchState } from '@/store/canvasAtom'
|
||||
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
|
||||
import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util'
|
||||
import { getIntersectionPoint } from '@/util/canvas-util'
|
||||
import { degreesToRadians } from '@turf/turf'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
@ -21,10 +21,13 @@ import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingA
|
||||
import { getBackGroundImage } from '@/lib/imageActions'
|
||||
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
|
||||
import { useText } from '@/hooks/useText'
|
||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
const { getMessage } = useMessage()
|
||||
const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon()
|
||||
const { drawDirectionArrow, addPolygon, addLengthText, setPolygonLinesActualSize } = usePolygon()
|
||||
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
|
||||
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
|
||||
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
|
||||
@ -36,11 +39,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
const { swalFire } = useSwal()
|
||||
const { addCanvasMouseEventListener, initEvent } = useEvent()
|
||||
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
|
||||
const { addPopup, closePopup } = usePopup()
|
||||
const { addPopup, closePopup, closeAll } = usePopup()
|
||||
const { setSurfaceShapePattern } = useRoofFn()
|
||||
const { changeCorridorDimensionText } = useText()
|
||||
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
|
||||
const { fetchSettings } = useCanvasSetting(false)
|
||||
const currentObject = useRecoilValue(currentObjectState)
|
||||
const [popupId, setPopupId] = useState(uuidv4())
|
||||
|
||||
const applySurfaceShape = (surfaceRefs, selectedType, id) => {
|
||||
let length1, length2, length3, length4, length5
|
||||
@ -879,6 +884,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
drawDirectionArrow(roof)
|
||||
changeCorridorDimensionText()
|
||||
addLengthText(roof)
|
||||
roof.setCoords()
|
||||
initEvent()
|
||||
canvas.renderAll()
|
||||
})
|
||||
@ -916,71 +922,138 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
|
||||
const resizeSurfaceShapeBatch = (side, target, width, height) => {
|
||||
const originTarget = { ...target }
|
||||
if (!target || target.type !== 'QPolygon') return
|
||||
|
||||
const objectWidth = target.width
|
||||
const objectHeight = target.height
|
||||
const changeWidth = width / 10 / objectWidth
|
||||
const changeHeight = height / 10 / objectHeight
|
||||
let sideX = 'left'
|
||||
let sideY = 'top'
|
||||
width = width / 10
|
||||
height = height / 10
|
||||
|
||||
//그룹 중심점 변경
|
||||
if (side === 2) {
|
||||
sideX = 'right'
|
||||
sideY = 'top'
|
||||
} else if (side === 3) {
|
||||
sideX = 'left'
|
||||
sideY = 'bottom'
|
||||
} else if (side === 4) {
|
||||
sideX = 'right'
|
||||
sideY = 'bottom'
|
||||
// 현재 QPolygon의 점들 가져오기 (변형 적용된 실제 좌표)
|
||||
const currentPoints = target.getCurrentPoints() || []
|
||||
const angle = target.angle % 360
|
||||
if (currentPoints.length === 0) return
|
||||
|
||||
// 현재 바운딩 박스 계산
|
||||
let minX = Math.min(...currentPoints.map((p) => p.x))
|
||||
let maxX = Math.max(...currentPoints.map((p) => p.x))
|
||||
let minY = Math.min(...currentPoints.map((p) => p.y))
|
||||
let maxY = Math.max(...currentPoints.map((p) => p.y))
|
||||
|
||||
let currentWidth = maxX - minX
|
||||
let currentHeight = maxY - minY
|
||||
|
||||
// 회전에 관계없이 단순한 앵커 포인트 계산
|
||||
let anchorX, anchorY
|
||||
switch (side) {
|
||||
case 1: // left-top
|
||||
anchorX = minX
|
||||
anchorY = minY
|
||||
break
|
||||
case 2: // right-top
|
||||
anchorX = maxX
|
||||
anchorY = minY
|
||||
break
|
||||
case 3: // left-bottom
|
||||
anchorX = minX
|
||||
anchorY = maxY
|
||||
break
|
||||
case 4: // right-bottom
|
||||
anchorX = maxX
|
||||
anchorY = maxY
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
//변경 전 좌표
|
||||
const newCoords = target.getPointByOrigin(sideX, sideY)
|
||||
// 목표 크기 (회전에 관계없이 동일하게 적용)
|
||||
let targetWidth = width
|
||||
let targetHeight = height
|
||||
|
||||
target.set({
|
||||
originX: sideX,
|
||||
originY: sideY,
|
||||
left: newCoords.x,
|
||||
top: newCoords.y,
|
||||
// 새로운 점들 계산 - 앵커 포인트는 고정, 나머지는 비례적으로 확장
|
||||
// 각도와 side에 따라 확장 방향 결정
|
||||
const newPoints = currentPoints.map((point) => {
|
||||
// 앵커 포인트 기준으로 새로운 위치 계산
|
||||
// side와 각도에 관계없이 일관된 방식으로 처리
|
||||
|
||||
// 앵커 포인트에서 각 점까지의 절대 거리
|
||||
const deltaX = point.x - anchorX
|
||||
const deltaY = point.y - anchorY
|
||||
|
||||
// 새로운 크기에 맞춰 비례적으로 확장
|
||||
const newDeltaX = (deltaX / currentWidth) * targetWidth
|
||||
const newDeltaY = (deltaY / currentHeight) * targetHeight
|
||||
|
||||
const newX = anchorX + newDeltaX
|
||||
const newY = anchorY + newDeltaY
|
||||
|
||||
return {
|
||||
x: newX, // 소수점 1자리로 반올림
|
||||
y: newY,
|
||||
}
|
||||
})
|
||||
|
||||
target.scaleX = changeWidth
|
||||
target.scaleY = changeHeight
|
||||
|
||||
const currentPoints = target.getCurrentPoints()
|
||||
|
||||
target.set({
|
||||
// 기존 객체의 속성들을 복사 (scale은 1로 고정)
|
||||
const originalOptions = {
|
||||
stroke: target.stroke,
|
||||
strokeWidth: target.strokeWidth,
|
||||
fill: target.fill,
|
||||
opacity: target.opacity,
|
||||
visible: target.visible,
|
||||
selectable: target.selectable,
|
||||
evented: target.evented,
|
||||
hoverCursor: target.hoverCursor,
|
||||
moveCursor: target.moveCursor,
|
||||
lockMovementX: target.lockMovementX,
|
||||
lockMovementY: target.lockMovementY,
|
||||
lockRotation: target.lockRotation,
|
||||
lockScalingX: target.lockScalingX,
|
||||
lockScalingY: target.lockScalingY,
|
||||
lockUniScaling: target.lockUniScaling,
|
||||
name: target.name,
|
||||
uuid: target.uuid,
|
||||
roofType: target.roofType,
|
||||
roofMaterial: target.roofMaterial,
|
||||
azimuth: target.azimuth,
|
||||
// tilt: target.tilt,
|
||||
// angle: target.angle,
|
||||
// scale은 항상 1로 고정
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
width: toFixedWithoutRounding(width / 10, 1),
|
||||
height: toFixedWithoutRounding(height / 10, 1),
|
||||
})
|
||||
//크기 변경후 좌표를 재 적용
|
||||
const changedCoords = target.getPointByOrigin(originTarget.originX, originTarget.originY)
|
||||
|
||||
target.set({
|
||||
originX: originTarget.originX,
|
||||
originY: originTarget.originY,
|
||||
left: changedCoords.x,
|
||||
top: changedCoords.y,
|
||||
})
|
||||
canvas.renderAll()
|
||||
|
||||
//면형상 리사이즈시에만
|
||||
target.fire('polygonMoved')
|
||||
target.points = currentPoints
|
||||
target.fire('modified')
|
||||
|
||||
setSurfaceShapePattern(target, roofDisplay.column, false, target.roofMaterial, true)
|
||||
|
||||
if (target.direction) {
|
||||
drawDirectionArrow(target)
|
||||
lines: target.lines,
|
||||
// 기타 모든 사용자 정의 속성들
|
||||
...Object.fromEntries(
|
||||
Object.entries(target).filter(
|
||||
([key, value]) =>
|
||||
!['type', 'left', 'top', 'width', 'height', 'scaleX', 'scaleY', 'points', 'lines', 'texts', 'canvas', 'angle', 'tilt'].includes(key) &&
|
||||
typeof value !== 'function',
|
||||
),
|
||||
),
|
||||
}
|
||||
target.setCoords()
|
||||
canvas.renderAll()
|
||||
|
||||
// 기존 QPolygon 제거
|
||||
canvas.remove(target)
|
||||
|
||||
// 새로운 QPolygon 생성 (scale은 1로 고정됨)
|
||||
const newPolygon = new QPolygon(newPoints, originalOptions, canvas)
|
||||
|
||||
newPolygon.set({
|
||||
originWidth: width,
|
||||
originHeight: height,
|
||||
})
|
||||
|
||||
// 캔버스에 추가
|
||||
canvas.add(newPolygon)
|
||||
|
||||
// 선택 상태 유지
|
||||
canvas.setActiveObject(newPolygon)
|
||||
|
||||
newPolygon.fire('modified')
|
||||
setSurfaceShapePattern(newPolygon, null, null, newPolygon.roofMaterial)
|
||||
drawDirectionArrow(newPolygon)
|
||||
newPolygon.setCoords()
|
||||
changeSurfaceLineType(newPolygon)
|
||||
canvas?.renderAll()
|
||||
closeAll()
|
||||
addPopup(popupId, 1, <SizeSetting id={popupId} side={side} target={newPolygon} />)
|
||||
}
|
||||
|
||||
const changeSurfaceLinePropertyEvent = () => {
|
||||
@ -1364,6 +1437,95 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
return orderedPoints
|
||||
}
|
||||
|
||||
const rotateSurfaceShapeBatch = () => {
|
||||
if (currentObject) {
|
||||
// 관련 객체들 찾기
|
||||
// arrow는 제거
|
||||
const arrow = canvas.getObjects().find((obj) => obj.parentId === currentObject.id && obj.name === 'arrow')
|
||||
if (arrow) {
|
||||
canvas.remove(arrow)
|
||||
}
|
||||
|
||||
const relatedObjects = canvas.getObjects().filter((obj) => obj.parentId === currentObject.id)
|
||||
|
||||
// 그룹화할 객체들 배열 (currentObject + relatedObjects)
|
||||
const objectsToGroup = [currentObject, ...relatedObjects]
|
||||
|
||||
// 기존 객체들을 캔버스에서 제거
|
||||
objectsToGroup.forEach((obj) => canvas.remove(obj))
|
||||
|
||||
// fabric.Group 생성
|
||||
const group = new fabric.Group(objectsToGroup, {
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
})
|
||||
|
||||
// 그룹을 캔버스에 추가
|
||||
canvas.add(group)
|
||||
|
||||
// 현재 회전값에 90도 추가
|
||||
const currentAngle = group.angle || 0
|
||||
const newAngle = (currentAngle + 90) % 360
|
||||
|
||||
// 그룹 전체를 회전
|
||||
group.rotate(newAngle)
|
||||
group.setCoords()
|
||||
|
||||
// 그룹을 해제하고 개별 객체로 복원
|
||||
group._restoreObjectsState()
|
||||
canvas.remove(group)
|
||||
|
||||
// 개별 객체들을 다시 캔버스에 추가하고 처리
|
||||
group.getObjects().forEach((obj) => {
|
||||
canvas.add(obj)
|
||||
obj.setCoords()
|
||||
|
||||
// currentObject인 경우 추가 처리
|
||||
if (obj.id === currentObject.id) {
|
||||
const originWidth = obj.originWidth
|
||||
const originHeight = obj.originHeight
|
||||
|
||||
// QPolygon 내부 구조 재구성 (선이 깨지는 문제 해결)
|
||||
if (obj.type === 'QPolygon' && obj.lines) {
|
||||
obj.initLines()
|
||||
}
|
||||
|
||||
obj.set({
|
||||
originWidth: originHeight,
|
||||
originHeight: originWidth,
|
||||
})
|
||||
} else {
|
||||
// relatedObject인 경우에도 필요한 처리
|
||||
if (obj.type === 'QPolygon' && obj.lines) {
|
||||
obj.initLines()
|
||||
}
|
||||
if (obj.type === 'group') {
|
||||
// 회전 후의 points를 groupPoints로 업데이트
|
||||
// getCurrentPoints를 직접 호출하지 말고 recalculateGroupPoints만 실행
|
||||
|
||||
obj.recalculateGroupPoints()
|
||||
|
||||
obj._objects?.forEach((obj) => {
|
||||
obj.initLines()
|
||||
obj.fire('modified')
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
currentObject.fire('modified')
|
||||
// 화살표와 선 다시 그리기
|
||||
drawDirectionArrow(currentObject)
|
||||
setTimeout(() => {
|
||||
setPolygonLinesActualSize(currentObject)
|
||||
changeSurfaceLineType(currentObject)
|
||||
}, 500)
|
||||
|
||||
// currentObject를 다시 선택 상태로 설정
|
||||
canvas.setActiveObject(currentObject)
|
||||
canvas.renderAll()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
applySurfaceShape,
|
||||
deleteAllSurfacesAndObjects,
|
||||
@ -1373,5 +1535,6 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
changeSurfaceLineProperty,
|
||||
changeSurfaceLinePropertyReset,
|
||||
changeSurfaceLineType,
|
||||
rotateSurfaceShapeBatch,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import { canvasSettingState, canvasState, currentMenuState, currentObjectState } from '@/store/canvasAtom'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { MENU, POLYGON_TYPE } from '@/common/common'
|
||||
import AuxiliarySize from '@/components/floor-plan/modal/auxiliary/AuxiliarySize'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@ -12,11 +11,9 @@ import { gridColorState } from '@/store/gridAtom'
|
||||
import { contextPopupPositionState, contextPopupState } from '@/store/popupAtom'
|
||||
import AuxiliaryEdit from '@/components/floor-plan/modal/auxiliary/AuxiliaryEdit'
|
||||
import SizeSetting from '@/components/floor-plan/modal/object/SizeSetting'
|
||||
import RoofMaterialSetting from '@/components/floor-plan/modal/object/RoofMaterialSetting'
|
||||
import DormerOffset from '@/components/floor-plan/modal/object/DormerOffset'
|
||||
import FontSetting from '@/components/common/font/FontSetting'
|
||||
import RoofAllocationSetting from '@/components/floor-plan/modal/roofAllocation/RoofAllocationSetting'
|
||||
import LinePropertySetting from '@/components/floor-plan/modal/lineProperty/LinePropertySetting'
|
||||
import FlowDirectionSetting from '@/components/floor-plan/modal/flowDirection/FlowDirectionSetting'
|
||||
|
||||
import { useCommonUtils } from './common/useCommonUtils'
|
||||
@ -29,7 +26,6 @@ import ColumnRemove from '@/components/floor-plan/modal/module/column/ColumnRemo
|
||||
import ColumnInsert from '@/components/floor-plan/modal/module/column/ColumnInsert'
|
||||
import RowRemove from '@/components/floor-plan/modal/module/row/RowRemove'
|
||||
import RowInsert from '@/components/floor-plan/modal/module/row/RowInsert'
|
||||
import CircuitNumberEdit from '@/components/floor-plan/modal/module/CircuitNumberEdit'
|
||||
import { useObjectBatch } from '@/hooks/object/useObjectBatch'
|
||||
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
|
||||
import { fontSelector, globalFontAtom } from '@/store/fontAtom'
|
||||
@ -45,6 +41,8 @@ import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placemen
|
||||
import { selectedMenuState } from '@/store/menuAtom'
|
||||
import { useTrestle } from './module/useTrestle'
|
||||
import { useCircuitTrestle } from './useCirCuitTrestle'
|
||||
import { usePolygon } from '@/hooks/usePolygon'
|
||||
import { useText } from '@/hooks/useText'
|
||||
|
||||
export function useContextMenu() {
|
||||
const canvas = useRecoilValue(canvasState)
|
||||
@ -64,7 +62,7 @@ export function useContextMenu() {
|
||||
const [column, setColumn] = useState(null)
|
||||
const { handleZoomClear } = useCanvasEvent()
|
||||
const { moveObjectBatch, copyObjectBatch } = useObjectBatch({})
|
||||
const { moveSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||
const { moveSurfaceShapeBatch, rotateSurfaceShapeBatch } = useSurfaceShapeBatch({})
|
||||
const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom)
|
||||
const { addLine, removeLine } = useLine()
|
||||
const { removeGrid } = useGrid()
|
||||
@ -73,10 +71,12 @@ export function useContextMenu() {
|
||||
const { settingsData, setSettingsDataSave } = useCanvasSetting(false)
|
||||
const { swalFire } = useSwal()
|
||||
const { alignModule, modulesRemove, moduleRoofRemove } = useModule()
|
||||
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines } = useRoofFn()
|
||||
const { removeRoofMaterial, removeAllRoofMaterial, moveRoofMaterial, removeOuterLines, setSurfaceShapePattern } = useRoofFn()
|
||||
const selectedMenu = useRecoilValue(selectedMenuState)
|
||||
const { isAllComplete, clear: resetModule } = useTrestle()
|
||||
const { isExistCircuit } = useCircuitTrestle()
|
||||
const { changeCorridorDimensionText } = useText()
|
||||
const { setPolygonLinesActualSize, drawDirectionArrow } = usePolygon()
|
||||
const currentMenuSetting = () => {
|
||||
switch (selectedMenu) {
|
||||
case 'outline':
|
||||
@ -170,6 +170,11 @@ export function useContextMenu() {
|
||||
name: getMessage('contextmenu.size.edit'),
|
||||
component: <SizeSetting id={popupId} target={currentObject} />,
|
||||
},
|
||||
{
|
||||
id: 'rotate',
|
||||
name: `${getMessage('contextmenu.rotate')}`,
|
||||
fn: () => rotateSurfaceShapeBatch(),
|
||||
},
|
||||
{
|
||||
id: 'roofMaterialRemove',
|
||||
shortcut: ['d', 'D'],
|
||||
|
||||
@ -213,7 +213,7 @@ export function useEvent() {
|
||||
const modulePoints = []
|
||||
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||
modules.forEach((module) => {
|
||||
module.points.forEach((point) => {
|
||||
module.getCurrentPoints().forEach((point) => {
|
||||
modulePoints.push({ x: point.x, y: point.y })
|
||||
})
|
||||
})
|
||||
|
||||
@ -173,6 +173,13 @@ export function usePlan(params = {}) {
|
||||
* @param {boolean} saveAlert - 저장 완료 알림 표시 여부
|
||||
*/
|
||||
const saveCanvas = async (saveAlert = true) => {
|
||||
// 저장 전 선택되어 있는 object 제거
|
||||
const setupSurfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
|
||||
|
||||
setupSurfaces.forEach((surface) => {
|
||||
surface.set({ fill: 'rgba(255,255,255,0.1)', strokeDashArray: [10, 4], strokeWidth: 1 })
|
||||
})
|
||||
|
||||
const canvasStatus = currentCanvasData('save')
|
||||
const result = await putCanvasStatus(canvasStatus, saveAlert)
|
||||
//캔버스 저장 완료 후
|
||||
|
||||
@ -99,6 +99,8 @@
|
||||
"modal.module.basic.setting.module.construction.method": "工法",
|
||||
"modal.module.basic.setting.module.under.roof": "屋根下地",
|
||||
"modal.module.basic.setting.module.setting": "架台設定",
|
||||
"modal.module.basic.setting.module.series.setting": "モジュールシリーズ",
|
||||
"modal.module.basic.setting.module.setting2": "モジュール選択",
|
||||
"modal.module.basic.setting.module.placement.area": "モジュール配置領域",
|
||||
"modal.module.basic.setting.module.placement.margin": "モジュール間の間隙",
|
||||
"modal.module.basic.setting.module.placement.area.eaves": "軒側",
|
||||
@ -444,6 +446,7 @@
|
||||
"contextmenu.remove": "削除",
|
||||
"contextmenu.remove.all": "完全削除",
|
||||
"contextmenu.move": "移動",
|
||||
"contextmenu.rotate": "回転",
|
||||
"contextmenu.copy": "コピー",
|
||||
"contextmenu.edit": "編集",
|
||||
"contextmenu.module.vertical.align": "モジュールの垂直方向の中央揃え",
|
||||
|
||||
@ -98,7 +98,9 @@
|
||||
"modal.module.basic.setting.module.rafter.margin": "서까래 간격",
|
||||
"modal.module.basic.setting.module.construction.method": "공법",
|
||||
"modal.module.basic.setting.module.under.roof": "지붕밑바탕",
|
||||
"modal.module.basic.setting.module.setting": "모듈 선택",
|
||||
"modal.module.basic.setting.module.setting": "가대 설정",
|
||||
"modal.module.basic.setting.module.series.setting": "모듈 시리즈",
|
||||
"modal.module.basic.setting.module.setting2": "모듈 선택",
|
||||
"modal.module.basic.setting.module.placement.area": "모듈 배치 영역",
|
||||
"modal.module.basic.setting.module.placement.margin": "모듈 배치 간격",
|
||||
"modal.module.basic.setting.module.placement.area.eaves": "처마쪽",
|
||||
@ -444,6 +446,7 @@
|
||||
"contextmenu.remove": "삭제",
|
||||
"contextmenu.remove.all": "전체 삭제",
|
||||
"contextmenu.move": "이동",
|
||||
"contextmenu.rotate": "회전",
|
||||
"contextmenu.copy": "복사",
|
||||
"contextmenu.edit": "편집",
|
||||
"contextmenu.module.vertical.align": "모듈 세로 가운데 정렬",
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
// Variables
|
||||
$colors: (
|
||||
'dark-700': #1f2937,
|
||||
'dark-600': #334155,
|
||||
'dark-500': #4b5563,
|
||||
'dark-400': #6b7280,
|
||||
'dark-300': #374151,
|
||||
'primary': #10b981,
|
||||
'primary-dark': #059669,
|
||||
'danger': #ef4444,
|
||||
'danger-dark': #dc2626,
|
||||
'warning': #f59e0b,
|
||||
'warning-dark': #d97706,
|
||||
'border': #475569
|
||||
'dark-700': #1f2937,
|
||||
'dark-600': #334155,
|
||||
'dark-500': #4b5563,
|
||||
'dark-400': #6b7280,
|
||||
'dark-300': #374151,
|
||||
'primary': #10b981,
|
||||
'primary-dark': #059669,
|
||||
'danger': #ef4444,
|
||||
'danger-dark': #dc2626,
|
||||
'warning': #f59e0b,
|
||||
'warning-dark': #d97706,
|
||||
'border': #475569,
|
||||
);
|
||||
|
||||
// Mixins
|
||||
@ -46,10 +46,15 @@ $colors: (
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 0.5rem;
|
||||
text-align: right;
|
||||
text-align: left; // 왼쪽 정렬
|
||||
cursor: pointer;
|
||||
font-size: 0.625rem;
|
||||
box-sizing: border-box;
|
||||
direction: ltr; // 왼쪽에서 오른쪽으로 입력
|
||||
white-space: nowrap; // 줄바꿈 방지
|
||||
// input 요소에서는 overflow 대신 다른 방법 사용
|
||||
text-overflow: clip; // 텍스트 잘림 처리
|
||||
unicode-bidi: bidi-override; // 텍스트 방향 강제
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
@ -67,7 +72,9 @@ $colors: (
|
||||
width: 120px;
|
||||
background-color: map-get($colors, 'dark-700');
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
padding: 0.5rem;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.15s ease-out;
|
||||
|
||||
232
src/util/fabric-extensions.js
Normal file
232
src/util/fabric-extensions.js
Normal file
@ -0,0 +1,232 @@
|
||||
import { fabric } from 'fabric'
|
||||
|
||||
/**
|
||||
* fabric.Rect에 getCurrentPoints 메서드를 추가
|
||||
* QPolygon의 getCurrentPoints와 동일한 방식으로 변형된 현재 점들을 반환
|
||||
*/
|
||||
fabric.Rect.prototype.getCurrentPoints = function () {
|
||||
// 사각형의 네 모서리 점들을 계산
|
||||
const width = this.width
|
||||
const height = this.height
|
||||
|
||||
// 사각형의 로컬 좌표계에서의 네 모서리 점
|
||||
const points = [
|
||||
{ x: -width / 2, y: -height / 2 }, // 좌상단
|
||||
{ x: width / 2, y: -height / 2 }, // 우상단
|
||||
{ x: width / 2, y: height / 2 }, // 우하단
|
||||
{ x: -width / 2, y: height / 2 }, // 좌하단
|
||||
]
|
||||
|
||||
// 변형 매트릭스 계산
|
||||
const matrix = this.calcTransformMatrix()
|
||||
|
||||
// 각 점을 변형 매트릭스로 변환
|
||||
return points.map(function (p) {
|
||||
const point = new fabric.Point(p.x, p.y)
|
||||
return fabric.util.transformPoint(point, matrix)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용)
|
||||
* 그룹의 groupPoints를 다시 계산하여 반환
|
||||
*/
|
||||
fabric.Group.prototype.getCurrentPoints = function () {
|
||||
// groupPoints를 다시 계산
|
||||
|
||||
// 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우)
|
||||
if (this.groupPoints && Array.isArray(this.groupPoints)) {
|
||||
const matrix = this.calcTransformMatrix()
|
||||
console.log('this.groupPoints', this.groupPoints)
|
||||
return this.groupPoints.map(function (p) {
|
||||
const point = new fabric.Point(p.x, p.y)
|
||||
return fabric.util.transformPoint(point, matrix)
|
||||
})
|
||||
}
|
||||
|
||||
// groupPoints가 없으면 바운딩 박스를 사용
|
||||
const bounds = this.getBoundingRect()
|
||||
const points = [
|
||||
{ x: bounds.left, y: bounds.top },
|
||||
{ x: bounds.left + bounds.width, y: bounds.top },
|
||||
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
|
||||
{ x: bounds.left, y: bounds.top + bounds.height },
|
||||
]
|
||||
|
||||
return points.map(function (p) {
|
||||
return new fabric.Point(p.x, p.y)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* fabric.Group에 groupPoints 재계산 메서드 추가
|
||||
* 그룹 내 모든 객체의 점들을 기반으로 groupPoints를 새로 계산
|
||||
* Convex Hull 알고리즘을 사용하여 가장 외곽의 점들만 반환
|
||||
*/
|
||||
fabric.Group.prototype.recalculateGroupPoints = function () {
|
||||
if (!this._objects || this._objects.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let allPoints = []
|
||||
|
||||
// 그룹 내 모든 객체의 점들을 수집
|
||||
this._objects.forEach(function (obj) {
|
||||
if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') {
|
||||
// getCurrentPoints가 있는 객체는 해당 메서드 사용
|
||||
const objPoints = obj.getCurrentPoints()
|
||||
allPoints = allPoints.concat(objPoints)
|
||||
} else if (obj.points && Array.isArray(obj.points)) {
|
||||
// QPolygon과 같이 points 배열이 있는 경우
|
||||
const pathOffset = obj.pathOffset || { x: 0, y: 0 }
|
||||
const matrix = obj.calcTransformMatrix()
|
||||
const transformedPoints = obj.points
|
||||
.map(function (p) {
|
||||
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
|
||||
})
|
||||
.map(function (p) {
|
||||
return fabric.util.transformPoint(p, matrix)
|
||||
})
|
||||
allPoints = allPoints.concat(transformedPoints)
|
||||
} else {
|
||||
// 일반 객체는 바운딩 박스의 네 모서리 점 사용
|
||||
const bounds = obj.getBoundingRect()
|
||||
const cornerPoints = [
|
||||
{ x: bounds.left, y: bounds.top },
|
||||
{ x: bounds.left + bounds.width, y: bounds.top },
|
||||
{ x: bounds.left + bounds.width, y: bounds.top + bounds.height },
|
||||
{ x: bounds.left, y: bounds.top + bounds.height },
|
||||
]
|
||||
allPoints = allPoints.concat(
|
||||
cornerPoints.map(function (p) {
|
||||
return new fabric.Point(p.x, p.y)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (allPoints.length > 0) {
|
||||
// Convex Hull 알고리즘을 사용하여 외곽 점들만 추출
|
||||
const convexHullPoints = this.getConvexHull(allPoints)
|
||||
|
||||
// 그룹의 로컬 좌표계로 변환하기 위해 그룹의 역변환 적용
|
||||
const groupMatrix = this.calcTransformMatrix()
|
||||
const invertedMatrix = fabric.util.invertTransform(groupMatrix)
|
||||
|
||||
this.groupPoints = convexHullPoints.map(function (p) {
|
||||
const localPoint = fabric.util.transformPoint(p, invertedMatrix)
|
||||
return { x: localPoint.x, y: localPoint.y }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graham Scan 알고리즘을 사용한 Convex Hull 계산
|
||||
* 점들의 집합에서 가장 외곽의 점들만 반환
|
||||
*/
|
||||
fabric.Group.prototype.getConvexHull = function (points) {
|
||||
if (points.length < 3) return points
|
||||
|
||||
// 중복 점 제거
|
||||
const uniquePoints = []
|
||||
const seen = new Set()
|
||||
|
||||
points.forEach(function (p) {
|
||||
const key = `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key)
|
||||
uniquePoints.push({ x: p.x, y: p.y })
|
||||
}
|
||||
})
|
||||
|
||||
if (uniquePoints.length < 3) return uniquePoints
|
||||
|
||||
// 가장 아래쪽 점을 찾기 (y가 가장 작고, 같으면 x가 가장 작은 점)
|
||||
let pivot = uniquePoints[0]
|
||||
for (let i = 1; i < uniquePoints.length; i++) {
|
||||
if (uniquePoints[i].y < pivot.y || (uniquePoints[i].y === pivot.y && uniquePoints[i].x < pivot.x)) {
|
||||
pivot = uniquePoints[i]
|
||||
}
|
||||
}
|
||||
|
||||
// 극각에 따라 정렬
|
||||
const sortedPoints = uniquePoints
|
||||
.filter(function (p) { return p !== pivot })
|
||||
.sort(function (a, b) {
|
||||
const angleA = Math.atan2(a.y - pivot.y, a.x - pivot.x)
|
||||
const angleB = Math.atan2(b.y - pivot.y, b.x - pivot.x)
|
||||
if (angleA !== angleB) return angleA - angleB
|
||||
|
||||
// 각도가 같으면 거리로 정렬
|
||||
const distA = Math.pow(a.x - pivot.x, 2) + Math.pow(a.y - pivot.y, 2)
|
||||
const distB = Math.pow(b.x - pivot.x, 2) + Math.pow(b.y - pivot.y, 2)
|
||||
return distA - distB
|
||||
})
|
||||
|
||||
// Graham Scan 실행
|
||||
const hull = [pivot]
|
||||
|
||||
for (let i = 0; i < sortedPoints.length; i++) {
|
||||
const current = sortedPoints[i]
|
||||
|
||||
// 반시계방향이 아닌 점들 제거
|
||||
while (hull.length > 1) {
|
||||
const p1 = hull[hull.length - 2]
|
||||
const p2 = hull[hull.length - 1]
|
||||
const cross = (p2.x - p1.x) * (current.y - p1.y) - (p2.y - p1.y) * (current.x - p1.x)
|
||||
|
||||
if (cross > 0) break // 반시계방향이면 유지
|
||||
hull.pop() // 시계방향이면 제거
|
||||
}
|
||||
|
||||
hull.push(current)
|
||||
}
|
||||
|
||||
return hull
|
||||
}
|
||||
|
||||
/**
|
||||
* fabric.Triangle에 getCurrentPoints 메서드를 추가
|
||||
* 삼각형의 세 꼭짓점을 반환
|
||||
*/
|
||||
fabric.Triangle.prototype.getCurrentPoints = function () {
|
||||
const width = this.width
|
||||
const height = this.height
|
||||
|
||||
// 삼각형의 로컬 좌표계에서의 세 꼭짓점
|
||||
const points = [
|
||||
{ x: 0, y: -height / 2 }, // 상단 중앙
|
||||
{ x: -width / 2, y: height / 2 }, // 좌하단
|
||||
{ x: width / 2, y: height / 2 }, // 우하단
|
||||
]
|
||||
|
||||
// 변형 매트릭스 계산
|
||||
const matrix = this.calcTransformMatrix()
|
||||
|
||||
// 각 점을 변형 매트릭스로 변환
|
||||
return points.map(function (p) {
|
||||
const point = new fabric.Point(p.x, p.y)
|
||||
return fabric.util.transformPoint(point, matrix)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* fabric.Polygon에 getCurrentPoints 메서드를 추가 (QPolygon이 아닌 일반 Polygon용)
|
||||
* QPolygon과 동일한 방식으로 구현
|
||||
*/
|
||||
if (!fabric.Polygon.prototype.getCurrentPoints) {
|
||||
fabric.Polygon.prototype.getCurrentPoints = function () {
|
||||
const pathOffset = this.get('pathOffset') || { x: 0, y: 0 }
|
||||
const matrix = this.calcTransformMatrix()
|
||||
|
||||
return this.get('points')
|
||||
.map(function (p) {
|
||||
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
|
||||
})
|
||||
.map(function (p) {
|
||||
return fabric.util.transformPoint(p, matrix)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default {}
|
||||
2050
src/util/skeleton-utils.js
Normal file
2050
src/util/skeleton-utils.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user