Compare commits

..

30 Commits

Author SHA1 Message Date
b6258c6a2a actureSize 2026-01-25 23:42:57 +09:00
75326e38ee Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev 2026-01-23 17:43:11 +09:00
3284401526 pcs 자동추천 이상 수정 2026-01-23 17:43:03 +09:00
1d941724db 할당변경(orange 제거) 2026-01-22 18:13:42 +09:00
f4846e62b0 eaveHelpLine actualSize 추가 2026-01-22 17:36:48 +09:00
eeddc300fd Merge pull request '전화번호 - 포함, 팩스필수' (#593) from dev_ysCha into dev
Reviewed-on: #593
2026-01-20 15:03:51 +09:00
2316e92cfe 전화번호 - 포함, 팩스필수 2026-01-20 15:03:03 +09:00
5cebb8a530 Merge pull request 'dev_ysCha' (#590) from dev_ysCha into dev
Reviewed-on: #590
2026-01-19 15:39:36 +09:00
578f6e8bd4 Merge remote-tracking branch 'origin/dev_ysCha' into dev_ysCha 2026-01-19 15:37:53 +09:00
bb47f14d95 [1413] 지붕재 목록 api 호출 다수 => 한번 2026-01-19 15:37:39 +09:00
a98fb2560f Merge pull request 'dev' (#587) from dev into dev_ysCha
Reviewed-on: #587
2026-01-19 13:41:29 +09:00
87a283c79d Merge pull request '이메일 30자 -> 50자' (#586) from dev_ysCha into dev
Reviewed-on: #586
2026-01-19 13:40:52 +09:00
97b3a8c2ba Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev_ysCha 2026-01-19 13:39:33 +09:00
a687be997f 이메일 30자 -> 50자 2026-01-19 13:39:11 +09:00
b361ccf4b7 세로로 긴 경우 이미지 잘리는 현상 수정 2026-01-19 10:23:12 +09:00
76e8e581db [1410] : [HANASYS DESIGN] [북면]으로 취급하는 범위에 대해 늘어남에 따라 계산 수정 2026-01-16 17:23:42 +09:00
310068bf6a circuitTestle 폰트 수정 제거 2026-01-16 09:51:32 +09:00
d95dcf608d 동쪽 치도리 배치시 문제 수정 2026-01-15 14:36:26 +09:00
6c23240345 logout 호출 시 전부 login 으로 튕기도록 수정 2026-01-15 10:26:39 +09:00
8475dc4b1f 패스워드 변경 팝업 변경안함 클릭 시 멈춤현상 수정 2026-01-15 10:24:50 +09:00
c501896f59 선 작성 시 기본 attributes.planeSize 추가 2026-01-15 09:55:52 +09:00
4fdd7d55d0 형올림내림 시 오류 수정 2026-01-13 13:44:29 +09:00
0f481e2933 선이 가지고있는 planeSize로 계산하도록 수정 2026-01-13 13:32:45 +09:00
65e268f3d6 Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into dev 2026-01-12 16:10:31 +09:00
f91afaed3a pcs가 한개인 경우 selectbox 제거 2026-01-12 16:10:27 +09:00
363c19d9a0 Merge pull request '[1383]견적서 아이템 인코딩' (#575) from dev_ysCha into dev
Reviewed-on: #575
2026-01-12 15:38:56 +09:00
3dd96d043f [1383]견적서 아이템 인코딩 2026-01-12 15:38:21 +09:00
aa837d8dbf 블럭처리 2026-01-12 11:12:29 +09:00
bdd7a475d5 1차점 생성 2차점 가격 미노출 2026-01-12 11:08:49 +09:00
a368c8d40d Merge pull request '1차점에서 만든 견적서 2차점 가격 미노출 설정' (#571) from dev_cha into dev
Reviewed-on: #571
2026-01-12 09:05:06 +09:00
17 changed files with 383 additions and 261 deletions

View File

@ -95,16 +95,11 @@ const resizeImage = async (image) => {
const scaleY = targetImageHeight / image.bitmap.height
let scale = Math.min(scaleX, scaleY) // 비율 유지하면서 최대한 크게
// scale 저장 (나중에 전체 확대에 사용)
const originalScale = scale
let finalWidth = Math.round(image.bitmap.width * scale)
let finalHeight = Math.round(image.bitmap.height * scale)
if (scale >= 0.6) {
// 실제 리사이즈 실행
// 항상 리사이즈 실행 (scale >= 0.6 조건 제거)
image.resize({ w: finalWidth, h: finalHeight })
}
//배경 이미지를 생성
const mixedImage = new Jimp({ width: convertStandardWidth, height: convertStandardHeight, color: 0xffffffff })
@ -119,14 +114,7 @@ const resizeImage = async (image) => {
opacityDest: 1,
})
// scale이 0.8 이하인 경우 완성된 이미지를 전체적으로 확대
if (originalScale <= 0.8) {
const enlargeRatio = 1.5 // 50% 확대
const newWidth = Math.round(mixedImage.bitmap.width * enlargeRatio)
const newHeight = Math.round(mixedImage.bitmap.height * enlargeRatio)
mixedImage.resize({ w: newWidth, h: newHeight })
}
// 1.5x 확대 로직 제거 - 이미지가 템플릿 크기를 초과하지 않도록 함
return mixedImage
}

View File

@ -33,6 +33,11 @@ export default function Join() {
//
const joinValidation = (formData) => {
// /FAX ( : 0 , )
const telRegex = /^0\d{1,4}-\d{1,4}-\d{4}$/
// -
const storeQcastNm = formData.get('storeQcastNm')
if (!isObjectNotEmpty(storeQcastNm)) {
@ -65,12 +70,34 @@ export default function Join() {
return false
}
// -
const telNo = formData.get('telNo')
if (!isObjectNotEmpty(telNo)) {
alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')]))
telNoRef.current.focus()
return false
} else if (!telRegex.test(telNo)) {
alert(getMessage('join.validation.check1', [getMessage('join.sub1.telNo')]))
telNoRef.current.focus()
return false
}
//
// // -
// const telNo = formData.get('telNo')
// if (!isObjectNotEmpty(telNo)) {
// alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')]))
// telNoRef.current.focus()
// return false
// }
// - FAX
const fax = formData.get('fax')
if (!isObjectNotEmpty(fax)) {
alert(getMessage('common.message.required.data', [getMessage('join.sub1.fax')]))
faxRef.current.focus()
return false
}
const bizNo = formData.get('bizNo')
@ -122,16 +149,38 @@ export default function Join() {
}
// -
// const userTelNo = formData.get('userTelNo')
// if (!isObjectNotEmpty(userTelNo)) {
// alert(getMessage('common.message.required.data', [getMessage('join.sub2.telNo')]))
// userTelNoRef.current.focus()
// return false
// }
const userTelNo = formData.get('userTelNo')
if (!isObjectNotEmpty(userTelNo)) {
alert(getMessage('common.message.required.data', [getMessage('join.sub2.telNo')]))
alert(getMessage('common.message.required.data', [getMessage('join.sub1.telNo')]))
userTelNoRef.current.focus()
return false
} else if (!telRegex.test(userTelNo)) {
alert(getMessage('join.validation.check1', [getMessage('join.sub1.telNo')]))
userTelNoRef.current.focus()
return false
}
// - FAX
const userFax = formData.get('userFax')
if (!isObjectNotEmpty(userFax)) {
alert(getMessage('common.message.required.data', [getMessage('join.sub2.fax')]))
userFaxRef.current.focus()
return false
}
return true
}
//
const joinProcess = async (e) => {
e.preventDefault()
@ -288,7 +337,8 @@ export default function Join() {
name="telNo"
className="input-light"
maxLength={15}
onChange={inputNumberCheck}
placeholder={getMessage('join.sub1.telNo_placeholder')}
onChange={inputTelNumberCheck}
ref={telNoRef}
/>
</div>
@ -296,7 +346,7 @@ export default function Join() {
</tr>
{/* FAX 번호 */}
<tr>
<th>{getMessage('join.sub1.fax')}</th>
<th>{getMessage('join.sub1.fax')}<span className="important">*</span></th>
<td>
<div className="input-wrap" style={{ width: '200px' }}>
<input type="text" id="fax" name="fax" className="input-light" maxLength={15} onChange={inputNumberCheck} ref={faxRef} />
@ -381,7 +431,7 @@ export default function Join() {
</th>
<td>
<div className="input-wrap" style={{ width: '200px' }}>
<input type="text" id="email" name="email" className="input-light" maxLength={30} ref={emailRef} />
<input type="text" id="email" name="email" className="input-light" maxLength={50} ref={emailRef} />
</div>
</td>
</tr>
@ -398,7 +448,8 @@ export default function Join() {
name="userTelNo"
className="input-light"
maxLength={15}
onChange={inputNumberCheck}
placeholder={getMessage('join.sub1.telNo_placeholder')}
onChange={inputTelNumberCheck}
ref={userTelNoRef}
/>
</div>
@ -406,7 +457,7 @@ export default function Join() {
</tr>
{/* FAX 번호 */}
<tr>
<th>{getMessage('join.sub2.fax')}</th>
<th>{getMessage('join.sub2.fax')}<span className="important">*</span></th>
<td>
<div className="input-wrap" style={{ width: '200px' }}>
<input

View File

@ -63,6 +63,33 @@ export const CalculatorInput = forwardRef(
const calculator = calculatorRef.current
let newDisplayValue = ''
// (Selection)
if (inputRef.current) {
const { selectionStart, selectionEnd } = inputRef.current
//
if (selectionStart !== null && selectionEnd !== null && selectionStart !== selectionEnd) {
// ( )
if (!hasOperation) {
calculator.currentOperand = num.toString()
calculator.previousOperand = ''
calculator.operation = undefined
calculator.shouldResetDisplay = false
newDisplayValue = calculator.currentOperand
setDisplayValue(newDisplayValue)
onChange(newDisplayValue)
requestAnimationFrame(() => {
if (inputRef.current) {
inputRef.current.focus()
inputRef.current.setSelectionRange(newDisplayValue.length, newDisplayValue.length)
}
})
return //
}
}
}
// maxLength
if (maxLength > 0) {
const currentLength = (calculator.currentOperand || '').length + (calculator.previousOperand || '').length + (calculator.operation || '').length

View File

@ -2039,7 +2039,11 @@ export default function Estimate({}) {
}
}}
menuPlacement={'auto'}
getOptionLabel={(x) => x.itemName + ' (' + x.itemNo + ')'}
getOptionLabel={(x) => {
//
const doc = new DOMParser().parseFromString(x.itemName, 'text/html');
return (doc.documentElement.textContent || x.itemName) + ' (' + x.itemNo + ')';
}}
getOptionValue={(x) => x.itemNo}
components={{
SingleValue: ({ children, ...props }) => {
@ -2048,13 +2052,21 @@ export default function Estimate({}) {
}}
isClearable={false}
isDisabled={!!item?.paDispOrder}
value={displayItemList.filter(function (option) {
if (item.itemNo === '') {
return false
} else {
return option.itemId === item.itemId
value={(() => {
const selectedOption = displayItemList.find((option) => {
return item.itemNo !== '' && option.itemId === item.itemId;
});
if (selectedOption) {
// itemName
const doc = new DOMParser().parseFromString(selectedOption.itemName, 'text/html');
return {
...selectedOption,
itemName: doc.documentElement.textContent || selectedOption.itemName
};
}
})}
return null;
})()}
/>
) : (
<Select

View File

@ -8,7 +8,7 @@ import { usePathname, useSearchParams } from 'next/navigation'
import { QcastContext } from '@/app/QcastProvider'
import { sessionStore } from '@/store/commonAtom'
export default function DocDownOptionPop({ planNo, setEstimatePopupOpen, docDownPopLockFlg, saleStoreId = '' }) {
export default function DocDownOptionPop({ planNo, setEstimatePopupOpen, docDownPopLockFlg, createStoreId = '' }) {
const { setIsGlobalLoading } = useContext(QcastContext)
const { getMessage } = useMessage()
@ -69,8 +69,9 @@ export default function DocDownOptionPop({ planNo, setEstimatePopupOpen, docDown
schDrawingFlg: defaultSchDrawingFlg,
pwrGnrSimType: 'D', //default
userId: sessionState.userId ? sessionState.userId : "",
saleStoreId: saleStoreId ? saleStoreId : (sessionState.saleStoreId ? sessionState.saleStoreId : ""),
storeLvl: sessionState.storeLvl
saleStoreId: sessionState.storeId ? sessionState.storeId : "",
storeLvl: sessionState.storeLvl,
createStoreId: createStoreId ? createStoreId : '',
}
const options = { responseType: 'blob' }

View File

@ -9,7 +9,7 @@ import BasicSetting from '@/components/floor-plan/modal/basic/BasicSetting'
import { useMasterController } from '@/hooks/common/useMasterController'
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { GlobalDataContext } from '@/app/GlobalDataProvider'
import { POLYGON_TYPE, MENU } from '@/common/common'
import { MENU, POLYGON_TYPE } from '@/common/common'
import { useSwal } from '@/hooks/useSwal'
import { canvasState, canvasZoomState, currentMenuState } from '@/store/canvasAtom'
@ -151,23 +151,30 @@ export default function CircuitTrestleSetting({ id }) {
// :
const getSortFn = () => {
switch (direction) {
case 'south': return (a, b) => b.y - a.y // (y )
case 'north': return (a, b) => a.y - b.y // (y )
case 'east': return (a, b) => b.x - a.x // (x )
case 'west': return (a, b) => a.x - b.x // (x )
default: return () => 0
case 'south':
return (a, b) => b.y - a.y // (y )
case 'north':
return (a, b) => a.y - b.y // (y )
case 'east':
return (a, b) => b.x - a.x // (x )
case 'west':
return (a, b) => a.x - b.x // (x )
default:
return () => 0
}
}
// ( )
const findTargetModules = (current, margin) => {
return centerPoints.filter(cp => {
const sameAxis = isVertical
? Math.abs(cp.x - current.x) < margin
: Math.abs(cp.y - current.y) < margin
const targetDirection = direction === 'south' ? cp.y < current.y
: direction === 'north' ? cp.y > current.y
: direction === 'east' ? cp.x < current.x
return centerPoints.filter((cp) => {
const sameAxis = isVertical ? Math.abs(cp.x - current.x) < margin : Math.abs(cp.y - current.y) < margin
const targetDirection =
direction === 'south'
? cp.y < current.y
: direction === 'north'
? cp.y > current.y
: direction === 'east'
? cp.x < current.x
: cp.x > current.x
return sameAxis && targetDirection
})
@ -178,11 +185,16 @@ export default function CircuitTrestleSetting({ id }) {
if (filtered.length === 0) return null
return filtered.reduce((closest, cp) => {
switch (direction) {
case 'south': return cp.y > closest.y ? cp : closest
case 'north': return cp.y < closest.y ? cp : closest
case 'east': return cp.x > closest.x ? cp : closest
case 'west': return cp.x < closest.x ? cp : closest
default: return closest
case 'south':
return cp.y > closest.y ? cp : closest
case 'north':
return cp.y < closest.y ? cp : closest
case 'east':
return cp.x > closest.x ? cp : closest
case 'west':
return cp.x < closest.x ? cp : closest
default:
return closest
}
})
}
@ -191,12 +203,12 @@ export default function CircuitTrestleSetting({ id }) {
const getGap = (current, target) => {
if (isVertical) {
return direction === 'south'
? (current.y - current.height / 2) - (target.y + target.height / 2)
: (target.y - target.height / 2) - (current.y + current.height / 2)
? current.y - current.height / 2 - (target.y + target.height / 2)
: target.y - target.height / 2 - (current.y + current.height / 2)
} else {
return direction === 'east'
? (current.x - current.width / 2) - (target.x + target.width / 2)
: (target.x - target.width / 2) - (current.x + current.width / 2)
? current.x - current.width / 2 - (target.x + target.width / 2)
: target.x - target.width / 2 - (current.x + current.width / 2)
}
}
@ -228,9 +240,13 @@ export default function CircuitTrestleSetting({ id }) {
const offsetAxis = isVertical
? Math.abs(cp.x - (current.x + offsetValue)) <= primaryInterval
: Math.abs(cp.y - (current.y + offsetValue)) <= primaryInterval
const targetDirection = direction === 'south' ? cp.y < current.y
: direction === 'north' ? cp.y > current.y
: direction === 'east' ? cp.x < current.x
const targetDirection =
direction === 'south'
? cp.y < current.y
: direction === 'north'
? cp.y > current.y
: direction === 'east'
? cp.x < current.x
: cp.x > current.x
return offsetAxis && targetDirection
})
@ -344,9 +360,9 @@ export default function CircuitTrestleSetting({ id }) {
canvas.zoomToPoint(new fabric.Point(x, y), 0.4)
changeFontSize('lengthText', '28')
changeFontSize('circuitNumber', '28')
changeFontSize('flowText', '28')
// changeFontSize('lengthText', '28')
// changeFontSize('circuitNumber', '28')
// changeFontSize('flowText', '28')
canvas.renderAll()
}
@ -428,12 +444,13 @@ export default function CircuitTrestleSetting({ id }) {
let pcsObj = {}
models.forEach((model) => {
pcsObj[model.itemId] = model
pcsObj[`${model.pcsSerCd}_${model.itemId}`] = model
})
res.data?.pcsItemList.forEach((item) => {
if (pcsObj[item.itemId]) {
const key = `${item.pcsSerCd}_${item.itemId}`
if (pcsObj[key]) {
pcsItemList.push({
...pcsObj[item.itemId],
...pcsObj[key],
isUsed: false,
id: uuidv4(),
})
@ -560,12 +577,13 @@ export default function CircuitTrestleSetting({ id }) {
let pcsItemList = []
let pcsObj = {}
models.forEach((model) => {
pcsObj[model.itemId] = model
pcsObj[`${model.pcsSerCd}_${model.itemId}`] = model
})
res.data?.pcsItemList.forEach((item) => {
if (pcsObj[item.itemId]) {
const key = `${item.pcsSerCd}_${item.itemId}`
if (pcsObj[key]) {
pcsItemList.push({
...pcsObj[item.itemId],
...pcsObj[key],
isUsed: false,
id: uuidv4(),
})

View File

@ -649,12 +649,18 @@ export default function StepUp(props) {
</tr>
</thead>
<tbody>
{pcsItem.serQtyList.map((item, serQtyIdx) => {
{pcsItem?.serQtyList?.map((item, serQtyIdx) => {
return (
<tr
key={`row-${serQtyIdx}`}
className={`${item.selected ? 'on' : ''}`}
style={{ cursor: allocationType === 'auto' ? 'pointer' : 'default' }}
onClick={() => {
if (stepUp?.pcsItemList.length !== 1) {
return
}
handleRowClick(idx, serQtyIdx, item.paralQty)
}}
>
<td
className="al-r"
@ -667,6 +673,7 @@ export default function StepUp(props) {
<td className="al-r">
{/* 2025.12.04 select 추가 */}
{idx === 0 ? (
stepUp?.pcsItemList.length !== 1 ? (
<select
className="select-light dark table-select"
defaultValue={item.paralQty}
@ -696,6 +703,9 @@ export default function StepUp(props) {
</select>
) : (
<>{item.paralQty}</>
)
) : (
<>{item.paralQty}</>
)}
</td>
{/* <td className="al-r">{item.paralQty}</td> */}

View File

@ -2,12 +2,13 @@ import { useContext } from 'react'
import { useMessage } from '@/hooks/useMessage'
import { useForm } from 'react-hook-form'
import { sessionStore } from '@/store/commonAtom'
import { useRecoilValue, useRecoilState } from 'recoil'
import { useRecoilState, useRecoilValue } from 'recoil'
import { useAxios } from '@/hooks/useAxios'
import { globalLocaleStore } from '@/store/localeAtom'
import { logout, setSession, login } from '@/lib/authActions'
import { login, logout, setSession } from '@/lib/authActions'
import { useSwal } from '@/hooks/useSwal'
import { QcastContext } from '@/app/QcastProvider'
import { useRouter } from 'next/navigation'
export default function ChangePasswordPop(props) {
const globalLocaleState = useRecoilValue(globalLocaleStore)
@ -18,6 +19,7 @@ export default function ChangePasswordPop(props) {
const { patch } = useAxios(globalLocaleState)
const { getMessage } = useMessage()
const [sessionState, setSessionState] = useRecoilState(sessionStore)
const router = useRouter()
const formInitValue = {
password1: '',
password2: '',
@ -128,12 +130,14 @@ export default function ChangePasswordPop(props) {
} else {
setIsGlobalLoading(false)
logout()
router.replace('/login', undefined, { shallow: true })
console.log('code not 200 error')
}
})
.catch((error) => {
setIsGlobalLoading(false)
logout()
router.replace('/login', undefined, { shallow: true })
console.log('catch::::::::', error)
})
}
@ -207,6 +211,7 @@ export default function ChangePasswordPop(props) {
className="btn-origin grey"
onClick={() => {
logout()
router.replace('/login', undefined, { shallow: true })
}}
>
{getMessage('main.popup.login.btn2')}

View File

@ -54,6 +54,8 @@ export default function StuffDetail() {
const globalLocaleState = useRecoilValue(globalLocaleStore)
const ref = useRef()
const { get, promiseGet, del, promisePost, promisePut } = useAxios(globalLocaleState)
const [createSaleStoreId, setCreateSaleStoreId] = useState('')
//form
const formInitValue = {
// T...() S...()
@ -350,6 +352,9 @@ export default function StuffDetail() {
promiseGet({ url: `/api/object/${objectNo}/detail` }).then((res) => {
setIsGlobalLoading(false)
if (res.status === 200) {
setCreateSaleStoreId(res?.data?.createSaleStoreId);
if (res?.data?.createSaleStoreId === 'T01') {
if (session?.storeId !== 'T01') {
setShowButton('none')
@ -2936,7 +2941,7 @@ export default function StuffDetail() {
<WindSelectPop setShowWindSpeedButtonValid={setShowWindSpeedButtonValid} prefName={form.watch('prefName')} windSpeedInfo={setWindSppedInfo} />
)}
{estimatePopupOpen && <DocDownOptionPop planNo={popPlanNo} setEstimatePopupOpen={setEstimatePopupOpen} saleStoreId={form.getValues('saleStoreId')}/>}
{estimatePopupOpen && <DocDownOptionPop planNo={popPlanNo} setEstimatePopupOpen={setEstimatePopupOpen} createStoreId={createSaleStoreId}/>}
</>
)
}

View File

@ -3,8 +3,11 @@ import { useMessage } from '@/hooks/useMessage'
import { useSwal } from '@/hooks/useSwal'
import { sessionStore } from '@/store/commonAtom'
import { getQueryString } from '@/util/common-utils'
import { useRecoilValue } from 'recoil'
import { atom, useRecoilState, useRecoilValue } from 'recoil'
// API 요청을 저장할 모듈 레벨 변수 (Hook 외부)
let roofMaterialPromise = null;
/**
* 마스터 컨트롤러
* @returns
@ -20,10 +23,23 @@ export function useMasterController() {
* @returns
*/
const getRoofMaterialList = async () => {
return await get({ url: '/api/v1/master/getRoofMaterialList' }).then((res) => {
// console.log('🚀🚀 ~ getRoofMaterialList ~ res:', res)
return res
// 1. 이미 진행 중이거나 완료된 Promise가 있으면 그것을 반환
if (roofMaterialPromise) {
return roofMaterialPromise;
}
// 2. 처음 호출될 때 Promise를 생성하여 변수에 할당
roofMaterialPromise = get({ url: '/api/v1/master/getRoofMaterialList' })
.then((res) => {
return res;
})
.catch((error) => {
// 에러 발생 시 다음 호출을 위해 초기화
roofMaterialPromise = null;
throw error;
});
return roofMaterialPromise;
}
/**

View File

@ -3094,7 +3094,7 @@ export const useTrestle = () => {
)
}
if (!halfBottomRightModule) {
halfBottomRightPoint = { x: x + w, y: y - height / 2 }
halfBottomRightPoint = { x: x + w, y: y + height / 2 }
halfBottomRightModule = centerPoints.find(
(centerPoint) => Math.abs(centerPoint.x - halfBottomRightPoint.x) < maxX && Math.abs(centerPoint.y - halfBottomRightPoint.y) < maxY,
)

View File

@ -10,7 +10,7 @@ import {
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
import { basicSettingState } from '@/store/settingAtom'
import { calcLineActualSizeByLineLength } from '@/util/qpolygon-utils'
import { calcLineActualSizeByLineLength, calcLinePlaneSize } from '@/util/qpolygon-utils'
import { getDegreeByChon } from '@/util/canvas-util'
import { useText } from '@/hooks/useText'
import { fontSelector } from '@/store/fontAtom'
@ -31,6 +31,7 @@ export const useLine = () => {
const addLine = (points = [], options) => {
const line = new QLine(points, {
...options,
attributes: {},
fontSize: lengthText.fontSize.value,
fontFamily: lengthText.fontFamily.value,
})
@ -38,7 +39,7 @@ export const useLine = () => {
if (line.length < 1) {
return null
}
line.attributes.planeSize = calcLinePlaneSize(line)
canvas?.add(line)
return line
}
@ -172,9 +173,9 @@ export const useLine = () => {
const isHorizontal = y1 === y2
const isVertical = x1 === x2
const isDiagonal = !isHorizontal && !isVertical
const lineLength = line.getLength()
const lineLength = line.attributes.planeSize ?? line.getLength()
line.attributes = { ...line.attributes, planeSize: line.getLength(), actualSize: line.getLength() }
line.attributes = { ...line.attributes, planeSize: lineLength, actualSize: lineLength }
if (+roofSizeSet === 1) {
if (direction === 'south' || direction === 'north') {

View File

@ -1,7 +1,7 @@
import { ANGLE_TYPE, canvasState, currentAngleTypeSelector, globalPitchState, pitchTextSelector } from '@/store/canvasAtom'
import { useRecoilValue } from 'recoil'
import { fabric } from 'fabric'
import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, getDegreeInOrientation, isPointOnLine } from '@/util/canvas-util'
import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, isPointOnLine } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon'
import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils'
import { basicSettingState, flowDisplaySelector } from '@/store/settingAtom'
@ -344,6 +344,7 @@ export const usePolygon = () => {
}
//arrow의 compass 값으로 방향 글자 설정 필요
// moduleCompass 각도와 direction(지붕면 방향)에 따라 한자 방위 텍스트 매핑
const drawDirectionStringToArrow2 = (polygon, showDirectionText) => {
let { direction, surfaceCompass, moduleCompass, arrow } = polygon
if (moduleCompass === null || moduleCompass === undefined) {
@ -371,121 +372,81 @@ export const usePolygon = () => {
let text = ''
let compassType = (375 + getDegreeInOrientation(moduleCompass)) / 15
// moduleCompass 각도와 direction에 따른 한자 방위 매핑
// direction: south(↓), west(←), north(↑), east(→)
// 각도 범위별 매핑 테이블 (사진 기준)
const getDirectionText = (angle, dir) => {
// 각도를 정규화 (-180 ~ 180 범위로)
let normalizedAngle = Number(angle)
while (normalizedAngle > 180) normalizedAngle -= 360
while (normalizedAngle < -180) normalizedAngle += 360
moduleCompass = -1 * moduleCompass
// 매핑 테이블: { south(↓), west(←), north(↑), east(→) }
// 각도 0: 남, 서, 북, 동
// 각도 45: 남서, 북서, 북동, 남동
// 각도 90: 서, 북, 동, 남
// 각도 135: 북서, 북동, 남동, 남서
// 각도 180: 북, 동, 남, 서
// 각도 -45: 남동, 남서, 북서, 북동
// 각도 -90: 동, 남, 서, 북
// 각도 -135: 북동, 남동, 남서, 북서
if (moduleCompass === 0 || (moduleCompass < 0 && moduleCompass >= -6)) {
compassType = 1
} else if (moduleCompass < 0 && moduleCompass >= -21) {
compassType = 2
} else if (moduleCompass < 0 && moduleCompass >= -36) {
compassType = 3
} else if (moduleCompass < 0 && moduleCompass >= -51) {
compassType = 4
} else if (moduleCompass < 0 && moduleCompass >= -66) {
compassType = 5
} else if (moduleCompass < 0 && moduleCompass >= -81) {
compassType = 6
} else if (moduleCompass < 0 && moduleCompass >= -96) {
compassType = 7
} else if (moduleCompass < 0 && moduleCompass >= -111) {
compassType = 8
} else if (moduleCompass < 0 && moduleCompass >= -126) {
compassType = 9
} else if (moduleCompass < 0 && moduleCompass >= -141) {
compassType = 10
} else if (moduleCompass < 0 && moduleCompass >= -156) {
compassType = 11
} else if (moduleCompass < 0 && moduleCompass >= -171) {
compassType = 12
} else if (Math.abs(moduleCompass) === 180) {
compassType = 13
let mapping
// 정확한 각도 먼저 체크
if (normalizedAngle === 0) {
mapping = { south: '南', west: '西', north: '北', east: '東' }
} else if (normalizedAngle === 45) {
mapping = { south: '南西', west: '北西', north: '北東', east: '南東' }
} else if (normalizedAngle === 90) {
mapping = { south: '西', west: '北', north: '東', east: '南' }
} else if (normalizedAngle === 135) {
mapping = { south: '北西', west: '北東', north: '南東', east: '南西' }
} else if (normalizedAngle === 180 || normalizedAngle === -180) {
mapping = { south: '北', west: '東', north: '南', east: '西' }
} else if (normalizedAngle === -45) {
mapping = { south: '南東', west: '南西', north: '北西', east: '北東' }
} else if (normalizedAngle === -90) {
mapping = { south: '東', west: '南', north: '西', east: '北' }
} else if (normalizedAngle === -135) {
mapping = { south: '北東', west: '南東', north: '南西', east: '北西' }
}
// 범위 각도 체크
else if (normalizedAngle >= 1 && normalizedAngle <= 44) {
// 1~44: 남남서, 서북서, 북북동, 동남동
mapping = { south: '南南西', west: '西北西', north: '北北東', east: '東南東' }
} else if (normalizedAngle >= 46 && normalizedAngle <= 89) {
// 46~89: 서남서, 북북서, 동북동, 남남동
mapping = { south: '西南西', west: '北北西', north: '東北東', east: '南南東' }
} else if (normalizedAngle >= 91 && normalizedAngle <= 134) {
// 91~134: 서북서, 북북동, 동남동, 남남서
mapping = { south: '西北西', west: '北北東', north: '東南東', east: '南南西' }
} else if (normalizedAngle >= 136 && normalizedAngle <= 179) {
// 136~179: 북북서, 동북동, 남남동, 서남서
mapping = { south: '北北西', west: '東北東', north: '南南東', east: '西南西' }
} else if (normalizedAngle >= -44 && normalizedAngle <= -1) {
// -1~-44: 남남동, 서남서, 북북서, 동북동
mapping = { south: '南南東', west: '西南西', north: '北北西', east: '東北東' }
} else if (normalizedAngle >= -89 && normalizedAngle <= -46) {
// -46~-89: 동남동, 남남서, 서북서, 북북동
mapping = { south: '東南東', west: '南南西', north: '西北西', east: '北北東' }
} else if (normalizedAngle >= -134 && normalizedAngle <= -91) {
// -91~-134: 동북동, 남남동, 서남서, 북북서
mapping = { south: '東北東', west: '南南東', north: '西南西', east: '北北西' }
} else if (normalizedAngle >= -179 && normalizedAngle <= -136) {
// -136~-179: 북북동, 동남동, 남남서, 서북서
mapping = { south: '北北東', west: '東南東', north: '南南西', east: '西北西' }
} else {
// 기본값: 0도
mapping = { south: '南', west: '西', north: '北', east: '東' }
}
if ([1, 25].includes(compassType)) {
direction === 'north' ? (text = '北') : direction === 'south' ? (text = '南') : direction === 'west' ? (text = '西') : (text = '東')
} else if ([2, 3].includes(compassType)) {
direction === 'north'
? (text = '北北東')
: direction === 'south'
? (text = '南南西')
: direction === 'west'
? (text = '西北西')
: (text = '東南東')
} else if ([4].includes(compassType)) {
direction === 'north' ? (text = '北東') : direction === 'south' ? (text = '南西') : direction === 'west' ? (text = '北西') : (text = '南東')
} else if ([5, 6].includes(compassType)) {
direction === 'north'
? (text = '東北東')
: direction === 'south'
? (text = '西南西')
: direction === 'west'
? (text = '北北西')
: (text = '南南東')
} else if ([7].includes(compassType)) {
direction === 'north' ? (text = '東') : direction === 'south' ? (text = '西') : direction === 'west' ? (text = '北') : (text = '南')
} else if ([8, 9].includes(compassType)) {
direction === 'north'
? (text = '東南東')
: direction === 'south'
? (text = '西北西')
: direction === 'west'
? (text = '北北東')
: (text = '南南西')
} else if ([10].includes(compassType)) {
direction === 'north' ? (text = '南東') : direction === 'south' ? (text = '北西') : direction === 'west' ? (text = '北東') : (text = '南西')
} else if ([11, 12].includes(compassType)) {
direction === 'north'
? (text = '南南東')
: direction === 'south'
? (text = '北北西')
: direction === 'west'
? (text = '東北東')
: (text = '西南西')
} else if ([13].includes(compassType)) {
direction === 'north' ? (text = '南') : direction === 'south' ? (text = '北') : direction === 'west' ? (text = '東') : (text = '西')
} else if ([14, 15].includes(compassType)) {
direction === 'north'
? (text = '南南西')
: direction === 'south'
? (text = '北北東')
: direction === 'west'
? (text = '東南東')
: (text = '西北西')
} else if ([16].includes(compassType)) {
direction === 'north' ? (text = '南西') : direction === 'south' ? (text = '北東') : direction === 'west' ? (text = '南東') : (text = '北西')
} else if ([17, 18].includes(compassType)) {
direction === 'north'
? (text = '西南西')
: direction === 'south'
? (text = '東北東')
: direction === 'west'
? (text = '南南東')
: (text = '北北西')
} else if ([19].includes(compassType)) {
direction === 'north' ? (text = '西') : direction === 'south' ? (text = '東') : direction === 'west' ? (text = '南') : (text = '北')
} else if ([20, 21].includes(compassType)) {
direction === 'north'
? (text = '西北西')
: direction === 'south'
? (text = '東南東')
: direction === 'west'
? (text = '南南西')
: (text = '北北東')
} else if ([22].includes(compassType)) {
direction === 'north' ? (text = '北西') : direction === 'south' ? (text = '南東') : direction === 'west' ? (text = '南西') : (text = '北東')
} else if ([23, 24].includes(compassType)) {
direction === 'north'
? (text = '北北西')
: direction === 'south'
? (text = '南南東')
: direction === 'west'
? (text = '西南西')
: (text = '東北東')
return mapping[dir] || '南'
}
// 東,西,南,北
text = getDirectionText(moduleCompass, direction)
// surfaceCompass가 있으면 text를 덮어쓰기 (기존 로직 유지)
if (surfaceCompass !== null && surfaceCompass !== undefined) {
if ([0].includes(surfaceCompass)) {
text = '南'
} else if ([15, 30].includes(surfaceCompass)) {
@ -515,9 +476,10 @@ export const usePolygon = () => {
} else if ([-75, -60].includes(surfaceCompass)) {
text = '西南西'
} else if ([-45].includes(surfaceCompass)) {
text = '西南'
text = '西'
} else if ([-30, -15].includes(surfaceCompass)) {
text = '西西南'
text = '南南西'
}
}
const sameDirectionCnt = canvas.getObjects().filter((obj) => {

View File

@ -683,9 +683,9 @@
"join.sub1.addr": "住所",
"join.sub1.addr_placeholder": "全角50文字以内",
"join.sub1.telNo": "電話番号",
"join.sub1.telNo_placeholder": "00 0000 0000",
"join.sub1.telNo_placeholder": "00-0000-0000",
"join.sub1.fax": "FAX番号",
"join.sub1.fax_placeholder": "00 0000 0000",
"join.sub1.fax_placeholder": "00-0000-0000",
"join.sub1.bizNo": "法人番号",
"join.sub2.title": "担当者情報",
"join.sub2.userNm": "担当者名",

View File

@ -683,7 +683,7 @@
"join.sub1.addr": "주소",
"join.sub1.addr_placeholder": "전각50자이내",
"join.sub1.telNo": "전화번호",
"join.sub1.telNo_placeholder": "00 0000 0000",
"join.sub1.telNo_placeholder": "000-0000-0000",
"join.sub1.fax": "FAX 번호",
"join.sub1.fax_placeholder": "00 0000 0000",
"join.sub1.bizNo": "법인번호",

View File

@ -5992,6 +5992,19 @@ export const calcLineActualSize = (points, degree = 0) => {
return Big(planeSize).div(theta).round().toNumber()
}
export const calcLineActualSize2 = (points, degree = 0) => {
const planeSize = calcLinePlaneSize(points)
const xLength = Math.abs(points.x2 - points.x1) * 10
const yLength = Math.abs(points.y2 - points.y1) * 10
const hx = xLength * Math.tan(degree * (Math.PI / 180))
const hy = yLength * Math.tan(degree * (Math.PI / 180))
const actualSize = Math.sqrt(hx ** 2 + planeSize ** 2)
const actualSize2 = Math.sqrt(hy ** 2 + planeSize ** 2)
return actualSize
}
/**
* 포인트와 기울기를 기준으로 선의 길이를 구한다.
* @param lineLength

View File

@ -1,6 +1,6 @@
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import { SkeletonBuilder } from '@/lib/skeletons'
import { calcLineActualSize, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
import { calcLineActualSize, calcLineActualSize2, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils'
import { QLine } from '@/components/fabric/QLine'
import { getDegreeByChon } from '@/util/canvas-util'
import Big from 'big.js'
@ -802,6 +802,10 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
const getAddLine = (p1, p2, stroke = '') => {
movedLines.push({ index, p1, p2 })
const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y);
const isDiagonal = dx > 0.5 && dy > 0.5; // x, y 변화가 모두 있으면 대각선
//console.log("mergeLines:::::::", mergeLines);
const line = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id,
@ -818,6 +822,15 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
type: 'eaveHelpLine',
isStart: true,
pitch: wallLine.attributes.pitch,
actualSize: (isDiagonal) ? calcLineActualSize(
{
x1: p1.x,
y1: p1.y,
x2: p2.x,
y2: p2.y
},
getDegreeByChon(wallLine.attributes.pitch)
) : calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }),
},
})
@ -1099,7 +1112,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green')
getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink')
}
getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange')
getAddLine(newPStart, newPEnd, 'red')
}
} else if (condition === 'right_out') {
@ -1439,7 +1452,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
}
getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange')
//getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange')
getAddLine(newPStart, newPEnd, 'red')
}
@ -1468,7 +1481,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green')
getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink')
}
getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange')
//getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange')
getAddLine(newPStart, newPEnd, 'red')
}
} else if (condition === 'bottom_out') {
@ -1792,7 +1805,7 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
function isOuterEdge(p1, p2, edges) {
const tolerance = 0.1;
return edges.some(edge => {
const lineStart = { x: edge.Begin.X, y: edge.Begin.Y };
const lineStart = { x: edge.Begin.X, y: edge.Begin.Y }
const lineEnd = { x: edge.End.X, y: edge.End.Y };
const forwardMatch = Math.abs(lineStart.x - p1.x) < tolerance && Math.abs(lineStart.y - p1.y) < tolerance && Math.abs(lineEnd.x - p2.x) < tolerance && Math.abs(lineEnd.y - p2.y) < tolerance;
const backwardMatch = Math.abs(lineStart.x - p2.x) < tolerance && Math.abs(lineStart.y - p2.y) < tolerance && Math.abs(lineEnd.x - p1.x) < tolerance && Math.abs(lineEnd.y - p1.y) < tolerance;
@ -1835,7 +1848,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is
p2,
attributes: {
roofId: id,
actualSize: (isDiagonal) ? calcLineActualSize(
actualSize: (isDiagonal) ? calcLineActualSize2(
{
x1: p1.x,
y1: p1.y,
@ -1856,7 +1869,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is
};
skeletonLines.push(newLine);
//console.log('skeletonLines', skeletonLines);
console.log('skeletonLines', skeletonLines);
}
/**