Merge branch 'dev' into feature/test-jy

# Conflicts:
#	src/hooks/roofcover/usePropertiesSetting.js
#	src/hooks/roofcover/useRoofAllocationSetting.js
This commit is contained in:
Jaeyoung Lee 2024-11-05 14:47:04 +09:00
commit 57ee521410
44 changed files with 4344 additions and 3427 deletions

View File

@ -0,0 +1,3 @@
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L5.5 5.5L10 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -0,0 +1,3 @@
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector 7174" d="M1 1L5.5 5.5L10 1" stroke="#C2D0DD" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -1,128 +0,0 @@
'use client'
import { Select, SelectItem } from '@nextui-org/react'
import { useEffect, useState } from 'react'
import { useAxios } from '@/hooks/useAxios'
export default function RoofSelect() {
const [roofMaterials, setRoofMaterials] = useState([])
const [manufacturers, setManufacturers] = useState([])
const [trestles, setTrestles] = useState([])
const [modules, setModules] = useState([])
const [originTrestles, setOriginTrestles] = useState([])
const [roofMaterialId, setRoofMaterialId] = useState(null)
const [manufacturerId, setManufacturerId] = useState(null)
const [trestleId, setTrestleId] = useState(null)
const { get } = useAxios()
useEffect(() => {
get({ url: '/api/roof-material/roof-material-infos' }).then((res) => {
//TODO: error handling
if (!res) return
setRoofMaterials(res)
})
}, [])
useEffect(() => {
if (!roofMaterialId) {
return
}
get({ url: `/api/roof-material/roof-material-infos/${roofMaterialId}/trestles` }).then((res) => {
if (res.length === 0) {
return
}
setOriginTrestles(res)
const manufactural = res.map((trestle) => {
return { id: trestle.manufacturerId, name: trestle.manufacturerName }
})
// Remove duplicates
const uniqueManufactural = Array.from(new Set(manufactural.map((a) => a.id))).map((id) => {
return manufactural.find((a) => a.id === id)
})
setManufacturers(uniqueManufactural)
})
}, [roofMaterialId])
useEffect(() => {
if (!manufacturerId) {
return
}
const trestles = originTrestles.filter((trestle) => trestle.manufacturerId === manufacturerId)
setTrestles(trestles)
}, [manufacturerId])
useEffect(() => {
if (!trestleId) {
return
}
get({ url: `/api/module/module-infos?roofMaterialId=${roofMaterialId}&trestleId=${trestleId}` }).then((res) => {
if (res.length === 0) {
return
}
setModules(res)
})
}, [trestleId])
const handleRoofMaterialOnChange = (e) => {
const roofMaterialId = e.target.value
setRoofMaterialId(roofMaterialId)
setManufacturers([])
setManufacturerId(null)
setTrestleId(null)
setTrestles([])
setModules([])
}
const handleManufacturersOnChange = (e) => {
const manufacturerId = Number(e.target.value)
setTrestles([])
setManufacturerId(manufacturerId)
setTrestleId(null)
setModules([])
}
const handleTrestlesOnChange = (e) => {
const trestleId = Number(e.target.value)
setTrestleId(trestleId)
setModules([])
}
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
{roofMaterials.length > 0 && (
<Select label="지붕재" className="max-w-xs" onChange={handleRoofMaterialOnChange}>
{roofMaterials.map((roofMaterial) => (
<SelectItem key={roofMaterial.id}>{roofMaterial.name}</SelectItem>
))}
</Select>
)}
{manufacturers.length > 0 && (
<Select label="제조 회사" className="max-w-xs" onChange={handleManufacturersOnChange}>
{manufacturers.map((manufacturer) => (
<SelectItem key={manufacturer.id}>{manufacturer.name}</SelectItem>
))}
</Select>
)}
{trestles.length > 0 && (
<Select label="가대" className="max-w-xs" onChange={handleTrestlesOnChange}>
{trestles.map((trestle) => (
<SelectItem key={trestle.id}>{trestle.name}</SelectItem>
))}
</Select>
)}
{modules.length > 0 && (
<Select label="설치가능 모듈" className="max-w-xs">
{modules.map((module) => (
<SelectItem key={module.id}>{module.name}</SelectItem>
))}
</Select>
)}
</div>
)
}

View File

