dev #799
@ -2,6 +2,8 @@
|
|||||||
import { useEffect, useState, useContext, useRef } from 'react'
|
import { useEffect, useState, useContext, useRef } from 'react'
|
||||||
import { useMessage } from '@/hooks/useMessage'
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import { useAxios } from '@/hooks/useAxios'
|
import { useAxios } from '@/hooks/useAxios'
|
||||||
|
import { useRecoilValue } from 'recoil'
|
||||||
|
import { globalLocaleStore } from '@/store/localeAtom'
|
||||||
import Select from 'react-select'
|
import Select from 'react-select'
|
||||||
import { SessionContext } from '@/app/SessionProvider'
|
import { SessionContext } from '@/app/SessionProvider'
|
||||||
import { isEmptyArray, isObjectNotEmpty } from '@/util/common-utils'
|
import { isEmptyArray, isObjectNotEmpty } from '@/util/common-utils'
|
||||||
@ -10,24 +12,35 @@ import { useEstimateController } from '@/hooks/floorPlan/estimate/useEstimateCon
|
|||||||
export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
const { get } = useAxios()
|
const { get } = useAxios()
|
||||||
|
const globalLocale = useRecoilValue(globalLocaleStore)
|
||||||
const { handleEstimateCopy, estimateContextState } = useEstimateController(planNo, true)
|
const { handleEstimateCopy, estimateContextState } = useEstimateController(planNo, true)
|
||||||
|
|
||||||
const { session } = useContext(SessionContext)
|
const { session } = useContext(SessionContext)
|
||||||
|
|
||||||
|
// storeLvl 기반 동적 라벨
|
||||||
|
const lvl = session?.storeLvl || '1'
|
||||||
|
const nextLvl = Number(lvl) + 1
|
||||||
|
const saleStoreLabel = globalLocale === 'ko'
|
||||||
|
? `${lvl}차 판매점명 / ID`
|
||||||
|
: `${lvl}次販売店名/ID`
|
||||||
|
const otherSaleStoreLabel = globalLocale === 'ko'
|
||||||
|
? `${nextLvl}차+하위 판매점명 / ID`
|
||||||
|
: `${nextLvl}次下位販売店名/ID`
|
||||||
|
|
||||||
const [saleStoreList, setSaleStoreList] = useState([]) // 판매점 리스트
|
const [saleStoreList, setSaleStoreList] = useState([]) // 판매점 리스트
|
||||||
const [favoriteStoreList, setFavoriteStoreList] = useState([]) //즐겨찾기한 판매점목록
|
const [favoriteStoreList, setFavoriteStoreList] = useState([]) //즐겨찾기한 판매점목록
|
||||||
const [showSaleStoreList, setShowSaleStoreList] = useState([]) //보여줄 판매점목록
|
const [showSaleStoreList, setShowSaleStoreList] = useState([]) //보여줄 판매점목록
|
||||||
const [otherSaleStoreList, setOtherSaleStoreList] = useState([])
|
const [otherSaleStoreList, setOtherSaleStoreList] = useState([])
|
||||||
const [originOtherSaleStoreList, setOriginOtherSaleStoreList] = useState([])
|
const [originOtherSaleStoreList, setOriginOtherSaleStoreList] = useState([])
|
||||||
|
|
||||||
const [saleStoreId, setSaleStoreId] = useState('') //선택한 1차점
|
const [saleStoreId, setSaleStoreId] = useState('') //선택한 상위점
|
||||||
const [otherSaleStoreId, setOtherSaleStoreId] = useState('') //선택한 1차점 이외
|
const [saleStoreName, setSaleStoreName] = useState('') //선택한 상위점명
|
||||||
|
const [otherSaleStoreId, setOtherSaleStoreId] = useState('') //선택한 하위점
|
||||||
|
|
||||||
const [sendPlanNo, setSendPlanNo] = useState('1')
|
const [sendPlanNo, setSendPlanNo] = useState('1')
|
||||||
const [copyReceiveUser, setCopyReceiveUser] = useState('')
|
const [copyReceiveUser, setCopyReceiveUser] = useState('')
|
||||||
|
|
||||||
const ref = useRef() //2차점 자동완성 초기화용
|
const ref = useRef() //하위점 자동완성 초기화용
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let url
|
let url
|
||||||
@ -40,8 +53,9 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
if (session.storeLvl === '1') {
|
if (session.storeLvl === '1') {
|
||||||
url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
||||||
} else {
|
} else {
|
||||||
//T01 or 1차점만 복사버튼 노출됨으로
|
// 2차+ 판매점: 자기 자신이 상위점, 하위점 목록 조회
|
||||||
// url = `/api/object/saleStore/${session?.storeId}/list?firstFlg=1&userId=${session?.userId}`
|
setSaleStoreId(session?.storeId)
|
||||||
|
url = `/api/object/saleStore/${session?.storeId}/childList?storeLvl=${session?.storeLvl}&userId=${session?.userId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +105,14 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
|
|
||||||
setOtherSaleStoreList(otherList)
|
setOtherSaleStoreList(otherList)
|
||||||
} else {
|
} else {
|
||||||
//T01 or 1차점만 복사버튼 노출됨으로
|
// 2차+ 판매점: 자기 자신은 상위점명으로, 나머지는 하위점 선택 목록으로 설정
|
||||||
|
const myself = res.find((row) => row.saleStoreId === session?.storeId)
|
||||||
|
if (myself) {
|
||||||
|
setSaleStoreName(myself.saleStoreName)
|
||||||
|
}
|
||||||
|
otherList = res.filter((row) => row.saleStoreId !== session?.storeId)
|
||||||
|
setOtherSaleStoreList(otherList)
|
||||||
|
setOriginOtherSaleStoreList(otherList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -119,7 +140,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1차점 변경 이벤트
|
// 상위점 변경 이벤트
|
||||||
const onSelectionChange = (key) => {
|
const onSelectionChange = (key) => {
|
||||||
if (isObjectNotEmpty(key)) {
|
if (isObjectNotEmpty(key)) {
|
||||||
if (key.saleStoreId === saleStoreId) {
|
if (key.saleStoreId === saleStoreId) {
|
||||||
@ -154,7 +175,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2차점 변경 이벤트
|
// 하위점 변경 이벤트
|
||||||
const onSelectionChange2 = (key) => {
|
const onSelectionChange2 = (key) => {
|
||||||
if (isObjectNotEmpty(key)) {
|
if (isObjectNotEmpty(key)) {
|
||||||
if (key.saleStoreId === otherSaleStoreId) {
|
if (key.saleStoreId === otherSaleStoreId) {
|
||||||
@ -169,7 +190,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//2차점 자동완성 초기화
|
//하위점 자동완성 초기화
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.clearValue()
|
ref.current.clearValue()
|
||||||
@ -198,7 +219,7 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
<div className="estimate-copy-info-wrap">
|
<div className="estimate-copy-info-wrap">
|
||||||
<div className="estimate-copy-info-item">
|
<div className="estimate-copy-info-item">
|
||||||
<div className="estimate-copy-info-tit">
|
<div className="estimate-copy-info-tit">
|
||||||
{getMessage('estimate.detail.estimateCopyPopup.label.saleStoreId')} <span className="red">*</span>
|
{saleStoreLabel} <span className="red">*</span>
|
||||||
</div>
|
</div>
|
||||||
{session.storeId === 'T01' && (
|
{session.storeId === 'T01' && (
|
||||||
<div className="estimate-copy-info-box">
|
<div className="estimate-copy-info-box">
|
||||||
@ -247,9 +268,17 @@ export default function EstimateCopyPop({ planNo, setEstimateCopyPopupOpen }) {
|
|||||||
<div className="estimate-copy-id">{saleStoreId}</div>
|
<div className="estimate-copy-id">{saleStoreId}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{session.storeId !== 'T01' && session.storeLvl !== '1' && (
|
||||||
|
<div className="estimate-copy-info-box">
|
||||||
|
<div className="estimate-copy-sel">
|
||||||
|
<input type="text" className="input-light" value={saleStoreName || ''} disabled />
|
||||||
|
</div>
|
||||||
|
<div className="estimate-copy-id">{saleStoreId}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="estimate-copy-info-item">
|
<div className="estimate-copy-info-item">
|
||||||
<div className="estimate-copy-info-tit">{getMessage('estimate.detail.estimateCopyPopup.label.otherSaleStoreId')}</div>
|
<div className="estimate-copy-info-tit">{otherSaleStoreLabel}</div>
|
||||||
<div className="estimate-copy-info-box">
|
<div className="estimate-copy-info-box">
|
||||||
<div className="estimate-copy-sel">
|
<div className="estimate-copy-sel">
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@ -6,7 +6,88 @@ import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, toGeo
|
|||||||
import * as turf from '@turf/turf'
|
import * as turf from '@turf/turf'
|
||||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import { drawSkeletonRidgeRoof } from '@/util/skeleton-utils'
|
import { drawSkeletonRidgeRoof, drawSkeletonRidgeRoofFromBaseLines, verifyMoveBoundary } from '@/util/skeleton-utils'
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// [DEBUG] 캔버스 라인 라벨 — 로컬 전용 (NEXT_PUBLIC_RUN_MODE === 'local')
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// 별도 util 파일을 두지 않고 QPolygon.js 안에 인라인.
|
||||||
|
// dev/prd 에선 가드가 즉시 false → 코드 미실행. 외부 의존성 없음.
|
||||||
|
// 라인 자체의 stroke/fill 은 절대 손대지 않고 fabric.Text 만 추가.
|
||||||
|
// ========================================================================
|
||||||
|
const DEBUG_LABEL_NAME = '__debugLabel'
|
||||||
|
|
||||||
|
function __isDebugLabelsEnabled() {
|
||||||
|
return process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||||
|
}
|
||||||
|
|
||||||
|
function __classifyLineForLabel(obj) {
|
||||||
|
const name = obj.name
|
||||||
|
const lineName = obj.lineName
|
||||||
|
const type = obj.attributes?.type
|
||||||
|
|
||||||
|
if (name === 'baseLine') return 'B'
|
||||||
|
if (name === 'outerLine' || name === 'eaves' || lineName === 'roofLine') return 'R'
|
||||||
|
if (name === LINE_TYPE.SUBLINE.HIP || lineName === LINE_TYPE.SUBLINE.HIP) return 'H'
|
||||||
|
if (name === LINE_TYPE.SUBLINE.RIDGE || lineName === LINE_TYPE.SUBLINE.RIDGE) return 'RG'
|
||||||
|
if (name === LINE_TYPE.SUBLINE.VALLEY || lineName === LINE_TYPE.SUBLINE.VALLEY) return 'V'
|
||||||
|
if (name === LINE_TYPE.SUBLINE.GABLE || lineName === LINE_TYPE.SUBLINE.GABLE) return 'G'
|
||||||
|
if (name === LINE_TYPE.SUBLINE.VERGE || lineName === LINE_TYPE.SUBLINE.VERGE) return 'VG'
|
||||||
|
if (
|
||||||
|
type === LINE_TYPE.WALLLINE.EAVE_HELP_LINE ||
|
||||||
|
lineName === LINE_TYPE.WALLLINE.EAVE_HELP_LINE ||
|
||||||
|
name === LINE_TYPE.WALLLINE.EAVE_HELP_LINE
|
||||||
|
) return 'E'
|
||||||
|
if (
|
||||||
|
typeof obj.x1 === 'number' &&
|
||||||
|
typeof obj.y1 === 'number' &&
|
||||||
|
typeof obj.x2 === 'number' &&
|
||||||
|
typeof obj.y2 === 'number'
|
||||||
|
) return 'SK'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function __attachDebugLabels(canvas, parentId) {
|
||||||
|
if (!__isDebugLabelsEnabled()) return
|
||||||
|
if (!canvas || !parentId) return
|
||||||
|
|
||||||
|
const counters = {}
|
||||||
|
const objects = canvas.getObjects().filter((o) => o.parentId === parentId && o.name !== DEBUG_LABEL_NAME)
|
||||||
|
|
||||||
|
objects.forEach((obj) => {
|
||||||
|
const prefix = __classifyLineForLabel(obj)
|
||||||
|
if (!prefix) return
|
||||||
|
|
||||||
|
counters[prefix] = (counters[prefix] || 0) + 1
|
||||||
|
const label = `${prefix}-${counters[prefix]}`
|
||||||
|
const mx = (obj.x1 + obj.x2) / 2
|
||||||
|
const my = (obj.y1 + obj.y2) / 2
|
||||||
|
|
||||||
|
const text = new fabric.Text(label, {
|
||||||
|
left: mx,
|
||||||
|
top: my,
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
fontSize: 12,
|
||||||
|
fill: '#000',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
backgroundColor: 'rgba(255,255,0,0.85)',
|
||||||
|
selectable: false,
|
||||||
|
evented: false,
|
||||||
|
hasControls: false,
|
||||||
|
hasBorders: false,
|
||||||
|
name: DEBUG_LABEL_NAME,
|
||||||
|
parentId: parentId,
|
||||||
|
excludeFromExport: true,
|
||||||
|
})
|
||||||
|
canvas.add(text)
|
||||||
|
text.bringToFront()
|
||||||
|
})
|
||||||
|
|
||||||
|
canvas.renderAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||||
type: 'QPolygon',
|
type: 'QPolygon',
|
||||||
@ -380,8 +461,26 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
|||||||
/**
|
/**
|
||||||
* 보조선 그리기
|
* 보조선 그리기
|
||||||
* @param settingModalFirstOptions
|
* @param settingModalFirstOptions
|
||||||
|
* @param forceRedraw - true 면 verifyMoveBoundary 'crossed' 가드를 우회하고 강제로 재빌드.
|
||||||
|
* (예: 오버 이동 상태에서 wall.baseLines/roof.lines 는 그대로 두고 SK 만 재생성하고 싶을 때)
|
||||||
*/
|
*/
|
||||||
drawHelpLine(settingModalFirstOptions) {
|
drawHelpLine(settingModalFirstOptions, forceRedraw = false) {
|
||||||
|
// [정책] 오버 이동(verifyMoveBoundary='crossed') 처리:
|
||||||
|
// - roofLine 은 그대로, wall.baseLines 는 이미 오버된 좌표로 mutate 된 상태.
|
||||||
|
// - 보조선 싹 지우고 '오버된 wall.baseLines 좌표 그대로' SK 재빌드.
|
||||||
|
// - 일반 경로(drawSkeletonRidgeRoof) 는 45도 대각 확장/roof 경계 클램핑이 들어가
|
||||||
|
// 오버 polygon 이 왜곡 → 용마루 지붕 케이스는 전용 함수(drawSkeletonRidgeRoofFromBaseLines)로 분기.
|
||||||
|
// - forceRedraw 인자는 명시적 수동 호출 의도 표현 용도로만 유지.
|
||||||
|
let __verdict = 'unknown'
|
||||||
|
try {
|
||||||
|
__verdict = verifyMoveBoundary(this.id, this.canvas)
|
||||||
|
if (__verdict === 'crossed') {
|
||||||
|
console.warn(`[drawHelpLine] 경계 넘음(crossed) → 오버 전용 경로(drawSkeletonRidgeRoofFromBaseLines) 사용 (forceRedraw=${forceRedraw})`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[drawHelpLine] verifyMoveBoundary 예외 → 기존 흐름 진행:', e)
|
||||||
|
}
|
||||||
|
|
||||||
/* innerLines 초기화 */
|
/* innerLines 초기화 */
|
||||||
this.canvas
|
this.canvas
|
||||||
.getObjects()
|
.getObjects()
|
||||||
@ -479,8 +578,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
|||||||
|
|
||||||
if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) {
|
if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) {
|
||||||
// 용마루 -- straight-skeleton
|
// 용마루 -- straight-skeleton
|
||||||
// console.log('용마루 지붕')
|
// 오버(crossed) 이동 시 wall.baseLines 좌표 그대로 사용하는 전용 경로로 분기.
|
||||||
drawSkeletonRidgeRoof(this.id, this.canvas, textMode)
|
if (__verdict === 'crossed') {
|
||||||
|
drawSkeletonRidgeRoofFromBaseLines(this.id, this.canvas, textMode)
|
||||||
|
} else {
|
||||||
|
drawSkeletonRidgeRoof(this.id, this.canvas, textMode)
|
||||||
|
}
|
||||||
} else if (isGableRoof(types)) {
|
} else if (isGableRoof(types)) {
|
||||||
// A형, B형 박공 지붕
|
// A형, B형 박공 지붕
|
||||||
// console.log('패턴 지붕')
|
// console.log('패턴 지붕')
|
||||||
@ -492,6 +595,22 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
|||||||
// console.log('변별로 설정')
|
// console.log('변별로 설정')
|
||||||
drawRoofByAttribute(this.id, this.canvas, textMode)
|
drawRoofByAttribute(this.id, this.canvas, textMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [DEBUG] 로컬(NEXT_PUBLIC_RUN_MODE==='local') 에서만 라벨 부착. 그 외 즉시 return.
|
||||||
|
__attachDebugLabels(this.canvas, this.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 오버 이동(crossed) 상태여도 wall.baseLines / roof.lines / outerLine / lengthText 는 그대로 두고
|
||||||
|
* 보조선(innerLines: eaveHelpLine, HIP, extensionLine, SK ridge 등) 만 제거 후 SK 재빌드.
|
||||||
|
*
|
||||||
|
* drawHelpLine 의 진입부 verifyMoveBoundary 가드만 우회하는 wrapper. 이후 흐름은 동일.
|
||||||
|
* - 초기화 필터(line 405-411) 가 이미 wall/roof/baseLine/outerLine/lengthText 를 제외하고 있어
|
||||||
|
* 해당 객체들은 보존됨.
|
||||||
|
* - 사용자가 오버된 상태에서 "SK 만 다시 그려보기" 의도로 명시적으로 호출할 때 사용.
|
||||||
|
*/
|
||||||
|
redrawHelpLineForced(settingModalFirstOptions) {
|
||||||
|
return this.drawHelpLine(settingModalFirstOptions, true)
|
||||||
},
|
},
|
||||||
|
|
||||||
addLengthText() {
|
addLengthText() {
|
||||||
|
|||||||
@ -455,6 +455,7 @@ export default function CanvasMenu(props) {
|
|||||||
if (createUser && tempFlg && lockFlg) {
|
if (createUser && tempFlg && lockFlg) {
|
||||||
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
||||||
setAllButtonStyles('none')
|
setAllButtonStyles('none')
|
||||||
|
setCopyButtonStyle('')
|
||||||
} else {
|
} else {
|
||||||
handleButtonStyles(tempFlg, lockFlg, docNo)
|
handleButtonStyles(tempFlg, lockFlg, docNo)
|
||||||
}
|
}
|
||||||
@ -516,6 +517,7 @@ export default function CanvasMenu(props) {
|
|||||||
if (createUser && tempFlg && lockFlg) {
|
if (createUser && tempFlg && lockFlg) {
|
||||||
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
if (createUser === 'T01' && sessionState.storeId !== 'T01') {
|
||||||
setAllButtonStyles('none')
|
setAllButtonStyles('none')
|
||||||
|
setCopyButtonStyle('')
|
||||||
} else {
|
} else {
|
||||||
setEstimateContextState({
|
setEstimateContextState({
|
||||||
tempFlg: estimateRecoilState.tempFlg,
|
tempFlg: estimateRecoilState.tempFlg,
|
||||||
@ -693,7 +695,7 @@ export default function CanvasMenu(props) {
|
|||||||
<span className="name">{getMessage('plan.menu.estimate.reset')}</span>
|
<span className="name">{getMessage('plan.menu.estimate.reset')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{estimateRecoilState?.docNo !== null && (sessionState.storeId === 'T01' || sessionState.storeLvl === '1') && (
|
{estimateRecoilState?.docNo !== null && (sessionState.storeId === 'T01' || sessionState.storeLvl) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
style={{ display: copyButtonStyle }}
|
style={{ display: copyButtonStyle }}
|
||||||
|
|||||||
@ -58,7 +58,6 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
const { setModuleStatisticsData } = useCircuitTrestle()
|
const { setModuleStatisticsData } = useCircuitTrestle()
|
||||||
const { handleCanvasToPng } = useImgLoader()
|
const { handleCanvasToPng } = useImgLoader()
|
||||||
const moduleSelectionData = useRecoilValue(moduleSelectionDataState)
|
const moduleSelectionData = useRecoilValue(moduleSelectionDataState)
|
||||||
const passivityCircuitAllocationRef = useRef()
|
|
||||||
const { setIsGlobalLoading } = useContext(QcastContext)
|
const { setIsGlobalLoading } = useContext(QcastContext)
|
||||||
|
|
||||||
const originCanvasViewPortTransform = useRef([])
|
const originCanvasViewPortTransform = useRef([])
|
||||||
@ -687,7 +686,18 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
.map((obj) => {
|
.map((obj) => {
|
||||||
obj.pcses = getStepUpListData()
|
obj.pcses = getStepUpListData()
|
||||||
})
|
})
|
||||||
await capture(1)
|
// 캡처 실패 시 에러 팝업 후 중단 (다시 회로구성 가능)
|
||||||
|
try {
|
||||||
|
await capture(1)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('🚀 ~ capture(1) error:', e)
|
||||||
|
setIsGlobalLoading(false)
|
||||||
|
swalFire({
|
||||||
|
text: getMessage('common.message.send.error'),
|
||||||
|
icon: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
//회로할당 저장 시 result=null인 경우에도 회로번호 텍스트 표시 유지 처리
|
//회로할당 저장 시 result=null인 경우에도 회로번호 텍스트 표시 유지 처리
|
||||||
|
|
||||||
@ -821,9 +831,23 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
|
|
||||||
// 승압설정 목록 조회
|
// 승압설정 목록 조회
|
||||||
const getStepUpListData = () => {
|
const getStepUpListData = () => {
|
||||||
|
// PCS 인스턴스별 실제 담당 모듈 타입 집합 수집 (Cross Mix DC3 접속함 분리용)
|
||||||
|
const pcsModuleTpCds = {}
|
||||||
|
canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.circuit && obj.pcsItemId)
|
||||||
|
.forEach((module) => {
|
||||||
|
const pcsInstanceId = module.circuit?.circuitInfo?.id
|
||||||
|
const tpCd = module.moduleInfo?.moduleTpCd
|
||||||
|
if (!pcsInstanceId || !tpCd) return
|
||||||
|
if (!pcsModuleTpCds[pcsInstanceId]) pcsModuleTpCds[pcsInstanceId] = new Set()
|
||||||
|
pcsModuleTpCds[pcsInstanceId].add(tpCd)
|
||||||
|
})
|
||||||
|
|
||||||
const pcs = []
|
const pcs = []
|
||||||
console.log(stepUpListData)
|
console.log(stepUpListData)
|
||||||
stepUpListData[0].pcsItemList.map((item, index) => {
|
stepUpListData[0].pcsItemList.map((item, index) => {
|
||||||
|
const targetTpCds = pcsModuleTpCds[selectedModels[index]?.id]
|
||||||
return item.serQtyList
|
return item.serQtyList
|
||||||
.filter((serQty) => serQty.selected && serQty.paralQty > 0)
|
.filter((serQty) => serQty.selected && serQty.paralQty > 0)
|
||||||
.forEach((serQty) => {
|
.forEach((serQty) => {
|
||||||
@ -833,9 +857,19 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
pcsItemId: item.itemId,
|
pcsItemId: item.itemId,
|
||||||
pscOptCd: getPcsOptCd(index),
|
pscOptCd: getPcsOptCd(index),
|
||||||
paralQty: serQty.paralQty,
|
paralQty: serQty.paralQty,
|
||||||
// 접속함 중복 체크: 동일 itemId는 1개만, 다르면 각각 올림
|
// 접속함: PCS가 실제 담당하는 모듈 타입으로 필터 후 itemId 기준 dedupe
|
||||||
|
// (Cross Mix 시 다른 모듈 계열의 conn 제외 / 단일·동계열 Mix는 기존과 동일 결과)
|
||||||
connections: item.connList?.length
|
connections: item.connList?.length
|
||||||
? [...new Map(item.connList.map((conn) => [conn.itemId, { connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt }])).values()]
|
? [
|
||||||
|
...new Map(
|
||||||
|
item.connList
|
||||||
|
.filter((conn) => !targetTpCds || !conn.moduleTpCd || targetTpCds.has(conn.moduleTpCd))
|
||||||
|
.map((conn) => [
|
||||||
|
conn.itemId,
|
||||||
|
{ connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt, moduleTpCd: conn.moduleTpCd },
|
||||||
|
]),
|
||||||
|
).values(),
|
||||||
|
]
|
||||||
: [],
|
: [],
|
||||||
})
|
})
|
||||||
// return {
|
// return {
|
||||||
@ -1030,7 +1064,7 @@ export default function CircuitTrestleSetting({ id }) {
|
|||||||
</div>
|
</div>
|
||||||
{tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && <PowerConditionalSelect {...powerConditionalSelectProps} />}
|
{tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && <PowerConditionalSelect {...powerConditionalSelectProps} />}
|
||||||
{tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && (
|
{tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && (
|
||||||
<PassivityCircuitAllocation {...passivityProps} ref={passivityCircuitAllocationRef} isFold={isFold} />
|
<PassivityCircuitAllocation {...passivityProps} isFold={isFold} />
|
||||||
)}
|
)}
|
||||||
{tabNum === 2 && <StepUp {...stepUpProps} onInitialize={handleStepUpInitialize} />}
|
{tabNum === 2 && <StepUp {...stepUpProps} onInitialize={handleStepUpInitialize} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -134,9 +134,7 @@ export default function StepUp(props) {
|
|||||||
const moduleIds = targetSurface.modules.map((module) => module.id)
|
const moduleIds = targetSurface.modules.map((module) => module.id)
|
||||||
|
|
||||||
/** 기존 모듈 텍스트 삭제 */
|
/** 기존 모듈 텍스트 삭제 */
|
||||||
canvas
|
;[...canvas.getObjects().filter((obj) => moduleIds.includes(obj.parentId))]
|
||||||
.getObjects()
|
|
||||||
.filter((obj) => moduleIds.includes(obj.parentId))
|
|
||||||
.forEach((text) => canvas.remove(text))
|
.forEach((text) => canvas.remove(text))
|
||||||
|
|
||||||
/** 새로운 모듈 회로 정보 추가 */
|
/** 새로운 모듈 회로 정보 추가 */
|
||||||
@ -347,6 +345,7 @@ export default function StepUp(props) {
|
|||||||
itemId: conn.itemId ?? '',
|
itemId: conn.itemId ?? '',
|
||||||
itemNm: conn.itemNm ?? '',
|
itemNm: conn.itemNm ?? '',
|
||||||
vstuParalCnt: conn.vstuParalCnt ?? 0,
|
vstuParalCnt: conn.vstuParalCnt ?? 0,
|
||||||
|
moduleTpCd: conn.moduleTpCd,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -410,61 +409,100 @@ export default function StepUp(props) {
|
|||||||
* 주어진 stepUpListData 를 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다.
|
* 주어진 stepUpListData 를 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다.
|
||||||
* 모든 pcsItem 의 selected serQty 를 순회해 적용한다.
|
* 모든 pcsItem 의 selected serQty 를 순회해 적용한다.
|
||||||
*/
|
*/
|
||||||
const applyCircuitsToCanvas = (stepUpListSrc) => {
|
/**
|
||||||
|
* 모듈에 circuit 텍스트를 추가하는 공통 함수
|
||||||
|
*/
|
||||||
|
const addCircuitTextToModule = (module) => {
|
||||||
|
const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId)
|
||||||
|
if (targetModule && module.circuit !== '' && module.circuit !== null) {
|
||||||
|
const moduleCircuitText = new fabric.Text(module.circuit, {
|
||||||
|
left: targetModule.left + targetModule.width / 2,
|
||||||
|
top: targetModule.top + targetModule.height / 2,
|
||||||
|
fontFamily: circuitNumberText.fontFamily.value,
|
||||||
|
fontWeight: circuitNumberText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal',
|
||||||
|
fontStyle: circuitNumberText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal',
|
||||||
|
fontSize: circuitNumberText.fontSize.value,
|
||||||
|
fill: circuitNumberText.fontColor.value,
|
||||||
|
width: targetModule.width,
|
||||||
|
height: targetModule.height,
|
||||||
|
textAlign: 'center',
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'center',
|
||||||
|
name: 'circuitNumber',
|
||||||
|
parentId: targetModule.id,
|
||||||
|
circuitInfo: module.pcsItemId,
|
||||||
|
visible: isDisplayCircuitNumber,
|
||||||
|
})
|
||||||
|
targetModule.circuit = moduleCircuitText
|
||||||
|
targetModule.pcsItemId = module.pcsItemId
|
||||||
|
targetModule.circuitNumber = module.circuit
|
||||||
|
canvas.add(moduleCircuitText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyCircuitsToCanvas = (stepUpListSrc, mainIdx, subIdx) => {
|
||||||
if (!stepUpListSrc?.[0]?.pcsItemList) return
|
if (!stepUpListSrc?.[0]?.pcsItemList) return
|
||||||
|
|
||||||
/** 모든 모듈 circuit 데이터 초기화 */
|
if (mainIdx >= 1) {
|
||||||
canvas
|
/**
|
||||||
.getObjects()
|
* 우측 PCS (mainIdx >= 1) 클릭 시:
|
||||||
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
* 해당 pcsItem 의 selected serQty 를 찾아서 moduleList 의 circuit 만 갱신한다.
|
||||||
.forEach((module) => {
|
* 좌측 PCS 의 circuit 은 그대로 유지.
|
||||||
|
*/
|
||||||
|
const pcsItem = stepUpListSrc[0].pcsItemList[mainIdx]
|
||||||
|
const sel = pcsItem?.serQtyList?.find((sq) => sq.selected)
|
||||||
|
if (!sel) return
|
||||||
|
|
||||||
|
// 해당 pcsItem 의 moduleList 에 포함된 모듈의 기존 circuit 텍스트만 제거
|
||||||
|
const moduleUniqueIds = new Set()
|
||||||
|
sel.roofSurfaceList.forEach((rs) => {
|
||||||
|
rs.moduleList.forEach((m) => moduleUniqueIds.add(m.uniqueId))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 해당 모듈의 circuit 데이터 초기화 + 기존 텍스트 제거
|
||||||
|
const targetModuleObjs = canvas.getObjects().filter(
|
||||||
|
(obj) => obj.name === POLYGON_TYPE.MODULE && moduleUniqueIds.has(obj.id),
|
||||||
|
)
|
||||||
|
targetModuleObjs.forEach((module) => {
|
||||||
module.circuit = null
|
module.circuit = null
|
||||||
module.circuitNumber = null
|
module.circuitNumber = null
|
||||||
module.pcsItemId = null
|
module.pcsItemId = null
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 기존 circuit 텍스트 객체 모두 제거 */
|
const textsToRemove = [...canvas.getObjects().filter(
|
||||||
canvas
|
(obj) => obj.name === 'circuitNumber' && moduleUniqueIds.has(obj.parentId),
|
||||||
.getObjects()
|
)]
|
||||||
.filter((obj) => obj.name === 'circuitNumber')
|
textsToRemove.forEach((text) => canvas.remove(text))
|
||||||
.forEach((text) => canvas.remove(text))
|
|
||||||
|
|
||||||
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
|
// 새 circuit 적용
|
||||||
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
|
|
||||||
if (!sel) return
|
|
||||||
sel.roofSurfaceList.forEach((roofSurface) => {
|
sel.roofSurfaceList.forEach((roofSurface) => {
|
||||||
const targetSurface = canvas.getObjects().filter((obj) => obj.id === roofSurface.roofSurfaceId)[0]
|
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
|
||||||
if (!targetSurface) return
|
})
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* 좌측 PCS (mainIdx === 0) 클릭 시:
|
||||||
|
* 전체 모듈 circuit 초기화 후 모든 pcsItem 의 selected 를 적용
|
||||||
|
*/
|
||||||
|
canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
|
||||||
|
.forEach((module) => {
|
||||||
|
module.circuit = null
|
||||||
|
module.circuitNumber = null
|
||||||
|
module.pcsItemId = null
|
||||||
|
})
|
||||||
|
|
||||||
roofSurface.moduleList.forEach((module) => {
|
const circuitTexts = [...canvas.getObjects().filter((obj) => obj.name === 'circuitNumber')]
|
||||||
const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId)
|
circuitTexts.forEach((text) => canvas.remove(text))
|
||||||
if (targetModule && module.circuit !== '' && module.circuit !== null) {
|
|
||||||
const moduleCircuitText = new fabric.Text(module.circuit, {
|
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
|
||||||
left: targetModule.left + targetModule.width / 2,
|
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
|
||||||
top: targetModule.top + targetModule.height / 2,
|
if (!sel) return
|
||||||
fontFamily: circuitNumberText.fontFamily.value,
|
sel.roofSurfaceList.forEach((roofSurface) => {
|
||||||
fontWeight: circuitNumberText.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal',
|
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
|
||||||
fontStyle: circuitNumberText.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal',
|
|
||||||
fontSize: circuitNumberText.fontSize.value,
|
|
||||||
fill: circuitNumberText.fontColor.value,
|
|
||||||
width: targetModule.width,
|
|
||||||
height: targetModule.height,
|
|
||||||
textAlign: 'center',
|
|
||||||
originX: 'center',
|
|
||||||
originY: 'center',
|
|
||||||
name: 'circuitNumber',
|
|
||||||
parentId: targetModule.id,
|
|
||||||
circuitInfo: module.pcsItemId,
|
|
||||||
visible: isDisplayCircuitNumber,
|
|
||||||
})
|
|
||||||
targetModule.circuit = moduleCircuitText
|
|
||||||
targetModule.pcsItemId = module.pcsItemId
|
|
||||||
targetModule.circuitNumber = module.circuit
|
|
||||||
canvas.add(moduleCircuitText)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
setModuleStatisticsData()
|
setModuleStatisticsData()
|
||||||
@ -483,8 +521,7 @@ export default function StepUp(props) {
|
|||||||
setStepUpListData(tempStepUpListData)
|
setStepUpListData(tempStepUpListData)
|
||||||
|
|
||||||
/** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */
|
/** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */
|
||||||
const needsRefetch =
|
const needsRefetch = tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
|
||||||
tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
|
|
||||||
|
|
||||||
if (needsRefetch) {
|
if (needsRefetch) {
|
||||||
/** 파워컨디셔너 옵션 조회 요청 파라미터 */
|
/** 파워컨디셔너 옵션 조회 요청 파라미터 */
|
||||||
@ -522,10 +559,10 @@ export default function StepUp(props) {
|
|||||||
const dataArray = Array.isArray(res.data) ? res.data : [res.data]
|
const dataArray = Array.isArray(res.data) ? res.data : [res.data]
|
||||||
const newStepUpListData = formatStepUpListData(dataArray)
|
const newStepUpListData = formatStepUpListData(dataArray)
|
||||||
setStepUpListData(newStepUpListData)
|
setStepUpListData(newStepUpListData)
|
||||||
applyCircuitsToCanvas(newStepUpListData)
|
applyCircuitsToCanvas(newStepUpListData, mainIdx, subIdx)
|
||||||
} else {
|
} else {
|
||||||
swalFire({ text: getMessage('common.message.send.error') })
|
swalFire({ text: getMessage('common.message.send.error') })
|
||||||
applyCircuitsToCanvas(tempStepUpListData)
|
applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@ -533,14 +570,28 @@ export default function StepUp(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */
|
/** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */
|
||||||
applyCircuitsToCanvas(tempStepUpListData)
|
applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 현재 선택된 값들을 가져오는 함수 추가
|
* 현재 선택된 값들을 가져오는 함수 추가
|
||||||
*/
|
*/
|
||||||
const getCurrentSelections = () => {
|
const getCurrentSelections = () => {
|
||||||
const selectedValues = stepUpListData[0].pcsItemList.forEach((item) => {
|
// PCS 인스턴스별 실제 담당 모듈 타입 집합 수집 (Cross Mix DC3 접속함 분리용)
|
||||||
|
const pcsModuleTpCds = {}
|
||||||
|
canvas
|
||||||
|
.getObjects()
|
||||||
|
.filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.circuit && obj.pcsItemId)
|
||||||
|
.forEach((module) => {
|
||||||
|
const pcsInstanceId = module.circuit?.circuitInfo?.id
|
||||||
|
const tpCd = module.moduleInfo?.moduleTpCd
|
||||||
|
if (!pcsInstanceId || !tpCd) return
|
||||||
|
if (!pcsModuleTpCds[pcsInstanceId]) pcsModuleTpCds[pcsInstanceId] = new Set()
|
||||||
|
pcsModuleTpCds[pcsInstanceId].add(tpCd)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedValues = stepUpListData[0].pcsItemList.forEach((item, index) => {
|
||||||
|
const targetTpCds = pcsModuleTpCds[selectedModels[index]?.id]
|
||||||
item.serQtyList.filter((serQty) => serQty.selected)
|
item.serQtyList.filter((serQty) => serQty.selected)
|
||||||
return item.serQtyList.map((serQty) => {
|
return item.serQtyList.map((serQty) => {
|
||||||
return {
|
return {
|
||||||
@ -549,9 +600,23 @@ export default function StepUp(props) {
|
|||||||
pcsItemId: serQty.itemId,
|
pcsItemId: serQty.itemId,
|
||||||
pcsOptCd: seletedOption,
|
pcsOptCd: seletedOption,
|
||||||
paralQty: serQty.paralQty,
|
paralQty: serQty.paralQty,
|
||||||
// 접속함 중복 체크: 동일 itemId는 1개만, 다르면 각각 올림
|
// 접속함: PCS가 실제 담당하는 모듈 타입으로 필터 후 itemId 기준 dedupe
|
||||||
|
// (Cross Mix 시 다른 모듈 계열의 conn 제외 / 단일·동계열 Mix는 기존과 동일 결과)
|
||||||
connections: item.connList?.length
|
connections: item.connList?.length
|
||||||
? [...new Map(item.connList.map((conn) => [conn.itemId, { connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt }])).values()]
|
? [
|
||||||
|
...new Map(
|
||||||
|
item.connList
|
||||||
|
.filter((conn) => !targetTpCds || !conn.moduleTpCd || targetTpCds.has(conn.moduleTpCd))
|
||||||
|
.map((conn) => [
|
||||||
|
conn.itemId,
|
||||||
|
{
|
||||||
|
connItemId: conn.itemId,
|
||||||
|
connMaxParalCnt: conn.connMaxParalCnt,
|
||||||
|
moduleTpCd: conn.moduleTpCd,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
).values(),
|
||||||
|
]
|
||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import Angle from '@/components/floor-plan/modal/lineTypes/Angle'
|
|||||||
import DoublePitch from '@/components/floor-plan/modal/lineTypes/DoublePitch'
|
import DoublePitch from '@/components/floor-plan/modal/lineTypes/DoublePitch'
|
||||||
import Diagonal from '@/components/floor-plan/modal/lineTypes/Diagonal'
|
import Diagonal from '@/components/floor-plan/modal/lineTypes/Diagonal'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -18,6 +19,7 @@ export default function WallLineSetting(props) {
|
|||||||
const { id } = props
|
const { id } = props
|
||||||
const { addPopup, closePopup } = usePopup()
|
const { addPopup, closePopup } = usePopup()
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
|
const { swalFire } = useSwal()
|
||||||
const [propertiesId, setPropertiesId] = useState(uuidv4())
|
const [propertiesId, setPropertiesId] = useState(uuidv4())
|
||||||
const [useCalcPad, setUseCalcPad] = useState(false)
|
const [useCalcPad, setUseCalcPad] = useState(false)
|
||||||
const {
|
const {
|
||||||
@ -182,10 +184,11 @@ export default function WallLineSetting(props) {
|
|||||||
<button
|
<button
|
||||||
className="btn-frame modal act"
|
className="btn-frame modal act"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFix()
|
swalFire({
|
||||||
// closePopup(id)
|
type: 'confirm',
|
||||||
|
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||||
// setShowPropertiesSettingModal(true)
|
confirmFn: handleFix,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{getMessage('modal.cover.outline.fix')}
|
{getMessage('modal.cover.outline.fix')}
|
||||||
|
|||||||
@ -9,11 +9,13 @@ import { OUTER_LINE_TYPE } from '@/store/outerLineAtom'
|
|||||||
import OuterLineWall from '@/components/floor-plan/modal/lineTypes/OuterLineWall'
|
import OuterLineWall from '@/components/floor-plan/modal/lineTypes/OuterLineWall'
|
||||||
import { usePlacementShapeDrawing } from '@/hooks/surface/usePlacementShapeDrawing'
|
import { usePlacementShapeDrawing } from '@/hooks/surface/usePlacementShapeDrawing'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
|
||||||
export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) {
|
export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) {
|
||||||
const { getMessage } = useMessage()
|
const { getMessage } = useMessage()
|
||||||
const { closePopup } = usePopup()
|
const { closePopup } = usePopup()
|
||||||
|
const { swalFire } = useSwal()
|
||||||
const [buttonAct, setButtonAct] = useState(1)
|
const [buttonAct, setButtonAct] = useState(1)
|
||||||
const [useCalcPad, setUseCalcPad] = useState(false)
|
const [useCalcPad, setUseCalcPad] = useState(false)
|
||||||
const types = [
|
const types = [
|
||||||
@ -155,7 +157,16 @@ export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) {
|
|||||||
<button className="btn-frame modal mr5" onClick={handleRollback}>
|
<button className="btn-frame modal mr5" onClick={handleRollback}>
|
||||||
{getMessage('modal.cover.outline.rollback')}
|
{getMessage('modal.cover.outline.rollback')}
|
||||||
</button>
|
</button>
|
||||||
<button className="btn-frame modal act" onClick={handleFix}>
|
<button
|
||||||
|
className="btn-frame modal act"
|
||||||
|
onClick={() => {
|
||||||
|
swalFire({
|
||||||
|
type: 'confirm',
|
||||||
|
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||||
|
confirmFn: handleFix,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
{getMessage('modal.placement.surface.drawing.fix')}
|
{getMessage('modal.placement.surface.drawing.fix')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -264,6 +264,8 @@ export function useMasterController() {
|
|||||||
*/
|
*/
|
||||||
const getPcsAutoRecommendList = async (params = null) => {
|
const getPcsAutoRecommendList = async (params = null) => {
|
||||||
return await post({ url: '/api/v1/master/getPcsAutoRecommendList', data: params }).then((res) => {
|
return await post({ url: '/api/v1/master/getPcsAutoRecommendList', data: params }).then((res) => {
|
||||||
|
console.log('[moduleTpCd] getPcsAutoRecommendList req:', params)
|
||||||
|
console.log('[moduleTpCd] getPcsAutoRecommendList res pcsItemList:', res?.data?.pcsItemList?.map((p) => ({ itemId: p.itemId, pcsTpCd: p.pcsTpCd, connList: p.connList?.map((c) => ({ itemId: c.itemId, moduleTpCd: c.moduleTpCd })) })))
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -286,6 +288,8 @@ export function useMasterController() {
|
|||||||
*/
|
*/
|
||||||
const getPcsVoltageChk = async (params = null) => {
|
const getPcsVoltageChk = async (params = null) => {
|
||||||
return await post({ url: '/api/v1/master/getPcsVoltageChk', data: params }).then((res) => {
|
return await post({ url: '/api/v1/master/getPcsVoltageChk', data: params }).then((res) => {
|
||||||
|
console.log('[moduleTpCd] getPcsVoltageChk req:', params)
|
||||||
|
console.log('[moduleTpCd] getPcsVoltageChk res pcsItemList:', res?.data?.pcsItemList?.map((p) => ({ itemId: p.itemId, pcsTpCd: p.pcsTpCd, connList: p.connList?.map((c) => ({ itemId: c.itemId, moduleTpCd: c.moduleTpCd })) })))
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -322,11 +326,15 @@ export function useMasterController() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await post({ url: '/api/v1/master/getPcsVoltageStepUpList', data: params }).then((res) => {
|
return await post({ url: '/api/v1/master/getPcsVoltageStepUpList', data: params }).then((res) => {
|
||||||
|
console.log('[moduleTpCd] getPcsVoltageStepUpList req:', params)
|
||||||
|
console.log('[moduleTpCd] getPcsVoltageStepUpList res pcsItemList:', res?.data?.pcsItemList?.map((p) => ({ itemId: p.itemId, pcsTpCd: p.pcsTpCd, connList: p.connList?.map((c) => ({ itemId: c.itemId, moduleTpCd: c.moduleTpCd })) })))
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getQuotationItem = async (params) => {
|
const getQuotationItem = async (params) => {
|
||||||
|
console.log('[moduleTpCd] getQuotationItem req pcses.connections:', params?.pcses?.map((p) => ({ pcsItemId: p.pcsItemId, connections: p.connections })))
|
||||||
|
console.log('[moduleTpCd] getQuotationItem full req:', params)
|
||||||
return await post({ url: '/api/v1/master/getQuotationItem', data: params })
|
return await post({ url: '/api/v1/master/getQuotationItem', data: params })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return res
|
return res
|
||||||
@ -355,6 +363,8 @@ export function useMasterController() {
|
|||||||
*/
|
*/
|
||||||
const getPcsConnOptionItemList = async (params = null) => {
|
const getPcsConnOptionItemList = async (params = null) => {
|
||||||
return await post({ url: '/api/v1/master/getPcsConnOptionItemList', data: params }).then((res) => {
|
return await post({ url: '/api/v1/master/getPcsConnOptionItemList', data: params }).then((res) => {
|
||||||
|
console.log('[moduleTpCd] getPcsConnOptionItemList req:', params)
|
||||||
|
console.log('[moduleTpCd] getPcsConnOptionItemList res pcsItemList:', res?.data?.pcsItemList?.map((p) => ({ itemId: p.itemId, pcsTpCd: p.pcsTpCd, connList: p.connList?.map((c) => ({ itemId: c.itemId, moduleTpCd: c.moduleTpCd })) })))
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,18 @@ export function useImgLoader() {
|
|||||||
|
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
|
|
||||||
|
// 렌더링 완료 대기 (최대 3초, 초과 시 에러)
|
||||||
|
const renderReady = await new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(() => resolve(false), 3000)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!renderReady) {
|
||||||
|
throw new Error('RENDER_TIMEOUT')
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
// 3. 도면 오브젝트의 바운딩 박스 계산
|
// 3. 도면 오브젝트의 바운딩 박스 계산
|
||||||
@ -164,8 +176,9 @@ export function useImgLoader() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
canvas.backgroundColor = originalBg
|
canvas.backgroundColor = originalBg
|
||||||
canvas.renderAll()
|
canvas.renderAll()
|
||||||
setIsGlobalLoading(false)
|
toggleLineEtc(true)
|
||||||
console.log('🚀 ~ handleCanvasToPng ~ e:', e)
|
console.log('🚀 ~ handleCanvasToPng ~ e:', e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2601,8 +2601,16 @@ export const useTrestle = () => {
|
|||||||
const { constTp } = moduleSelection.construction
|
const { constTp } = moduleSelection.construction
|
||||||
const { addRoof } = moduleSelection
|
const { addRoof } = moduleSelection
|
||||||
|
|
||||||
|
// 서브모듈 코드 판별: 서로 다른 계열 혼합(예: DC3=D+C3)일 때만 서브모듈 코드 세팅
|
||||||
|
// 단일(C3) 또는 같은 계열 혼합(C1C2C3)은 빈 문자열
|
||||||
|
const subCodes = module.itemTp?.match(/[A-Z]\d*/g) || []
|
||||||
|
const uniquePrefixes = new Set(subCodes.map((code) => code.charAt(0)))
|
||||||
|
const isDifferentSeriesMix = subCodes.length > 1 && uniquePrefixes.size > 1
|
||||||
|
const subModuleTpCd = isDifferentSeriesMix ? surface.modules?.[0]?.moduleInfo?.moduleTpCd || '' : ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
moduleTpCd: module.itemTp,
|
moduleTpCd: module.itemTp,
|
||||||
|
subModuleTpCd,
|
||||||
roofMatlCd: parent.roofMaterial.roofMatlCd,
|
roofMatlCd: parent.roofMaterial.roofMatlCd,
|
||||||
mixMatlNo: module.mixMatlNo,
|
mixMatlNo: module.mixMatlNo,
|
||||||
raftBaseCd: addRoof.raft ?? addRoof.raftBaseCd,
|
raftBaseCd: addRoof.raft ?? addRoof.raftBaseCd,
|
||||||
|
|||||||
@ -11,6 +11,14 @@ import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
|||||||
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
||||||
import { useMouse } from '@/hooks/useMouse'
|
import { useMouse } from '@/hooks/useMouse'
|
||||||
|
|
||||||
|
// [정책 flag] 오버 이동(인접 라인 반전 = polygon self-cross) 입력 처리 방법.
|
||||||
|
// 'clamp' : (기본/2026-04-24 최종) 평행 위치-OVER_EPS 까지 클램핑. wall.baseLines 는 항상 정상 polygon, SK 확장 정상.
|
||||||
|
// 'reject' : (2026-04-22 정책) silent skip — 오버면 해당 target 의 mutation 자체를 건너뜀. 토스트 없음.
|
||||||
|
// 'passthrough' : 가드 전면 스킵 — 사용자가 입력한 raw 값을 그대로 적용. (과거 crossed 허용 동작; polygon 왜곡 위험)
|
||||||
|
// 교체 방법: 이 상수만 변경. OVER_GUARD 분기가 자동 반응.
|
||||||
|
const OVER_MOVE_POLICY = 'clamp'
|
||||||
|
const OVER_EPS = 0.5
|
||||||
|
|
||||||
//동선이동 형 올림 내림
|
//동선이동 형 올림 내림
|
||||||
export function useMovementSetting(id) {
|
export function useMovementSetting(id) {
|
||||||
const TYPE = {
|
const TYPE = {
|
||||||
@ -746,6 +754,57 @@ export function useMovementSetting(id) {
|
|||||||
deltaX = value.toNumber()
|
deltaX = value.toNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [OVER_GUARD] 오버 이동(인접 라인 반전) 처리. 정책은 파일 상단 OVER_MOVE_POLICY 로 전환.
|
||||||
|
// 한계 계산 (공통):
|
||||||
|
// nLimit = (nextLine 의 free 끝점 좌표) - (currentLine 의 해당 끝점 좌표)
|
||||||
|
// pLimit = (prevLine 의 free 끝점 좌표) - (currentLine 의 해당 시작점 좌표)
|
||||||
|
// delta 와 같은 부호인 한계만 후보(반대 부호 한계는 인접 라인이 길어지는 방향).
|
||||||
|
// 정책별 동작:
|
||||||
|
// 'passthrough' → 가드 전체 스킵
|
||||||
|
// 'reject' → |delta| > |한계-EPS| 이면 해당 target 은 mutation 없이 건너뜀 (silent skip)
|
||||||
|
// 'clamp' → |delta| 를 |한계-EPS| 로 잘라 평행 직전까지 이동. roof.moveUpDown 도 동일 비율 동기화.
|
||||||
|
if (OVER_MOVE_POLICY !== 'passthrough') {
|
||||||
|
const __isHoriz = currentLine.y1 === currentLine.y2
|
||||||
|
const __raw = __isHoriz ? deltaY : deltaX
|
||||||
|
const __sgn = Math.sign(__raw)
|
||||||
|
if (__sgn !== 0) {
|
||||||
|
const __nLimit = __isHoriz ? (nextLine.y2 - currentLine.y2) : (nextLine.x2 - currentLine.x2)
|
||||||
|
const __pLimit = __isHoriz ? (prevLine.y1 - currentLine.y1) : (prevLine.x1 - currentLine.x1)
|
||||||
|
let __absLimit = Infinity
|
||||||
|
if (Math.sign(__nLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__nLimit))
|
||||||
|
if (Math.sign(__pLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__pLimit))
|
||||||
|
// [EPS] 평행 정확히 도달 시 인접 baseLine zero-length → polygon 중복 꼭짓점 → SkeletonBuilder 헛선 발생.
|
||||||
|
// OVER_EPS 여유 두어 인접 baseLine 길이 OVER_EPS(=0.5) 로 남기고 SHOULDER_ABSORBED(planeSize<1) 가 자연 흡수.
|
||||||
|
const __safeAbsLimit = Math.max(0, __absLimit - OVER_EPS)
|
||||||
|
const __isOver = Math.abs(__raw) > __safeAbsLimit + 0.001
|
||||||
|
|
||||||
|
if (__isOver && OVER_MOVE_POLICY === 'reject') {
|
||||||
|
// silent skip: mutation 자체 건너뜀. movingLineFromSkeleton 의 roof.points 확장도 막으려면 moveUpDown=0.
|
||||||
|
console.warn(
|
||||||
|
`[handleSave] OVER 거부(reject): ${__isHoriz ? 'deltaY' : 'deltaX'}=${__raw.toFixed(1)} > limit=${__safeAbsLimit.toFixed(1)} → target skip`
|
||||||
|
)
|
||||||
|
roof.moveUpDown = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OVER_MOVE_POLICY === 'clamp') {
|
||||||
|
const __limited = __sgn * Math.min(Math.abs(__raw), __safeAbsLimit)
|
||||||
|
if (Math.abs(__limited - __raw) > 0.001) {
|
||||||
|
console.warn(
|
||||||
|
`[handleSave] OVER 클램핑: ${__isHoriz ? 'deltaY' : 'deltaX'} ${__raw.toFixed(1)} → ${__limited.toFixed(1)} (nLimit=${__nLimit.toFixed(1)} pLimit=${__pLimit.toFixed(1)}, EPS=${OVER_EPS})`
|
||||||
|
)
|
||||||
|
if (__isHoriz) deltaY = __limited
|
||||||
|
else deltaX = __limited
|
||||||
|
// [SYNC roof.moveUpDown] movingLineFromSkeleton 은 roof.moveUpDown(mm) 로 roof.points 확장.
|
||||||
|
// delta 만 줄이고 moveUpDown 방치 시 wall 은 평행까지, roof.points 는 원본 양만큼 이동 →
|
||||||
|
// polygon 자기교차 → removeNonOrthogonalPoints 가 꼭짓점 축소 → "SK 가 wall 안에서 멈춤".
|
||||||
|
// __limited(cm) × 10 = mm.
|
||||||
|
roof.moveUpDown = Math.round(Math.abs(__limited) * 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
currentLine.set({
|
currentLine.set({
|
||||||
x1: currentLine.x1 + deltaX,
|
x1: currentLine.x1 + deltaX,
|
||||||
y1: currentLine.y1 + deltaY,
|
y1: currentLine.y1 + deltaY,
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import { calculateAngle } from '@/util/qpolygon-utils'
|
|||||||
import { fabric } from 'fabric'
|
import { fabric } from 'fabric'
|
||||||
import { outlineDisplaySelector } from '@/store/settingAtom'
|
import { outlineDisplaySelector } from '@/store/settingAtom'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import Big from 'big.js'
|
import Big from 'big.js'
|
||||||
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
||||||
import { useObject } from '@/hooks/useObject'
|
import { useObject } from '@/hooks/useObject'
|
||||||
@ -84,6 +86,8 @@ export function useOuterLineWall(id, propertiesId) {
|
|||||||
const arrow1Ref = useRef(arrow1)
|
const arrow1Ref = useRef(arrow1)
|
||||||
const arrow2Ref = useRef(arrow2)
|
const arrow2Ref = useRef(arrow2)
|
||||||
const { addPopup, closePopup } = usePopup()
|
const { addPopup, closePopup } = usePopup()
|
||||||
|
const { swalFire } = useSwal()
|
||||||
|
const { getMessage } = useMessage()
|
||||||
|
|
||||||
const setOuterLineFix = useSetRecoilState(outerLineFixState)
|
const setOuterLineFix = useSetRecoilState(outerLineFixState)
|
||||||
|
|
||||||
@ -929,7 +933,11 @@ export function useOuterLineWall(id, propertiesId) {
|
|||||||
|
|
||||||
const enterCheck = (e) => {
|
const enterCheck = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleFix()
|
swalFire({
|
||||||
|
type: 'confirm',
|
||||||
|
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||||
|
confirmFn: handleFix,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,8 @@ import {
|
|||||||
import { usePolygon } from '@/hooks/usePolygon'
|
import { usePolygon } from '@/hooks/usePolygon'
|
||||||
import { POLYGON_TYPE } from '@/common/common'
|
import { POLYGON_TYPE } from '@/common/common'
|
||||||
import { usePopup } from '@/hooks/usePopup'
|
import { usePopup } from '@/hooks/usePopup'
|
||||||
|
import { useSwal } from '@/hooks/useSwal'
|
||||||
|
import { useMessage } from '@/hooks/useMessage'
|
||||||
import { useSurfaceShapeBatch } from './useSurfaceShapeBatch'
|
import { useSurfaceShapeBatch } from './useSurfaceShapeBatch'
|
||||||
|
|
||||||
import { roofDisplaySelector } from '@/store/settingAtom'
|
import { roofDisplaySelector } from '@/store/settingAtom'
|
||||||
@ -48,6 +50,8 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
const { setSurfaceShapePattern } = useRoofFn()
|
const { setSurfaceShapePattern } = useRoofFn()
|
||||||
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
|
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
|
||||||
const { handleSelectableObjects } = useObject()
|
const { handleSelectableObjects } = useObject()
|
||||||
|
const { swalFire } = useSwal()
|
||||||
|
const { getMessage } = useMessage()
|
||||||
|
|
||||||
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
|
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
|
||||||
const adsorptionRange = useRecoilValue(adsorptionRangeState)
|
const adsorptionRange = useRecoilValue(adsorptionRangeState)
|
||||||
@ -1089,7 +1093,11 @@ export function usePlacementShapeDrawing(id) {
|
|||||||
|
|
||||||
const enterCheck = (e) => {
|
const enterCheck = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
handleFix()
|
swalFire({
|
||||||
|
type: 'confirm',
|
||||||
|
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||||
|
confirmFn: handleFix,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -70,6 +70,7 @@
|
|||||||
"modal.cover.outline.length": "長さ(mm)",
|
"modal.cover.outline.length": "長さ(mm)",
|
||||||
"modal.cover.outline.arrow": "方向(矢印)",
|
"modal.cover.outline.arrow": "方向(矢印)",
|
||||||
"modal.cover.outline.fix": "外壁線確定",
|
"modal.cover.outline.fix": "外壁線確定",
|
||||||
|
"modal.cover.outline.fix.confirm": "本当に確定しますか?確定後は修正できません。",
|
||||||
"modal.cover.outline.rollback": "前に戻る",
|
"modal.cover.outline.rollback": "前に戻る",
|
||||||
"modal.cover.outline.finish": "設定完了",
|
"modal.cover.outline.finish": "設定完了",
|
||||||
"common.setting.finish": "設定完了",
|
"common.setting.finish": "設定完了",
|
||||||
|
|||||||
@ -70,6 +70,7 @@
|
|||||||
"modal.cover.outline.length": "길이(mm)",
|
"modal.cover.outline.length": "길이(mm)",
|
||||||
"modal.cover.outline.arrow": "방향(화살표)",
|
"modal.cover.outline.arrow": "방향(화살표)",
|
||||||
"modal.cover.outline.fix": "외벽선 확정",
|
"modal.cover.outline.fix": "외벽선 확정",
|
||||||
|
"modal.cover.outline.fix.confirm": "정말로 확정하시겠습니까? 확정 후에는 수정할 수 없습니다.",
|
||||||
"modal.cover.outline.rollback": "일변전으로 돌아가기",
|
"modal.cover.outline.rollback": "일변전으로 돌아가기",
|
||||||
"modal.cover.outline.finish": "설정완료",
|
"modal.cover.outline.finish": "설정완료",
|
||||||
"common.setting.finish": "설정완료",
|
"common.setting.finish": "설정완료",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user