@ -1,14 +1,8 @@
import Roof2 from '@/components/Roof2'
import RoofSelect from '@/app/roof2/RoofSelect'
export default async function Roof2Page() {
return (
<>
<div>
<div className="m-2">
<RoofSelect />
</div>
</div>
<div className="flex flex-col justify-center my-8 pt-20">
<Roof2 />
</div>

View File

@ -29,13 +29,6 @@ export default function InitSettingsModal(props) {
//const { get, post } = useAxios()
useEffect(() => {
get({ url: '/api/roof-material/roof-material-infos' }).then((res) => {
//TODO: error handling
if (!res) return
setRoofMaterials(res)
})
get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${objectNo}` }).then((res) => {
if (res.length == 0) return

View File

@ -8,7 +8,7 @@ import { contextPopupPositionState } from '@/store/popupAtom'
export default function ColorPickerModal(props) {
const contextPopupPosition = useRecoilValue(contextPopupPositionState) //
const { isShow, setIsShow, pos = contextPopupPosition, color = '#ff0000', setColor, id } = props
const { isShow, setIsShow, pos = contextPopupPosition, color = '#ff0000', setColor, id, isConfig = false } = props
const { getMessage } = useMessage()
const [originColor, setOriginColor] = useState(color)
const { closePopup } = usePopup()
@ -29,7 +29,7 @@ export default function ColorPickerModal(props) {
setIsShow(false)
}
console.log(id)
closePopup(id)
closePopup(id, isConfig)
}}
>
닫기
@ -49,7 +49,7 @@ export default function ColorPickerModal(props) {
if (setColor) setColor(originColor)
if (setIsShow) setIsShow(false)
closePopup(id)
closePopup(id, isConfig)
}}
>
{getMessage('common.message.save')}

View File

@ -3,12 +3,14 @@ import { useEffect } from 'react'
import '@/styles/contents.scss'
import { useRecoilState } from 'recoil'
import { contextMenuListState, contextMenuState } from '@/store/contextMenu'
import { useTempGrid } from '@/hooks/useTempGrid'
export default function QContextMenu(props) {
const { contextRef, canvasProps, handleKeyup } = props
const [contextMenu, setContextMenu] = useRecoilState(contextMenuState)
const [contextMenuList, setContextMenuList] = useRecoilState(contextMenuListState)
const activeObject = canvasProps?.getActiveObject() //
const { tempGridMode, setTempGridMode } = useTempGrid()
let contextType = ''
@ -32,6 +34,7 @@ export default function QContextMenu(props) {
const handleContextMenu = (e) => {
// e.preventDefault() // contextmenu
if (tempGridMode) return
const position = {
x: window.innerWidth / 2 < e.pageX ? e.pageX - 240 : e.pageX,
y: window.innerHeight / 2 < e.pageY ? getYPosition(e) : e.pageY,

View File

@ -14,15 +14,7 @@ const fonts = [
{ name: '@Yu Gothic UI', value: '@Yu Gothic UI' },
{ name: 'Yu Gothic UI', value: 'Yu Gothic UI' },
]
const fontOptions = [
{ name: '보통', value: 'normal' },
{ name: '기울임꼴', value: 'italic' },
{
name: '굵게',
value: 'bold',
},
{ name: '굵은 기울임꼴', value: 'boldAndItalic' },
]
const fontSizes = [
...Array.from({ length: 4 }).map((_, index) => {
return { name: index + 8, value: index + 8 }
@ -34,20 +26,10 @@ const fontSizes = [
{ name: 48, value: 48 },
{ name: 72, value: 72 },
]
const fontColors = [
{ name: '검정색', value: 'black' },
{ name: '빨강색', value: 'red' },
{ name: '파랑색', value: 'blue' },
{ name: '회색', value: 'gray' },
{ name: '황색', value: 'yellow' },
{ name: '녹색', value: 'green' },
{ name: '분홍색', value: 'pink' },
{ name: '황금색', value: 'gold' },
{ name: '남색', value: 'darkblue' },
]
export default function FontSetting(props) {
const contextPopupPosition = useRecoilValue(contextPopupPositionState)
const { id, setIsShow, pos = contextPopupPosition, type } = props
const { id, setIsShow, pos = contextPopupPosition, type, isConfig = false } = props
const { getMessage } = useMessage()
const { closePopup } = usePopup()
const [globalFont, setGlobalFont] = useRecoilState(globalFontAtom)
@ -57,7 +39,26 @@ export default function FontSetting(props) {
const [selectedFontWeight, setSelectedFontWeight] = useState(currentFont.fontWeight)
const [selectedFontSize, setSelectedFontSize] = useState(currentFont.fontSize)
const [selectedFontColor, setSelectedFontColor] = useState(currentFont.fontColor)
const fontOptions = [
{ name: getMessage('font.style.normal'), value: 'normal' },
{ name: getMessage('font.style.italic'), value: 'italic' },
{
name: getMessage('font.style.bold'),
value: 'bold',
},
{ name: getMessage('font.style.bold.italic'), value: 'boldAndItalic' },
]
const fontColors = [
{ name: getMessage('color.black'), value: 'black' },
{ name: getMessage('color.red'), value: 'red' },
{ name: getMessage('color.blue'), value: 'blue' },
{ name: getMessage('color.gray'), value: 'gray' },
{ name: getMessage('color.yellow'), value: 'yellow' },
{ name: getMessage('color.green'), value: 'green' },
{ name: getMessage('color.pink'), value: 'pink' },
{ name: getMessage('color.gold'), value: 'gold' },
{ name: getMessage('color.darkblue'), value: 'darkblue' },
]
const handleSaveBtn = () => {
setGlobalFont((prev) => {
return {
@ -83,7 +84,7 @@ export default function FontSetting(props) {
className="modal-close"
onClick={() => {
if (setIsShow) setIsShow(false)
closePopup(id)
closePopup(id, isConfig)
}}
>
닫기

View File

@ -5,5 +5,9 @@ import { Fragment } from 'react'
export default function PopupManager() {
const [popup, setPopup] = useRecoilState(popupState)
return popup.children?.map((child) => <Fragment key={child.id}>{child.component}</Fragment>)
return [
...popup?.config.map((child) => <Fragment key={child.id}>{child.component}</Fragment>),
...popup?.other.map((child) => <Fragment key={child.id}>{child.component}</Fragment>),
]
}

View File

@ -8,13 +8,41 @@ import { useMessage } from '@/hooks/useMessage'
import { useCanvasMenu } from '@/hooks/common/useCanvasMenu'
import SingleDatePicker from '../common/datepicker/SingleDatePicker'
import EstimateFileUploader from './EstimateFileUploader'
import { useAxios } from '@/hooks/useAxios'
import { globalLocaleStore } from '@/store/localeAtom'
import { isObjectNotEmpty } from '@/util/common-utils'
import dayjs from 'dayjs'
import { useCommonCode } from '@/hooks/common/useCommonCode'
import Select from 'react-select'
import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController'
export default function Estimate({ params }) {
const [objectNo, setObjectNo] = useState('')
const [files, setFiles] = useState([]) //
const [objectNo, setObjectNo] = useState('') //
const [planNo, setPlanNo] = useState('') //
const [files, setFiles] = useState([]) //
//
const [hidden, setHidden] = useState(false)
//
const { findCommonCode } = useCommonCode()
const [honorificCodeList, setHonorificCodeList] = useState([]) //
const [startDate, setStartDate] = useState(new Date())
const singleDatePickerProps = {
startDate,
setStartDate,
}
const sessionState = useRecoilValue(sessionStore)
const objectRecoil = useRecoilValue(floorPlanObjectState)
//
const { state, setState } = useEstimateController(params.pid)
const globalLocaleState = useRecoilValue(globalLocaleStore)
const { get, post } = useAxios(globalLocaleState)
const { getMessage } = useMessage()
const { setMenuNumber } = useCanvasMenu()
@ -27,46 +55,54 @@ export default function Estimate({ params }) {
setUploadFiles: setFiles,
}
useEffect(() => {
setObjectNo(objectRecoil.floorPlanObjectNo)
}, [objectRecoil])
useEffect(() => {
if (objectNo) {
//Q101X278191023001
console.log('세션정보::::', sessionState)
//API
}
}, [objectNo])
useEffect(() => {
setMenuNumber(5)
setObjectNo(objectRecoil.floorPlanObjectNo)
setPlanNo(params.pid)
//
const code1 = findCommonCode(200800)
if (code1 != null) {
setHonorificCodeList(code1)
}
// API
//http://localhost:8080/api/estimate/special-note-list
}, [])
// set
useEffect(() => {
let estimateDatej = dayjs(startDate).format('YYYY-MM-DD')
setState({ estimateDate: estimateDatej })
}, [startDate])
return (
<div className="sub-content estimate">
<div className="sub-content-inner">
{/* 물건번호, 견적서번호, 등록일, 변경일시 시작 */}
{/* <form onSubmit={handleSubmit(onValid)}> */}
<div className="sub-content-box">
<div className="sub-table-box">
<div className="estimate-list-wrap one">
<div className="estimate-box">
<div className="estimate-tit">{getMessage('estimate.detail.objectNo')}</div>
<div className="estimate-name">
{objectNo} (Plan No: {params.pid})
{objectNo} (Plan No: {planNo})
</div>
</div>
<div className="estimate-box">
<div className="estimate-tit">{getMessage('estimate.detail.estimateNo')}</div>
<div className="estimate-name">5242310200065242</div>
<div className="estimate-tit">{getMessage('estimate.detail.docNo')}</div>
<div className="estimate-name">{state.docNo}</div>
</div>
<div className="estimate-box">
<div className="estimate-tit">{getMessage('estimate.detail.createDatetime')}</div>
<div className="estimate-name">9999.09.28</div>
<div className="estimate-tit">{getMessage('estimate.detail.drawingEstimateCreateDate')}</div>
<div className="estimate-name">
{state?.drawingEstimateCreateDate ? `${dayjs(state.drawingEstimateCreateDate).format('YYYY.MM.DD')}` : ''}
</div>
</div>
<div className="estimate-box">
<div className="estimate-tit">{getMessage('estimate.detail.lastEditDatetime')}</div>
<div className="estimate-name">9999.09.28 06:36</div>
<div className="estimate-name">{state?.lastEditDatetime ? `${dayjs(state.lastEditDatetime).format('YYYY.MM.DD HH:mm')}` : ''}</div>
</div>
</div>
</div>
@ -99,7 +135,7 @@ export default function Estimate({ params }) {
</th>
<td>
<div className="date-picker" style={{ width: '350px' }}>
<SingleDatePicker />
<SingleDatePicker {...singleDatePickerProps} />
</div>
</td>
</tr>
@ -113,64 +149,152 @@ export default function Estimate({ params }) {
</th>
<td>
<div className="input-wrap" style={{ width: '350px' }}>
<input type="text" className="input-light" defaultValue={'물건정보에서 입력한 담당자명 표시'} />
<input
type="text"
className="input-light"
defaultValue={state?.charger}
onChange={(e) => {
// charger
// console.log(':::::', e.target.value)
setState({ charger: e.target.value })
}}
/>
</div>
</td>
</tr>
<tr>
{/* 안건명 */}
<th>
{getMessage('estimate.detail.title')} <span className="important">*</span>
{getMessage('estimate.detail.objectName')} <span className="important">*</span>
</th>
<td colSpan={3}>
<div className="form-flex-wrap">
<div className="input-wrap mr5" style={{ width: '610px' }}>
<input type="text" className="input-light" defaultValue={'안건명:::'} />
<input
type="text"
className="input-light"
defaultValue={state?.objectName}
onChange={(e) => {
// objectName
// console.log('::::', e.target.value)
setState({ objectName: e.target.value })
}}
/>
</div>
<div className="input-wrap" style={{ width: '200px' }}>
<input type="text" className="input-light" defaultValue={'경칭?'} />
<div className="select-wrap" style={{ width: '200px' }}>
<Select
id="objectNameOmit"
instanceId="objectNameOmit"
className="react-select-custom"
classNamePrefix="custom"
placeholder="Select"
options={honorificCodeList}
onChange={(e) => {
if (isObjectNotEmpty(e)) {
setState({ objectNameOmit: e.clCodeNm })
} else {
// console.log('XXX')
setState({ objectNameOmit: '' })
}
}}
getOptionLabel={(x) => x.clCodeNm}
getOptionValue={(x) => x.clCode}
isClearable={true}
isSearchable={false}
value={honorificCodeList.filter(function (option) {
return option.clCodeNm === state.objectNameOmit
})}
/>
</div>
</div>
</td>
</tr>
<tr>
{/* 메모 */}
<th>{getMessage('estimate.detail.remarks')}</th>
<td colSpan={3}>물건정보에서 입력한 메모 표시</td>
{/* 물건정보에서 입력한 메모 */}
<th>{getMessage('estimate.detail.objectRemarks')}</th>
<td colSpan={3}>{state?.objectRemarks}</td>
</tr>
<tr>
{/* 주문분류 */}
<th>
{getMessage('estimate.detail.orderType')} <span className="important">*</span>
{getMessage('estimate.detail.estimateType')} <span className="important">*</span>
</th>
<td colSpan={3}>
<div className="radio-wrap"></div>
<div className="radio-wrap">
<div className="d-check-radio light mr10">
<input
type="radio"
name="estimateType"
id="YJSS"
value={'YJSS'}
checked={state?.estimateType === 'YJSS' ? true : false}
onChange={(e) => {
setState({ estimateType: e.target.value })
}}
/>
<label htmlFor="YJSS">住宅PKG</label>
</div>
<div className="d-check-radio light">
<input
type="radio"
name="estimateType"
id="YJOD"
value={'YJOD'}
checked={state?.estimateType === 'YJOD' ? true : false}
onChange={(e) => {
setState({ estimateType: e.target.value })
}}
/>
<label htmlFor="YJOD">積上げ( YJOD )</label>
</div>
</div>
</td>
</tr>
<tr>
{/* 지붕재・사양시공 최대4개*/}
<th>{getMessage('estimate.detail.roofCns')}</th>
<td colSpan={3}>
<div className="form-flex-wrap mb5">
<div className="input-wrap mr5" style={{ width: '610px' }}>
<input type="text" className="input-light" defaultValue={'ハゼ式折板(角ハゼ)'} readOnly />
</div>
<div className="input-wrap" style={{ width: '200px' }}>
<input type="text" className="input-light" defaultValue={'標準施工'} readOnly />
</div>
</div>
<div className="form-flex-wrap mb5"></div>
<div className="form-flex-wrap mb5"></div>
{/* 마지막div엔 mb5 제외 */}
<div className="form-flex-wrap"></div>
{state?.roofMaterialIdMulti?.split('、').map((row, index) => {
//
let roofList = row
let roofListLength = state?.roofMaterialIdMulti?.split('、').length
let style = 'mb5'
if (roofListLength == index + 1) {
style = ''
}
//
let constructSpecificationMulti = state?.constructSpecificationMulti?.split('、')
return (
<>
<div className={`form-flex-wrap ${style}`}>
<div className="input-wrap mr5" style={{ width: '610px' }}>
<input type="text" className="input-light" defaultValue={roofList} readOnly />
</div>
<div className="input-wrap" style={{ width: '200px' }}>
<input type="text" className="input-light" defaultValue={constructSpecificationMulti[index]} readOnly />
</div>
</div>
</>
)
})}
</td>
</tr>
<tr>
{/* 비고 */}
<th>{getMessage('estimate.detail.note')}</th>
<th>{getMessage('estimate.detail.remarks')}</th>
<td colSpan={3}>
<div className="input-wrap">
<input type="text" className="input-light" />
<input
type="text"
className="input-light"
defaultValue={state?.remarks}
onChange={(e) => {
//
// console.log(':::::', e.target.value)
setState({ remarks: e.target.value })
}}
/>
</div>
</td>
</tr>
@ -200,25 +324,6 @@ export default function Estimate({ params }) {
<th>{getMessage('estimate.detail.header.fileList1')}</th>
<td>
<EstimateFileUploader {...fileUploadProps} />
{/* <div className="drag-file-box">
<div className="btn-area">
<Button type="button" className="btn-origin grey" onClick={handleButtonClick}>
{getMessage('estimate.detail.fileList.btn')}
</Button>
<input
type="file"
id="fileUpload"
name="fileUpload"
ref={fileInputRef}
onChange={onChangeFiles}
style={{ display: 'none' }}
/>
</div>
<div className="drag-file-area">
<p>Drag file here</p>
<ul className="file-list"></ul>
</div>
</div> */}
</td>
</tr>
</tbody>
@ -246,21 +351,40 @@ export default function Estimate({ params }) {
<h3 className="product">{getMessage('estimate.detail.header.specialEstimate')}</h3>
<div className="product_tit">{getMessage('estimate.detail.header.specialEstimateProductInfo')}</div>
</div>
<div className="left-unit-box">
<button className={`estimate-arr-btn down mr5 ${hidden ? '' : 'on'}`} onClick={() => setHidden(false)}></button>
<button className={`estimate-arr-btn up ${hidden ? 'on' : ''}`} onClick={() => setHidden(true)}></button>
</div>
</div>
{/* 공통코드영역시작 */}
<div className="special-note-check-wrap"></div>
{/* 공통코드영역끝 */}
{/* 견적특이사항 내용영역시작 */}
<div className="calculation-estimate"></div>
{/* 견적특이사항 내용영역끝 */}
{/* 견적특이사항 끝 */}
{/* 견적 특이사항 코드영역시작 */}
<div className={`estimate-check-wrap ${hidden ? 'hide' : ''}`}>
<div className="estimate-check-inner">
<div className="special-note-check-wrap"></div>
{/* 견적특이사항 선택한 내용?영역시작 */}
<div className="calculation-estimate">
<dl>
<dt>제목11??</dt>
<dd>제목1 비고</dd>
</dl>
<dl>
<dt>제목22??</dt>
<dd>제목2 비고</dd>
</dl>
</div>
{/* 견적특이사항 선택한 내용?영역끝 */}
</div>
</div>
{/* 견적 특이사항 코드영역 끝 */}
{/* 견적특이사항 영역끝 */}
{/* 제품정보 시작 */}
<div className="table-box-title-wrap">
<div className="title-wrap">
<h3>{getMessage('estimate.detail.header.specialEstimateProductInfo')}</h3>
</div>
</div>
<div className="estimate-wrap">
<div className="esimate-wrap">
<div className="estimate-list-wrap one">
<div className="estimate-box">
<div className="estimate-tit">{getMessage('estimate.detail.sepcialEstimateProductInfo.totPcs')}</div>
@ -285,7 +409,7 @@ export default function Estimate({ params }) {
</div>
</div>
{/* YJOD면 아래영역 숨김 */}
<div className="common-table bt-able">
<div className="common-table bt-able" style={{ display: state?.estimateType === 'YJSS' ? '' : 'none' }}>
<table>
<colgroup>
<col style={{ width: '160px' }} />
@ -319,18 +443,35 @@ export default function Estimate({ params }) {
<div className="estimate-product-option">
<div className="product-price-wrap">
<div className="product-price-tit">{getMessage('estimate.detail.header.showPrice')}</div>
<div className="select-wrap"></div>
<div className="select-wrap">
<select className="select-light" name="" id="">
<option value="">111</option>
<option value="">222</option>
</select>
</div>
<button className="btn-origin grey ml5">{getMessage('estimate.detail.showPrice.btn1')}</button>
</div>
<div className="product-edit-wrap">
<div className="product-edit-explane">
<div className="click-check">
<ul className="product-edit-explane">
<li className="explane-item item01">
<span className="ico"></span>
{getMessage('estimate.detail.showPrice.description')}
</div>
</div>
{getMessage('estimate.detail.showPrice.description1')}
</li>
<li className="explane-item item02">
<span className="ico"></span>
{getMessage('estimate.detail.showPrice.description2')}
</li>
<li className="explane-item item03">
<span className="ico"></span>
{getMessage('estimate.detail.showPrice.description3')}
</li>
<li className="explane-item item04">
<span className="ico"></span>
{getMessage('estimate.detail.showPrice.description4')}
</li>
</ul>
<div className="product-edit-btn">
<button className="btn-origin navy mr5">
<button className="btn-origin navy mr5" type="submit">
<span className="plus"></span>
{getMessage('estimate.detail.showPrice.btn2')}
</button>
@ -348,6 +489,7 @@ export default function Estimate({ params }) {
</div>
</div>
{/* 기본정보끝 */}
{/* </form> */}
</div>
</div>
)

View File

@ -32,6 +32,8 @@ import { menusState, menuTypeState } from '@/store/menuAtom'
import useMenu from '@/hooks/common/useMenu'
import { MENU } from '@/common/common'
import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateController'
export default function CanvasMenu(props) {
const { menuNumber, setMenuNumber } = props
const pathname = usePathname()
@ -49,8 +51,9 @@ export default function CanvasMenu(props) {
const sessionState = useRecoilValue(sessionStore)
const globalLocale = useRecoilValue(globalLocaleStore)
const canvas = useRecoilValue(canvasState)
const { handleZoomClear } = useCanvasEvent()
const { handleZoomClear, handleZoom } = useCanvasEvent()
const { handleMenu } = useMenu()
const { handleEstimateSubmit } = useEstimateController()
const { getMessage } = useMessage()
const { currentCanvasPlan, saveCanvas } = usePlan()
@ -129,7 +132,7 @@ export default function CanvasMenu(props) {
const handlePopup = () => {
const id = uuidv4()
addPopup(id, 0, <SettingModal01 id={id} />)
addPopup(id, 1, <SettingModal01 id={id} />, true)
}
useEffect(() => {
@ -201,14 +204,18 @@ export default function CanvasMenu(props) {
<button
className="control-btn minus"
onClick={() => {
canvas.setZoom(canvas.getZoom() - 0.1)
handleZoom(false)
}}
></button>
<span>{canvasZoom}%</span>
<button className="control-btn plus" onClick={handleZoomClear}></button>
<button
className="control-btn plus"
onClick={() => {
handleZoom(true)
}}
></button>
</div>
<div className="btn-from">
<button className="btn07" onClick={handleClear}></button>
<button className="btn08" onClick={handleSaveCanvas}></button>
<button className="btn09"></button>
</div>
@ -222,7 +229,7 @@ export default function CanvasMenu(props) {
<span className="ico ico01"></span>
<span>{getMessage('plan.menu.estimate.docDown')}</span>
</button>
<button className="btn-frame gray ico-flx">
<button className="btn-frame gray ico-flx" onClick={handleEstimateSubmit}>
<span className="ico ico02"></span>
<span>{getMessage('plan.menu.estimate.save')}</span>
</button>

View File

@ -50,7 +50,11 @@ export default function CircuitTrestleSetting({ id }) {
Next
</button>
)}
{tabNum === 3 && <button className="btn-frame modal act">保存 (仮割り当て)</button>}
{tabNum === 3 && (
<button className="btn-frame modal act">
{`${getMessage('modal.common.save')} (${getMessage('modal.circuit.trestle.setting.alloc.trestle')})`}
</button>
)}
</div>
</div>
</div>

View File

@ -114,7 +114,7 @@ export default function PowerConditionalSelect({ setTabNum }) {
</div>
</div>
<div className="circuit-right-wrap mb10">
<button className="btn-frame self mr5">追加</button>
<button className="btn-frame self mr5">{getMessage('modal.common.add')}</button>
</div>
<div className="circuit-data-form">
<span className="normal-font">
@ -132,12 +132,12 @@ export default function PowerConditionalSelect({ setTabNum }) {
<div className="slope-wrap">
<div className="d-check-box pop mb15">
<input type="checkbox" id="ch03" />
<label htmlFor="ch03">同一傾斜同一方面の面積の場合同じ面として回路を分ける</label>
<label htmlFor="ch03"> {getMessage('modal.circuit.trestle.setting.power.conditional.select.check1')}</label>
</div>
<div className="d-check-box pop">
<input type="checkbox" id="ch04" />
<label className="red" htmlFor="ch04">
MAX接続過積で回路を分ける
{getMessage('modal.circuit.trestle.setting.power.conditional.select.check2')}
</label>
</div>
</div>

View File

@ -18,8 +18,8 @@ export default function StepUp({}) {
<table>
<thead>
<tr>
<th>シリアル枚数</th>
<th>総回路数</th>
<th>{getMessage('modal.circuit.trestle.setting.step.up.allocation.serial.amount')}</th>
<th>{getMessage('modal.circuit.trestle.setting.step.up.allocation.total.amount')}</th>
</tr>
</thead>
<tbody>
@ -49,14 +49,14 @@ export default function StepUp({}) {
</div>
<div className="circuit-table-flx-wrap">
<div className="circuit-table-flx-box">
<div className="bold-font mb10">接続する</div>
<div className="bold-font mb10">{getMessage('modal.circuit.trestle.setting.step.up.allocation.connected')}</div>
<div className="roof-module-table mb10">
<table>
<thead>
<tr>
<th style={{ width: '140px' }}>名称</th>
<th>回路数</th>
<th>昇圧回路数</th>
<th style={{ width: '140px' }}>{getMessage('modal.circuit.trestle.setting.power.conditional.select.name')}</th>
<th>{getMessage('modal.circuit.trestle.setting.power.conditional.select.circuit.amount')}</th>
<th>{getMessage('modal.circuit.trestle.setting.step.up.allocation.circuit.amount')}</th>
</tr>
</thead>
<tbody>
@ -80,7 +80,7 @@ export default function StepUp({}) {
</div>
<div className="bottom-wrap">
<div className="circuit-right-wrap mb10">
<button className="btn-frame self mr5">追加</button>
<button className="btn-frame self mr5">{getMessage('modal.common.add')}</button>
</div>
<div className="circuit-data-form">
<span className="normal-font">
@ -90,13 +90,13 @@ export default function StepUp({}) {
</div>
</div>
<div className="circuit-table-flx-box">
<div className="bold-font mb10">昇圧オプション</div>
<div className="bold-font mb10">{getMessage('modal.circuit.trestle.setting.step.up.allocation.option')}</div>
<div className="roof-module-table mb10">
<table>
<thead>
<tr>
<th>名称</th>
<th>昇圧回路数</th>
<th>{getMessage('modal.circuit.trestle.setting.power.conditional.select.name')}</th>
<th>{getMessage('modal.circuit.trestle.setting.step.up.allocation.circuit.amount')}</th>
</tr>
</thead>
<tbody>
@ -113,7 +113,7 @@ export default function StepUp({}) {
</div>
<div className="bottom-wrap">
<div className="circuit-right-wrap mb10">
<button className="btn-frame self mr5">追加</button>
<button className="btn-frame self mr5">{getMessage('modal.common.add')}</button>
</div>
<div className="circuit-data-form">
<span className="normal-font">
@ -124,7 +124,7 @@ export default function StepUp({}) {
</div>
</div>
<div className="circuit-count-input">
<span className="normal-font">綿調道区分</span>
<span className="normal-font">{getMessage('modal.module.basic.setting.module.cotton.classification')}</span>
<div className="input-grid mr5" style={{ width: '40px' }}>
<input type="text" className="input-origin block" />
</div>
@ -395,7 +395,7 @@ export default function StepUp({}) {
<div className="slope-wrap">
<div className="outline-form">
<span className="mr10" style={{ width: 'auto' }}>
モニターの選択
{getMessage('modal.circuit.trestle.setting.step.up.allocation.select.monitor')}
</span>
<div className="grid-select mr10">
<QSelectBox title={'電力検出ユニット (モニター付き)'} option={SelectOption01} />

View File

@ -4,13 +4,13 @@ export default function PassivityCircuitAllocation() {
const { getMessage } = useMessage()
const moduleData = {
header: [
{ name: getMessage('屋根面'), prop: 'roofShape' },
{ name: getMessage('modal.panel.batch.statistic.roof.shape'), prop: 'roofShape' },
{
name: getMessage('Q.TRON M-G2'),
prop: 'moduleName',
},
{
name: getMessage('発電量 (kW)'),
name: `${getMessage('modal.panel.batch.statistic.power.generation.amount')}(kW)`,
prop: 'powerGeneration',
},
],

View File

@ -5,9 +5,7 @@ import { useMessage } from '@/hooks/useMessage'
import { canvasState, dotLineGridSettingState, dotLineIntervalSelector } from '@/store/canvasAtom'
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
import { onlyNumberInputChange } from '@/util/input-utils'
import { fabric } from 'fabric'
import { gridColorState } from '@/store/gridAtom'
import { gridDisplaySelector, settingModalGridOptionsState } from '@/store/settingAtom'
import { settingModalGridOptionsState } from '@/store/settingAtom'
import { useAxios } from '@/hooks/useAxios'
import { useSwal } from '@/hooks/useSwal'
import { usePopup } from '@/hooks/usePopup'
@ -33,7 +31,7 @@ export default function DotLineGrid(props) {
// const [modalOption, setModalOption] = useRecoilState(modalState); //modal state
const [objectNo, setObjectNo] = useState('test123240912001') //
const [close, setClose] = useState(false)
const { id, setIsShow, pos = { x: 840, y: -815 } } = props
const { id, setIsShow, pos = { x: 840, y: -815 }, isConfig = false } = props
const setSettingModalGridOptions = useSetRecoilState(settingModalGridOptionsState)
const canvas = useRecoilValue(canvasState)
@ -142,7 +140,7 @@ export default function DotLineGrid(props) {
await post({ url: `/api/canvas-management/canvas-grid-settings`, data: patternData }).then((res) => {
swalFire({ text: getMessage(res.returnMessage) })
setDotLineGridSettingState({ ...currentSetting })
closePopup(id)
closePopup(id, isConfig)
})
} catch (error) {
swalFire({ text: getMessage(res.returnMessage), icon: 'error' })
@ -213,7 +211,7 @@ export default function DotLineGrid(props) {
className="modal-close"
onClick={() => {
setIsShow(false)
closePopup(id)
closePopup(id, isConfig)
}}
>
닫기

View File

@ -78,14 +78,14 @@ export default function GridOption() {
//
setShowDotLineGridModal(selectedOption.selected)
addPopup(dotLineId, 2, <DotLineGrid {...dotLineGridProps} />)
addPopup(dotLineId, 2, <DotLineGrid {...dotLineGridProps} />, true)
} else if (selectedOption.id === 3) {
//
setAdsorptionPointAddMode(selectedOption.selected)
} else if (selectedOption.id === 4) {
//
setShowColorPickerModal(selectedOption.selected)
addPopup(colorId, 2, <ColorPickerModal {...colorPickerProps} />)
addPopup(colorId, 2, <ColorPickerModal {...colorPickerProps} />, true)
}
setGridOptions(newGridOptions)
@ -94,6 +94,7 @@ export default function GridOption() {
const dotLineGridProps = {
id: dotLineId,
setIsShow: setShowDotLineGridModal,
isConfig: true,
pos: {
x: 845,
y: 180,
@ -106,6 +107,7 @@ export default function GridOption() {
setColor: setGridColor,
isShow: showColorPickerModal,
setIsShow: setShowColorPickerModal,
isConfig: true,
pos: {
x: 785,
y: 180,

View File

@ -1,4 +1,4 @@
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { useRecoilValue } from 'recoil'
import { useMessage } from '@/hooks/useMessage'
import React, { useEffect, useState } from 'react'
import DimensionLineSetting from '@/components/floor-plan/modal/setting01/dimensionLine/DimensionLineSetting'
@ -67,6 +67,7 @@ export default function SecondOption() {
id: fontId,
pos: { x: 745, y: 180 },
setIsShow: setShowFontSettingModal,
isConfig: true,
}
const planSizeProps = {
id: planSizeId,
@ -86,36 +87,41 @@ export default function SecondOption() {
case 'font1': {
//
setShowFontSettingModal(true)
setShowDimensionLineSettingModal(false)
fontProps.type = 'commonText'
fontProps.id = fontId + 1
addPopup(fontId + 1, 2, <FontSetting {...fontProps} />)
addPopup(fontId + 1, 2, <FontSetting {...fontProps} />, true)
break
}
case 'font2': {
//
setShowFontSettingModal(true)
setShowDimensionLineSettingModal(false)
fontProps.type = 'flowText'
fontProps.id = fontId + 2
addPopup(fontId + 2, 2, <FontSetting {...fontProps} />)
addPopup(fontId + 2, 2, <FontSetting {...fontProps} />, true)
break
}
case 'font3': {
//
setShowFontSettingModal(true)
setShowDimensionLineSettingModal(false)
fontProps.type = 'lengthText'
fontProps.id = fontId + 3
addPopup(fontId + 3, 2, <FontSetting {...fontProps} />)
addPopup(fontId + 3, 2, <FontSetting {...fontProps} />, true)
break
}
case 'font4': {
//
setShowFontSettingModal(true)
setShowDimensionLineSettingModal(false)
fontProps.type = 'circuitNumberText'
fontProps.id = fontId
addPopup(fontId, 2, <FontSetting {...fontProps} />)
addPopup(fontId, 2, <FontSetting {...fontProps} />, true)
break
}
@ -123,7 +129,7 @@ export default function SecondOption() {
//
if (!showDimensionLineSettingModal) {
setShowDimensionLineSettingModal(true)
addPopup(dimensionId, 2, <DimensionLineSetting {...dimensionProps} />)
addPopup(dimensionId, 2, <DimensionLineSetting {...dimensionProps} />, true)
} else {
setShowDimensionLineSettingModal(false)
closePopup(dimensionId)
@ -134,7 +140,8 @@ export default function SecondOption() {
case 'planSize': {
//
setShowPlanSizeSettingModal(true)
addPopup(planSizeId, 2, <PlanSizeSetting {...planSizeProps} />)
setShowDimensionLineSettingModal(false)
addPopup(planSizeId, 2, <PlanSizeSetting {...planSizeProps} />, true)
break
}
}

View File

@ -11,7 +11,7 @@ import { useRecoilValue } from 'recoil'
import { usePopup } from '@/hooks/usePopup'
export default function SettingModal01(props) {
const { setShowDotLineGridModal, setShowFontSettingModal, id } = props
const { setShowDotLineGridModal, setShowFontSettingModal, id, isConfig } = props
console.log(props)
const [buttonAct, setButtonAct] = useState(1)
const { getMessage } = useMessage()
@ -27,7 +27,7 @@ export default function SettingModal01(props) {
<div className={`modal-pop-wrap sm mount`}>
<div className="modal-head">
<h1 className="title">{getMessage('modal.canvas.setting')}</h1>
<button className="modal-close" onClick={() => closePopup(id)}>
<button className="modal-close" onClick={() => closePopup(id, true)}>
닫기
</button>
</div>

View File

@ -87,6 +87,7 @@ export default function DimensionLineSetting(props) {
setFontColor: setOriginFontColor,
fontSize: originFontSize,
setFontSize: setOriginFontSize,
isConfig: true,
id: fontModalId,
pos: {
x: 455,
@ -97,17 +98,17 @@ export default function DimensionLineSetting(props) {
const popupHandle = (type) => {
switch (type) {
case 'color':
addPopup(colorModalId, 3, <ColorPickerModal {...colorPickerProps} />)
addPopup(colorModalId, 3, <ColorPickerModal {...colorPickerProps} />, true)
break
case 'font':
addPopup(fontModalId, 3, <FontSetting {...fontProps} />)
addPopup(fontModalId, 3, <FontSetting {...fontProps} />, true)
break
}
}
return (
<WithDraggable isShow={true} pos={pos}>
<div className={`modal-pop-wrap xxxm`}>
<div className={`modal-pop-wrap xxxm mount`}>
<div className="modal-head">
<h1 className="title">{getMessage('modal.canvas.setting.font.plan.absorption.dimension.line')} </h1>
<button

View File

@ -15,14 +15,14 @@ export default function PlanSizeSetting(props) {
return (
<WithDraggable isShow={true} pos={pos}>
<div className={`modal-pop-wrap xsm`}>
<div className={`modal-pop-wrap xsm mount`}>
<div className="modal-head">
<h1 className="title">{getMessage('modal.canvas.setting.font.plan.absorption.plan.size.setting')}</h1>
<button
className="modal-close"
onClick={() => {
setIsShow(false)
closePopup(id)
closePopup(id, true)
}}
>
닫기

View File

@ -0,0 +1,153 @@
import { useAxios } from '@/hooks/useAxios'
import { useEffect, useReducer, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { globalLocaleStore } from '@/store/localeAtom'
import { estimateState, floorPlanObjectState } from '@/store/floorPlanObjectAtom'
import { isObjectNotEmpty } from '@/util/common-utils'
const reducer = (prevState, nextState) => {
return { ...prevState, ...nextState }
}
// Constants
const ESTIMATE_API_ENDPOINT = '/api/estimates' // API 엔드포인트 정의
const defaultEstimateData = {
estimateDate: new Date(), //견적일
charger: '', //담당자
objectName: '', //안건명
objectNameOmit: '', //경칭코드
estimateType: 'YJOD', //주문분류
remarks: '', //비고
estimateOption: '', //견적특이사항
// itemList: [{ id: 1, name: '' }],
//아이템에 필요없는거 빼기
itemList: [
{
amount: '',
fileUploadFlg: '',
itemChangeFlg: '',
itemGroup: '',
itemId: '', //키값??
itemName: '',
itemNo: '',
moduleFlg: '',
objectNo: '',
pkgMaterialFlg: '',
planNo: '',
pnowW: '',
salePrice: '',
saleTotPrice: '',
specification: '',
unit: '',
},
],
}
// Helper functions
// const updateItemInList = (itemList, id, updates) => {
const updateItemInList = (itemList, itemId, updates) => {
// return itemList.map((item) => (item.id === id ? { ...item, ...updates } : item))
return itemList.map((item) => (item.itemId === itemId ? { ...item, ...updates } : item))
}
export const useEstimateController = (planNo) => {
const globalLocaleState = useRecoilValue(globalLocaleStore)
const objectRecoil = useRecoilValue(floorPlanObjectState)
const [estimateData, setEstimateData] = useRecoilState(estimateState)
const { get, post } = useAxios(globalLocaleState)
const [isLoading, setIsLoading] = useState(false)
const { promisePost } = useAxios()
const [state, setState] = useReducer(reducer, defaultEstimateData)
useEffect(() => {
if (!isLoading) {
if (objectRecoil.floorPlanObjectNo && planNo) {
fetchSetting()
}
}
}, [])
// 상세 조회
const fetchSetting = async () => {
try {
await get({ url: `/api/estimate/${objectRecoil.floorPlanObjectNo}/${planNo}/detail` }).then((res) => {
if (isObjectNotEmpty(res)) {
setState(res)
}
})
setIsLoading(true)
} catch (error) {
console.error('견적서 상세조회 Error: ', error)
setIsLoading(true)
}
}
// const updateItem = (id, updates) => {
const updateItem = (itemId, updates) => {
setState({
// itemList: updateItemInList(state.itemList, id, updates),
itemList: updateItemInList(state.itemList, itemId, updates),
})
}
const addItem = () => {
// const newId = Math.max(...state.itemList.map((item) => item.id)) + 1
const newItemId = Math.max(...state.itemList.map((item) => item.itemId)) + 1
setState({
// itemList: [...state.itemList, { id: newId, name: '' }],
//셋팅할필요없는거 빼기
itemList: [
...state.itemList,
{
itemId: newItemId,
amount: '',
fileUploadFlg: '',
itemChangeFlg: '',
itemGroup: '',
itemName: '',
itemNo: '',
moduleFlg: '',
objectNo: '',
pkgMaterialFlg: '',
planNo: '',
pnowW: '',
salePrice: '',
saleTotPrice: '',
specification: '',
unit: '',
},
],
})
}
useEffect(() => {
setEstimateData({ ...state })
}, [state])
//견적서 저장
const handleEstimateSubmit = async () => {
console.log('::담긴 estimateData:::', estimateData)
return
try {
const result = await promisePost({
url: ESTIMATE_API_ENDPOINT,
data: estimateData,
})
return result
} catch (error) {
console.error('Failed to submit estimate:', error)
throw error
}
}
return {
state,
setState,
updateItem,
addItem,
handleEstimateSubmit,
fetchSetting,
}
}

View File

@ -223,9 +223,9 @@ export function useCanvasSetting() {
const option1 = settingModalFirstOptions.option1
// 'allocDisplay' 할당 표시
// 'outlineDisplay' 외벽선 표시 'outerLine', 'wallLine'
// 'outlineDisplay' 외벽선 표시 'outerLine', POLYGON_TYPE.WALL
// 'gridDisplay' 그리드 표시 'lindGrid', 'dotGrid'
// 'lineDisplay' 지붕선 표시 'roof', 'roofBase'
// 'lineDisplay' 지붕선 표시 'roof', POLYGON_TYPE.ROOF
// 'wordDisplay' 문자 표시
// 'circuitNumDisplay' 회로번호 표시
// 'flowDisplay' 흐름방향 표시 'arrow'
@ -248,7 +248,7 @@ export function useCanvasSetting() {
optionName = ['lindGrid', 'dotGrid']
break
case 'lineDisplay': //지붕선 표시
optionName = ['roof', 'roofBase']
optionName = ['roof', POLYGON_TYPE.ROOF]
break
case 'wordDisplay': //문자 표시
optionName = ['6']

View File

@ -13,9 +13,9 @@ export function useFirstOption() {
const option1 = settingModalFirstOptions.option1
// 'allocDisplay' 할당 표시
// 'outlineDisplay' 외벽선 표시 'outerLine', 'wallLine'
// 'outlineDisplay' 외벽선 표시 'outerLine', POLYGON_TYPE.WALL
// 'gridDisplay' 그리드 표시 'lindGrid', 'dotGrid'
// 'lineDisplay' 지붕선 표시 'roof', 'roofBase'
// 'lineDisplay' 지붕선 표시 'roof', POLYGON_TYPE.ROOF
// 'wordDisplay' 문자 표시
// 'circuitNumDisplay' 회로번호 표시
// 'flowDisplay' 흐름방향 표시 'arrow'
@ -37,7 +37,7 @@ export function useFirstOption() {
optionName = ['lineGrid', 'dotGrid', 'adsorptionPoint', 'tempGrid']
break
case 'lineDisplay': //지붕선 표시
optionName = ['roof', 'roofBase']
optionName = ['roof', POLYGON_TYPE.ROOF]
break
case 'wordDisplay': //문자 표시
optionName = ['6']

View File

@ -15,7 +15,7 @@ import {
outerLineLength2State,
outerLineTypeState,
} from '@/store/outerLineAtom'
import { calculateIntersection, distanceBetweenPoints, findClosestPoint, polygonToTurfPolygon } from '@/util/canvas-util'
import { calculateIntersection, distanceBetweenPoints, findClosestPoint, isPointOnLine, polygonToTurfPolygon } from '@/util/canvas-util'
import { fabric } from 'fabric'
import { useAdsorptionPoint } from '@/hooks/useAdsorptionPoint'
import { useSwal } from '@/hooks/useSwal'
@ -23,6 +23,7 @@ import { booleanPointInPolygon } from '@turf/turf'
import { usePopup } from '@/hooks/usePopup'
import { calculateAngle } from '@/util/qpolygon-utils'
import { QPolygon } from '@/components/fabric/QPolygon'
import { POLYGON_TYPE } from '@/common/common'
// 보조선 작성
export function useAuxiliaryDrawing(id) {
@ -80,7 +81,7 @@ export function useAuxiliaryDrawing(id) {
useEffect(() => {
// innerLines가 있을경우 삭제
const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roofBase')
const roofs = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
if (roofs.length === 0) {
swalFire({ text: '지붕형상이 없습니다.' })
closePopup(id)
@ -561,7 +562,7 @@ export function useAuxiliaryDrawing(id) {
return
}
const roofBases = canvas.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
/*const allLines = [...auxiliaryLines]
roofBases.forEach((roofBase) => {
@ -611,9 +612,41 @@ export function useAuxiliaryDrawing(id) {
},
)
lineHistory.current.push(newLine)
lineHistory.current = lineHistory.current.filter((history) => history !== line1)
removeLine(line1)
intersectionPoints.current.push(...interSectionPointsWithRoofLines)
return
} else if (interSectionPointsWithRoofLines.length === 1) {
//지붕선과 만나는 점이 하나일 경우
const distance1 = distanceBetweenPoints({ x: line1.x1, y: line1.y1 }, interSectionPointsWithRoofLines[0])
const distance2 = distanceBetweenPoints({ x: line1.x2, y: line1.y2 }, interSectionPointsWithRoofLines[0])
if (!(distance1 === 0 || distance2 === 0)) {
if (distance1 >= distance2) {
const newLine = addLine([line1.x1, line1.y1, interSectionPointsWithRoofLines[0].x, interSectionPointsWithRoofLines[0].y], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'auxiliaryLine',
isFixed: true,
})
lineHistory.current.push(newLine)
lineHistory.current = lineHistory.current.filter((history) => history !== line1)
removeLine(line1)
} else {
const newLine = addLine([line1.x2, line1.y2, interSectionPointsWithRoofLines[0].x, interSectionPointsWithRoofLines[0].y], {
stroke: 'black',
strokeWidth: 1,
selectable: false,
name: 'auxiliaryLine',
isFixed: true,
})
lineHistory.current.push(newLine)
lineHistory.current = lineHistory.current.filter((history) => history !== line1)
removeLine(line1)
}
intersectionPoints.current.push(interSectionPointsWithRoofLines[0])
}
}
//보조선과 만나는 점을 찾는다.
@ -659,6 +692,7 @@ export function useAuxiliaryDrawing(id) {
})
}
lineHistory.current.push(newLine)
lineHistory.current = lineHistory.current.filter((history) => history !== line1)
removeLine(line1)
})
@ -742,7 +776,7 @@ export function useAuxiliaryDrawing(id) {
return
}
const roofBases = canvas.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
//lineHistory.current에 있는 선들 중 startPoint와 endPoint가 겹치는 line은 제거
// 겹치는 선 하나는 canvas에서 제거한다.
@ -772,9 +806,13 @@ export function useAuxiliaryDrawing(id) {
})
const roofInnerLines = innerLines.filter((line) => {
const inPolygon1 =
tempPolygonPoints.some((point) => point.x === line.x1 && point.y === line.y1) || roofBase.inPolygon({ x: line.x1, y: line.y1 })
tempPolygonPoints.some((point) => point.x === line.x1 && point.y === line.y1) ||
roofBase.inPolygon({ x: line.x1, y: line.y1 }) ||
roofBase.lines.some((line) => isPointOnLine(line, { x: line.x1, y: line.y1 }))
const inPolygon2 =
tempPolygonPoints.some((point) => point.x === line.x2 && point.y === line.y2) || roofBase.inPolygon({ x: line.x2, y: line.y2 })
tempPolygonPoints.some((point) => point.x === line.x2 && point.y === line.y2) ||
roofBase.inPolygon({ x: line.x2, y: line.y2 }) ||
roofBase.lines.some((line) => isPointOnLine(line, { x: line.x2, y: line.y2 }))
if (inPolygon1 && inPolygon2) {
line.attributes = { ...line.attributes, roofId: roofBase.id }

View File

@ -160,7 +160,7 @@ export function useEavesGableEdit(id) {
attributes,
})
const roofBases = canvas?.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
roofBases.forEach((roof) => {
roof.innerLines.forEach((line) => {

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
import { useRecoilValue, useResetRecoilState } from 'recoil'
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
import { canvasState, currentObjectState } from '@/store/canvasAtom'
import { useMode } from '@/hooks/useMode'
import { usePolygon } from '@/hooks/usePolygon'

View File

@ -82,12 +82,12 @@ export function useRoofAllocationSetting(id) {
const [selectedRoofMaterial, setSelectedRoofMaterial] = useState(roofMaterials[0])
useEffect(() => {
const roofBases = canvas.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
if (roofBases.length === 0) {
swalFire({ text: '할당할 지붕이 없습니다.' })
closePopup(id)
}
// if (type === 'roofBase') {
// if (type === POLYGON_TYPE.ROOF) {
// // 지붕면 할당
//
// } else if ('roof') {
@ -105,7 +105,7 @@ export function useRoofAllocationSetting(id) {
// 선택한 지붕재로 할당
const handleSave = () => {
const roofBases = canvas.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
roofBases.forEach((roofBase) => {
try {
@ -118,7 +118,7 @@ export function useRoofAllocationSetting(id) {
canvas.remove(line)
})
canvas.remove(roofBase)
// canvas.remove(roofBase)
})
wallLines.forEach((wallLine) => {

View File

@ -185,7 +185,7 @@ export function useRoofShapePassivitySetting(id) {
}
const handleLineToPolygon = () => {
const roofBases = canvas.getObjects().filter((obj) => obj.name === 'roofBase')
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
const exceptObjs = canvas.getObjects().filter((obj) => obj.name !== 'outerLine' && obj.parent?.name !== 'outerLine')
const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
exceptObjs.forEach((obj) => {

View File

@ -383,7 +383,7 @@ export function useRoofShapeSetting(id) {
canvas
.getObjects()
.filter((obj) => obj.name === 'roofBase')
.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
.forEach((obj) => {
canvas.remove(...obj.innerLines)
canvas.remove(obj)

View File

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { v4 as uuidv4 } from 'uuid'
import { canvasSizeState, canvasState, canvasZoomState, currentObjectState, fontFamilyState, fontSizeState } from '@/store/canvasAtom'
@ -18,6 +18,10 @@ export function useCanvasEvent() {
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
const { modifiedPlanFlag, setModifiedPlanFlag } = usePlan()
useEffect(() => {
canvas?.setZoom(canvasZoom / 100)
}, [canvasZoom])
// 기본적인 이벤트 필요시 추가
const attachDefaultEventOnCanvas = () => {
removeEventOnCanvas()
@ -365,6 +369,14 @@ export function useCanvasEvent() {
})
}
const handleZoom = (isZoom) => {
if (isZoom) {
setCanvasZoom(canvasZoom + 10)
} else {
setCanvasZoom(canvasZoom - 10)
}
}
const handleZoomClear = () => {
setCanvasZoom(100)
canvas.set({ zoom: 1 })
@ -376,5 +388,6 @@ export function useCanvasEvent() {
setCanvasForEvent,
attachDefaultEventOnCanvas,
handleZoomClear,
handleZoom,
}
}

View File

@ -244,7 +244,7 @@ export function useContextMenu() {
if (temp.length > 0) menu = temp
}
handleClick(null, menu)
if (menu) handleClick(null, menu)
}
useEffect(() => {
@ -256,8 +256,6 @@ export function useContextMenu() {
}, [currentContextMenu])
useEffect(() => {
console.log('currentObject', currentObject)
if (currentObject?.name) {
console.log(currentObject?.name)
switch (currentObject.name) {
@ -477,7 +475,7 @@ export function useContextMenu() {
{
id: 'dimensionLineDisplayEdit',
name: getMessage('contextmenu.display.edit'),
component: <DimensionLineSetting id={popupId} />,
component: <DimensionLineSetting id={popupId} isConfig={false} />,
},
],
])

View File

@ -4,47 +4,90 @@ import { contextPopupState, popupState } from '@/store/popupAtom'
export function usePopup() {
const [popup, setPopup] = useRecoilState(popupState)
const [contextMenuPopup, setContextMenuPopup] = useRecoilState(contextPopupState)
const addPopup = (id, depth, component) => {
setPopup({ children: [...filterDepth(depth), { id: id, depth: depth, component: component }] })
const addPopup = (id, depth, component, isConfig = false) => {
setPopup({
config: isConfig ? [...filterDepth(depth, isConfig), { id, depth, component, isConfig }] : [...popup.config],
other: !isConfig ? [...filterDepth(depth, isConfig), { id, depth, component, isConfig }] : [...popup.other],
})
}
const closePopup = (id) => {
const closePopup = (id, isConfig = false) => {
if (contextMenuPopup) setContextMenuPopup(null)
setPopup({ children: [...filterChildrenPopup(id).filter((child) => child.id !== id)] })
if (isConfig) {
setPopup({
config: [...filterChildrenPopup(id, isConfig).filter((child) => child.id !== id)],
other: popup.other,
})
} else {
setPopup({
config: popup.config,
other: [...filterChildrenPopup(id, isConfig).filter((child) => child.id !== id)],
})
}
}
const filterPopup = (depth) => {
setPopup({ children: [...filterDepth(depth)] })
setPopup({
config: [...filterDepth(depth)],
other: [],
})
}
const filterChildrenPopup = (id) => {
const target = popup.children.filter((child) => child.id === id)
if (target.length !== 0) {
return popup.children.filter((child) => child.depth <= target[0].depth)
const filterChildrenPopup = (id, isConfig) => {
let target = []
if (isConfig) {
target = popup?.config.filter((child) => child.id === id)
} else {
target = popup?.other.filter((child) => child.id === id)
}
return popup.children
if (target.length !== 0) {
if (isConfig) {
return popup?.config.filter((child) => child.depth <= target[0].depth)
} else {
return popup?.other.filter((child) => child.depth <= target[0].depth)
}
} else {
if (isConfig) {
return popup.config
} else {
return popup.other
}
}
}
const closePopups = (ids) => {
setPopup({ children: [...popup.children.filter((child) => !ids.includes(child.id))] })
setPopup({
config: [...popup?.config.filter((child) => !ids.includes(child.id))],
other: [...popup?.other.filter((child) => !ids.includes(child.id))],
})
}
const closeAll = () => {
setPopup({ children: [] })
setPopup({
other: [],
config: [],
})
}
const closePrevPopup = () => {
setPopup({ children: [...popup.children.slice(popup.children.length - 1)] })
setPopup({
config: [...popup?.slice(popup?.length - 1)],
other: [],
})
}
const filterDepth = (depth) => {
return [...popup.children.filter((child) => child.depth !== depth)]
const filterDepth = (depth, isConfig) => {
if (isConfig) {
return [...popup?.config.filter((child) => child.depth < depth)]
} else {
return [...popup?.other.filter((child) => child.depth < depth)]
}
}
return {
popup,
setPopup,
addPopup,
closePopup,
closePopups,

View File

@ -123,6 +123,7 @@
"modal.module.basic.setting.auto.placement": "設定値に自動配置",
"plan.menu.module.circuit.setting.circuit.trestle.setting": "回路と架台の設定",
"modal.circuit.trestle.setting": "回路と架台設定",
"modal.circuit.trestle.setting.alloc.trestle": "仮割り当て",
"modal.circuit.trestle.setting.power.conditional.select": "パワーコンディショナーを選択",
"modal.circuit.trestle.setting.power.conditional.select.name": "名称",
"modal.circuit.trestle.setting.power.conditional.select.rated.output": "定格出力",
@ -130,6 +131,8 @@
"modal.circuit.trestle.setting.power.conditional.select.max.connection": "最大接続枚数",
"modal.circuit.trestle.setting.power.conditional.select.max.overload": "過積最大枚数",
"modal.circuit.trestle.setting.power.conditional.select.output.current": "出力電流",
"modal.circuit.trestle.setting.power.conditional.select.check1": "同一傾斜同一方面の面積の場合、同じ面として回路を分ける。",
"modal.circuit.trestle.setting.power.conditional.select.check2": "MAX接続過積で回路を分ける。",
"modal.circuit.trestle.setting.circuit.allocation": "回路割り当て",
"modal.circuit.trestle.setting.circuit.allocation.auto": "自動回路割り当て",
"modal.circuit.trestle.setting.circuit.allocation.passivity": "手動回路割当",
@ -140,6 +143,12 @@
"modal.circuit.trestle.setting.circuit.allocation.passivity.all.power.conditional.reset": "すべての回路番号の初期化",
"modal.circuit.trestle.setting.circuit.allocation.passivity.circuit.num.fix": "番号確定",
"modal.circuit.trestle.setting.step.up.allocation": "昇圧設定",
"modal.circuit.trestle.setting.step.up.allocation.serial.amount": "シリアル枚数",
"modal.circuit.trestle.setting.step.up.allocation.total.amount": "総回路数",
"modal.circuit.trestle.setting.step.up.allocation.connected": "接続する",
"modal.circuit.trestle.setting.step.up.allocation.circuit.amount": "昇圧回路数",
"modal.circuit.trestle.setting.step.up.allocation.option": "昇圧オプション",
"modal.circuit.trestle.setting.step.up.allocation.select.monitor": "モニターの選択",
"plan.menu.module.circuit.setting.plan.orientation": "図面方位の適用",
"plan.menu.estimate": "見積",
"plan.menu.estimate.roof.alloc": "屋根面の割り当て",
@ -474,6 +483,19 @@
"commons.east": "ドン",
"commons.south": "南",
"commons.north": "北",
"font.style.normal": "보통(JA)",
"font.style.italic": "기울임꼴(JA)",
"font.style.bold": "굵게(JA)",
"font.style.bold.italic": "굵은 기울임꼴(JA)",
"color.black": "검정색(JA)",
"color.red": "빨강색(JA)",
"color.blue": "파랑색(JA)",
"color.gray": "회색(JA)",
"color.yellow": "황색(JA)",
"color.green": "녹색(JA)",
"color.pink": "분홍색(JA)",
"color.gold": "황금색(JA)",
"color.darkblue": "남색(JA)",
"site.name": "Q.CAST III",
"site.sub_name": "太陽光発電システム図面管理サイト",
"board.notice.title": "お知らせ",
@ -814,7 +836,10 @@
"estimate.detail.sepcialEstimateProductInfo.calcFormula2": "PKG単価 (W)×PKG容量(W)",
"estimate.detail.header.showPrice": "価格表示",
"estimate.detail.showPrice.btn1": "Pricing",
"estimate.detail.showPrice.description": "クリックして製品の特異性を確認する",
"estimate.detail.showPrice.description1": "製品価格 OPEN",
"estimate.detail.showPrice.description2": "追加, 変更資材",
"estimate.detail.showPrice.description3": "添付必須",
"estimate.detail.showPrice.description4": "クリックして製品の特異性を確認する",
"estimate.detail.showPrice.btn2": "製品を追加",
"estimate.detail.showPrice.btn3": "製品削除"
}

View File

@ -127,6 +127,7 @@
"modal.module.basic.setting.auto.placement": "설정값으로 자동 배치",
"plan.menu.module.circuit.setting.circuit.trestle.setting": "회로 및 가대 설정",
"modal.circuit.trestle.setting": "회로 및 가대설정",
"modal.circuit.trestle.setting.alloc.trestle": "가대할당",
"modal.circuit.trestle.setting.power.conditional.select": "파워컨디셔너 선택",
"modal.circuit.trestle.setting.power.conditional.select.name": "명칭",
"modal.circuit.trestle.setting.power.conditional.select.rated.output": "정격출력",
@ -134,6 +135,8 @@
"modal.circuit.trestle.setting.power.conditional.select.max.connection": "최대접속매수",
"modal.circuit.trestle.setting.power.conditional.select.max.overload": "과적최대매수",
"modal.circuit.trestle.setting.power.conditional.select.output.current": "출력전류",
"modal.circuit.trestle.setting.power.conditional.select.check1": "동일경사 동일 방면의 면적인 경우, 같은 면으로서 회로를 나눈다.",
"modal.circuit.trestle.setting.power.conditional.select.check2": "MAX 접속(과적)으로 회로를 나눈다.",
"modal.circuit.trestle.setting.circuit.allocation": "회로 할당",
"modal.circuit.trestle.setting.circuit.allocation.auto": "자동 회로 할당",
"modal.circuit.trestle.setting.circuit.allocation.passivity": "수동 회로 할당",
@ -144,6 +147,12 @@
"modal.circuit.trestle.setting.circuit.allocation.passivity.all.power.conditional.reset": "모든 회로번호 초기화",
"modal.circuit.trestle.setting.circuit.allocation.passivity.circuit.num.fix": "번호 확정",
"modal.circuit.trestle.setting.step.up.allocation": "승압 설정",
"modal.circuit.trestle.setting.step.up.allocation.serial.amount": "직렬매수",
"modal.circuit.trestle.setting.step.up.allocation.total.amount": "총 회로수",
"modal.circuit.trestle.setting.step.up.allocation.connected": "연결함",
"modal.circuit.trestle.setting.step.up.allocation.circuit.amount": "승압회로수",
"modal.circuit.trestle.setting.step.up.allocation.option": "승압옵션",
"modal.circuit.trestle.setting.step.up.allocation.select.monitor": "모니터 선택",
"plan.menu.module.circuit.setting.plan.orientation": "도면 방위 적용",
"plan.menu.estimate": "견적서",
"plan.menu.estimate.roof.alloc": "지붕면 할당",
@ -480,6 +489,19 @@
"commons.east": "동",
"commons.south": "남",
"commons.north": "북",
"font.style.normal": "보통",
"font.style.italic": "기울임꼴",
"font.style.bold": "굵게",
"font.style.bold.italic": "굵은 기울임꼴",
"color.black": "검정색",
"color.red": "빨강색",
"color.blue": "파랑색",
"color.gray": "회색",
"color.yellow": "황색",
"color.green": "녹색",
"color.pink": "분홍색",
"color.gold": "황금색",
"color.darkblue": "남색",
"site.name": "Q.CAST III",
"site.sub_name": "태양광 발전 시스템 도면관리 사이트",
"board.notice.title": "공지사항",
@ -820,7 +842,10 @@
"estimate.detail.sepcialEstimateProductInfo.calcFormula2": "PKG단가(W) * PKG용량(W)",
"estimate.detail.header.showPrice": "가격표시",
"estimate.detail.showPrice.btn1": "Pricing",
"estimate.detail.showPrice.description": "클릭하여 제품 특이사항 확인",
"estimate.detail.showPrice.description1": "제품 가격 OPEN",
"estimate.detail.showPrice.description2": "추가, 변경 자재",
"estimate.detail.showPrice.description3": "첨부필수",
"estimate.detail.showPrice.description4": "클릭하여 제품 특이사항 확인",
"estimate.detail.showPrice.btn2": "제품추가",
"estimate.detail.showPrice.btn3": "제품삭제"
}

View File

@ -7,3 +7,9 @@ export const floorPlanObjectState = atom({
},
dangerouslyAllowMutability: true,
})
export const estimateState = atom({
key: `estimateState`,
default: {},
dangerouslyAllowMutability: true,
})

View File

@ -7,7 +7,8 @@ import { atom } from 'recoil'
export const popupState = atom({
key: 'popupState',
default: {
children: [],
config: [],
other: [],
},
dangerouslyAllowMutability: true,
})

View File

@ -5,7 +5,7 @@ export const settingModalFirstOptionsState = atom({
default: {
option1: [
{ id: 1, column: 'allocDisplay', name: 'modal.canvas.setting.first.option.alloc', selected: false },
{ id: 2, column: 'outlineDisplay', name: 'modal.canvas.setting.first.option.outline', selected: false },
{ id: 2, column: 'outlineDisplay', name: 'modal.canvas.setting.first.option.outline', selected: true },
{ id: 3, column: 'gridDisplay', name: 'modal.canvas.setting.first.option.grid', selected: false },
{ id: 4, column: 'lineDisplay', name: 'modal.canvas.setting.first.option.roof.line', selected: false },
{ id: 5, column: 'wordDisplay', name: 'modal.canvas.setting.first.option.word', selected: false },

File diff suppressed because it is too large Load Diff

View File

@ -239,6 +239,7 @@ footer{
nav{
.nav-list{
.nav-item{
a,
button{
font-size: 13px;
}

View File

@ -37,9 +37,10 @@
top: 50%;
left: 0;
transform: translateY(-50%);
width: 20px;
height: 20px;
width: 22px;
height: 22px;
background: url(../../public/static/images/main/id_icon.svg)no-repeat center;
background-size: cover;
}
}
.store-arr{

File diff suppressed because it is too large Load Diff

View File

@ -521,9 +521,11 @@ export function isPointOnLine(line, point) {
const a = line.y2 - line.y1
const b = line.x1 - line.x2
const c = line.x2 * line.y1 - line.x1 * line.y2
return a * point.x + b * point.y + c === 0
}
const result = Math.abs(a * point.x + b * point.y + c) / 100
// 점이 선 위에 있는지 확인
return result <= 10
}
/**
* 점과 가까운 line 찾기
* @param point

View File

@ -1,6 +1,14 @@
import { fabric } from 'fabric'
import { QLine } from '@/components/fabric/QLine'
import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDegreeByChon, getDirectionByPoint } from '@/util/canvas-util'
import {
calculateIntersection,
distanceBetweenPoints,
findClosestPoint,
getDegreeByChon,
getDirectionByPoint,
isPointOnLine,
} from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon'
import * as turf from '@turf/turf'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
@ -958,10 +966,13 @@ export default function offsetPolygon(vertices, offset) {
}
}
export const splitPolygonWithLines = (polygon) => {
/*export const splitPolygonWithLines = (polygon) => {
const roofs = []
const allLines = [...polygon.innerLines]
const polygonLines = polygon.lines
const innerLines = polygon.innerLines
allLines.forEach((line) => {
line.startPoint = { x: line.x1, y: line.y1 }
line.endPoint = { x: line.x2, y: line.y2 }
@ -984,10 +995,10 @@ export const splitPolygonWithLines = (polygon) => {
})
})
/**
/!**
* 좌표 테스트용
*/
/*allLines.forEach((line) => {
*!/
/!*allLines.forEach((line) => {
const text = new fabric.Text(`(${line.startPoint.x},${line.startPoint.y})`, {
left: line.startPoint.x,
top: line.startPoint.y,
@ -1016,10 +1027,10 @@ export const splitPolygonWithLines = (polygon) => {
polygon.canvas.add(text)
polygon.canvas.renderAll()
})*/
/**
})*!/
/!**
* 좌표 테스트용
*/
*!/
polygon.points.forEach((point, index) => {
allLines.forEach((line) => {
@ -1165,6 +1176,273 @@ export const splitPolygonWithLines = (polygon) => {
polygon.canvas.add(roof)
polygon.canvas.renderAll()
})
}*/
export const splitPolygonWithLines = (polygon) => {
const canvas = polygon.canvas
polygon.set({ visible: false })
let innerLines = [...polygon.innerLines]
let polygonLines = [...polygon.lines]
const roofs = []
let delIndexs = []
let newLines = []
polygonLines.forEach((line, index) => {
line.tempIndex = index
innerLines.forEach((innerLine) => {
let newLine1, newLine2
if (isPointOnLine(line, innerLine.startPoint)) {
// 해당 line을 startPoint로 나눈 line2개를 canvas에 추가 하고 기존 line을 제거한다.
newLine1 = new QLine([line.x1, line.y1, innerLine.startPoint.x, innerLine.startPoint.y], {
fontSize: polygon.fontSize,
stroke: 'black',
strokeWidth: 3,
})
newLine2 = new QLine([innerLine.startPoint.x, innerLine.startPoint.y, line.x2, line.y2], {
fontSize: polygon.fontSize,
stroke: 'black',
strokeWidth: 3,
})
delIndexs.push(polygonLines.indexOf(line))
canvas.remove(polygonLines[polygonLines.indexOf(line)])
if (newLine1.length / 10 > 10) {
newLines.push(newLine1)
}
if (newLine2.length / 10 > 10) {
newLines.push(newLine2)
}
}
if (isPointOnLine(line, innerLine.endPoint)) {
newLine1 = new QLine([line.x1, line.y1, innerLine.endPoint.x, innerLine.endPoint.y], {
fontSize: polygon.fontSize,
stroke: 'black',
strokeWidth: 3,
})
newLine2 = new QLine([innerLine.endPoint.x, innerLine.endPoint.y, line.x2, line.y2], {
fontSize: polygon.fontSize,
stroke: 'black',
strokeWidth: 3,
})
delIndexs.push(polygonLines.indexOf(line))
canvas.remove(polygonLines[polygonLines.indexOf(line)])
if (newLine1.length / 10 > 10) {
newLines.push(newLine1)
}
if (newLine2.length / 10 > 10) {
newLines.push(newLine2)
}
}
})
})
polygonLines = polygonLines.filter((line) => !delIndexs.includes(line.tempIndex))
polygonLines = [...polygonLines, ...newLines]
const allLines = [...polygonLines, ...innerLines]
/**
* 왼쪽 상단을 startPoint로 전부 변경
*/
allLines.forEach((line) => {
let startPoint // 시작점
let endPoint // 끝점
if (line.x1 < line.x2) {
startPoint = { x: line.x1, y: line.y1 }
endPoint = { x: line.x2, y: line.y2 }
} else if (line.x1 > line.x2) {
startPoint = { x: line.x2, y: line.y2 }
endPoint = { x: line.x1, y: line.y1 }
} else {
if (line.y1 < line.y2) {
startPoint = { x: line.x1, y: line.y1 }
endPoint = { x: line.x2, y: line.y2 }
} else {
startPoint = { x: line.x2, y: line.y2 }
endPoint = { x: line.x1, y: line.y1 }
}
}
line.startPoint = startPoint
line.endPoint = endPoint
})
polygonLines.forEach((line) => {
const startPoint = line.startPoint // 시작점
let arrivalPoint = line.endPoint // 도착점
let currentPoint = startPoint
const roofPoints = [startPoint]
const startLine = line
const visitPoints = [startPoint]
const visitLines = [startLine]
let cnt = 0
while (!isSamePoint(currentPoint, arrivalPoint)) {
line.set({ stroke: 'red' })
canvas.renderAll()
let nextLines = allLines.filter(
(line2) =>
(isSamePoint(line2.startPoint, currentPoint) || isSamePoint(line2.endPoint, currentPoint)) &&
line !== line2 &&
innerLines.includes(line2) &&
!visitLines.includes(line2),
)
if (nextLines.length === 0) {
nextLines = allLines.filter(
(line2) =>
(isSamePoint(line2.startPoint, currentPoint) || isSamePoint(line2.endPoint, currentPoint)) &&
line !== line2 &&
!visitLines.includes(line2),
)
}
if (!nextLines) {
break
}
let comparisonPoints = []
nextLines.forEach((nextLine) => {
if (isSamePoint(nextLine.startPoint, currentPoint)) {
comparisonPoints.push(nextLine.endPoint)
} else {
comparisonPoints.push(nextLine.startPoint)
}
})
comparisonPoints = comparisonPoints.filter((point) => !visitPoints.some((visitPoint) => isSamePoint(visitPoint, point)))
comparisonPoints = comparisonPoints.filter((point) => !isSamePoint(point, currentPoint))
const minDistancePoint = comparisonPoints.reduce((prev, current) => {
const prevDistance = Math.sqrt(Math.pow(prev.x - arrivalPoint.x, 2) + Math.pow(prev.y - arrivalPoint.y, 2))
const currentDistance = Math.sqrt(Math.pow(current.x - arrivalPoint.x, 2) + Math.pow(current.y - arrivalPoint.y, 2))
return prevDistance < currentDistance ? prev : current
}, comparisonPoints[0])
nextLines.forEach((nextLine) => {
if (isSamePoint(nextLine.startPoint, minDistancePoint) || isSamePoint(nextLine.endPoint, minDistancePoint)) {
visitLines.push(nextLine)
}
})
currentPoint = { ...minDistancePoint }
roofPoints.push(currentPoint)
cnt++
if (cnt > 100) {
throw new Error('무한루프')
}
}
roofs.push(roofPoints)
})
const newRoofs = removeDuplicatePolygons(roofs)
newRoofs.forEach((roofPoint, index) => {
let defense, pitch
const polygonLines = [...polygon.lines]
let representLines = []
let representLine
// 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다.
polygonLines.forEach((line) => {
let startFlag = false
let endFlag = false
const startPoint = line.startPoint
const endPoint = line.endPoint
roofPoint.forEach((point, index) => {
if (isSamePoint(point, startPoint)) {
startFlag = true
}
if (isSamePoint(point, endPoint)) {
endFlag = true
}
})
if (startFlag && endFlag) {
representLines.push(line)
}
})
// representLines중 가장 긴 line을 찾는다.
representLines.forEach((line) => {
if (!representLine) {
representLine = line
} else {
if (representLine.length < line.length) {
representLine = line
}
}
})
const direction = representLine.direction
switch (direction) {
case 'top':
defense = 'east'
break
case 'right':
defense = 'south'
break
case 'bottom':
defense = 'west'
break
case 'left':
defense = 'north'
break
}
pitch = polygon.lines[index].attributes?.pitch ?? 0
const roof = new QPolygon(roofPoint, {
fontSize: polygon.fontSize,
stroke: 'black',
fill: 'transparent',
strokeWidth: 3,
name: POLYGON_TYPE.ROOF,
originX: 'center',
originY: 'center',
selectable: true,
defense: defense,
direction: defense,
pitch: pitch,
})
polygon.canvas.add(roof)
canvas.remove(polygon)
polygon.canvas.renderAll()
})
}
function normalizePoint(point) {
return {
x: Math.round(point.x),
y: Math.round(point.y),
}
}
function arePolygonsEqual(polygon1, polygon2) {
if (polygon1.length !== polygon2.length) return false
const normalizedPolygon1 = polygon1.map(normalizePoint).sort((a, b) => a.x - b.x || a.y - b.y)
const normalizedPolygon2 = polygon2.map(normalizePoint).sort((a, b) => a.x - b.x || a.y - b.y)
return normalizedPolygon1.every((point, index) => arePointsEqual(point, normalizedPolygon2[index]))
}
function removeDuplicatePolygons(polygons) {
const uniquePolygons = []
polygons.forEach((polygon) => {
const isDuplicate = uniquePolygons.some((uniquePolygon) => arePolygonsEqual(polygon, uniquePolygon))
if (!isDuplicate) {
uniquePolygons.push(polygon)
}
})
return uniquePolygons
}
const isSamePoint = (a, b) => {
@ -3348,7 +3626,7 @@ function createRoofPaddingPolygon(polygon, lines, arcSegments = 0) {
}
function arePointsEqual(point1, point2) {
return point1.x === point2.x && point1.y === point2.y
return Math.abs(point1.x - point2.x) <= 1 && Math.abs(point1.y - point2.y) <= 1
}
function arraysHaveSamePoints(array1, array2) {