diff --git a/src/components/floor-plan/CanvasFrame.jsx b/src/components/floor-plan/CanvasFrame.jsx index 9441dc7c..20d6a348 100644 --- a/src/components/floor-plan/CanvasFrame.jsx +++ b/src/components/floor-plan/CanvasFrame.jsx @@ -33,6 +33,7 @@ import { compasDegAtom } from '@/store/orientationAtom' import { hotkeyStore } from '@/store/hotkeyAtom' import { usePopup } from '@/hooks/usePopup' import { outerLinePointsState } from '@/store/outerLineAtom' +import { canvasSettingState } from '@/store/canvasAtom' export default function CanvasFrame() { const canvasRef = useRef(null) @@ -59,6 +60,7 @@ export default function CanvasFrame() { const { basicSetting, fetchBasicSettings } = useCanvasSetting() const { selectedMenu, setSelectedMenu } = useCanvasMenu() const { initEvent } = useEvent() + const canvasSetting = useRecoilValue(canvasSettingState) const loadCanvas = () => { if (!canvas) return @@ -82,7 +84,12 @@ export default function CanvasFrame() { setSelectedMenu('module') }, 500) } else if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL).length > 0) { - setSelectedMenu('outline') + // canvasSetting.roofSizeSet에 따라 메뉴 결정 + if (canvasSetting?.roofSizeSet === '2') { + setSelectedMenu('surface') // 실측값입력 → surface + } else { + setSelectedMenu('outline') // 복시도입력 → outline + } } else { setTimeout(() => { setSelectedMenu('surface') @@ -121,7 +128,11 @@ export default function CanvasFrame() { if (currentCanvasPlan.planNo) { /* 약간의 지연을 줘서 roofMaterials가 로드될 시간을 확보 */ setTimeout(() => { - fetchBasicSettings(Number(currentCanvasPlan.planNo), null) + // 메뉴 이동 시 canvasSetting이 덮어쓰이는 것을 방지 + // 이미 canvasSetting에 roofSizeSet이 있으면 API 호출 건너뛰기 + if (!canvasSetting?.roofSizeSet) { + fetchBasicSettings(Number(currentCanvasPlan.planNo), null) + } }, 100) } }, [currentCanvasPlan, canvas]) diff --git a/src/components/floor-plan/CanvasMenu.jsx b/src/components/floor-plan/CanvasMenu.jsx index 97d878a0..be42c977 100644 --- a/src/components/floor-plan/CanvasMenu.jsx +++ b/src/components/floor-plan/CanvasMenu.jsx @@ -426,7 +426,7 @@ export default function CanvasMenu(props) { return ( (['2', '3'].includes(canvasSetting?.roofSizeSet) && menu.type === 'outline') || (selectedMenu === 'module' && ['placement', 'outline'].includes(menu.type)) || - (isExistModule() && ['placement', 'outline'].some((num) => num === menu.type)) || + (isExistModule() && canvasSetting?.roofSizeSet !== '1' && ['placement', 'outline'].some((num) => num === menu.type)) || (['estimate', 'simulation'].includes(selectedMenu) && ['placement', 'outline', 'surface'].includes(menu.type)) ) } diff --git a/src/components/floor-plan/FloorPlan.jsx b/src/components/floor-plan/FloorPlan.jsx index 417cc559..6098aec3 100644 --- a/src/components/floor-plan/FloorPlan.jsx +++ b/src/components/floor-plan/FloorPlan.jsx @@ -41,7 +41,7 @@ export default function FloorPlan({ children }) { promiseGet({ url: `/api/object/${objectNo}/detail` }).then((res) => { if (res.status === 200) { const { data } = res - console.log(data) + //console.log(data) let surfaceTypeValue if (res.data.surfaceType === 'Ⅲ・Ⅳ') { diff --git a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx index 02e9d171..c98b75f0 100644 --- a/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx +++ b/src/components/floor-plan/modal/circuitTrestle/CircuitTrestleSetting.jsx @@ -63,6 +63,7 @@ export default function CircuitTrestleSetting({ id }) { const originCanvasViewPortTransform = useRef([]) const [isFold, setIsFold] = useState(false) + const [showHiddenBasicSetting, setShowHiddenBasicSetting] = useState(true) const { makers, @@ -108,6 +109,14 @@ export default function CircuitTrestleSetting({ id }) { } }, []) + // 모듈/가대설정 팝업을 순간적으로 열었다가 닫아 초기화 로직 실행 + useEffect(() => { + const timer = setTimeout(() => { + setShowHiddenBasicSetting(false) + }, 300) + return () => clearTimeout(timer) + }, []) + // 모듈이 설치된 경우 rack설치 여부에 따라 설치 된 모듈 아래에 작은 모듈이 설치되어 있을 경우는 모듈 설치 메뉴 reopen useEffect(() => { const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE) @@ -998,63 +1007,70 @@ export default function CircuitTrestleSetting({ id }) { } return ( - - handleClose()} - isFold={isFold} - onFold={() => setIsFold(!isFold)} - /> - -
-
-
-
{getMessage('modal.circuit.trestle.setting.power.conditional.select')}
- -
- {getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')}) + <> + {showHiddenBasicSetting && ( +
+ +
+ )} + + handleClose()} + isFold={isFold} + onFold={() => setIsFold(!isFold)} + /> + +
+
+
+
{getMessage('modal.circuit.trestle.setting.power.conditional.select')}
+ +
+ {getMessage('modal.circuit.trestle.setting.circuit.allocation')}({getMessage('modal.circuit.trestle.setting.step.up.allocation')}) +
+ {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && } + {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( + + )} + {tabNum === 2 && }
- {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && } - {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( - - )} - {tabNum === 2 && } -
- {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && ( -
- - -
- )} - {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( -
- - -
- )} - {tabNum === 2 && ( -
- - {/* -
- )} - - + {tabNum === 1 && allocationType === ALLOCATION_TYPE.AUTO && ( +
+ + +
+ )} + {tabNum === 1 && allocationType === ALLOCATION_TYPE.PASSIVITY && ( +
+ + +
+ )} + {tabNum === 2 && ( +
+ + {/* +
+ )} + + + ) } diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index a537b065..87ad753c 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -16,7 +16,7 @@ import { globalLocaleStore } from '@/store/localeAtom' import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util' import { usePolygon } from '@/hooks/usePolygon' -import { canvasState, currentMenuState } from '@/store/canvasAtom' +import { canvasState, canvasSettingState, currentCanvasPlanState, currentMenuState } from '@/store/canvasAtom' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { MENU, POLYGON_TYPE } from '@/common/common' import { useRoofFn } from '@/hooks/common/useRoofFn' @@ -51,6 +51,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla const { drawDirectionArrow } = usePolygon() const { setSurfaceShapePattern } = useRoofFn() const canvas = useRecoilValue(canvasState) + const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState) + const currentCanvasPlan = useRecoilValue(currentCanvasPlanState) const roofDisplay = useRecoilValue(roofDisplaySelector) const { setPolygonLinesActualSize } = usePolygon() @@ -122,6 +124,15 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla if (openPoint && openPoint === 'canvasMenus') fetchBasicSettings(planNo, openPoint) }, []) + /** + * 현재 활성 플랜이 변경될 때 currentRoof.planNo 업데이트 + */ + useEffect(() => { + if (currentCanvasPlan?.planNo && currentRoof) { + setCurrentRoof(prev => ({ ...prev, planNo: currentCanvasPlan.planNo })) + } + }, [currentCanvasPlan?.planNo]) + /** * 배치면초기설정 데이터 조회 후 화면 오픈 */ @@ -131,7 +142,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla setRaftCodes(raftCodeList) setCurrentRoof({ ...addedRoofs[0], - planNo: planNo, + planNo: currentCanvasPlan?.planNo || planNo, roofSizeSet: String(basicSetting.roofSizeSet), roofAngleSet: basicSetting.roofAngleSet, }) @@ -167,10 +178,6 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla }) }, [currentRoof]) - const handleRoofSizeSetChange = (value) => { - setCurrentRoof({ ...currentRoof, roofSizeSet: value }) - } - const handleRoofAngleSetChange = (value) => { setCurrentRoof({ ...currentRoof, roofAngleSet: value }) } @@ -224,7 +231,7 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla const handleSaveBtn = async () => { const roofInfo = { ...currentRoof, - planNo: basicSetting.planNo, + planNo: currentCanvasPlan?.planNo || basicSetting.planNo, roofCd: roofRef.roofCd.current?.value, width: roofRef.width.current?.value, length: roofRef.length.current?.value, @@ -239,18 +246,15 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla newAddedRoofs[0] = { ...roofInfo } setAddedRoofs(newAddedRoofs) - console.log('save Info', { - ...basicSetting, - selectedRoofMaterial: { - ...newAddedRoofs[0], - }, - }) + // currentRoof의 roofSizeSet을 canvasSetting에 반영 + setCanvasSetting({ ...canvasSetting, roofSizeSet: currentRoof?.roofSizeSet }) /** * 배치면초기설정 저장 (메뉴 변경/useEffect 트리거 없이) */ basicSettingSave({ ...basicSetting, + planNo: currentCanvasPlan?.planNo || basicSetting.planNo, /** * 선택된 지붕재 정보 */ @@ -272,7 +276,15 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla /** 지붕면 존재 여부에 따라 메뉴 설정 */ const hasRoofs = canvas.getObjects().some((obj) => obj.name === POLYGON_TYPE.ROOF) - if (hasRoofs) { + + // roofSizeSet에 따라 메뉴 설정 + if (currentRoof?.roofSizeSet === '2') { + setSelectedMenu('surface') + setCurrentMenu(MENU.BATCH_CANVAS.BATCH_DRAWING) + } else if (currentRoof?.roofSizeSet === '1') { + setSelectedMenu('outline') + setCurrentMenu(MENU.ROOF_COVERING.EXTERIOR_WALL_LINE) + } else if (hasRoofs) { setSelectedMenu('surface') setCurrentMenu(MENU.BATCH_CANVAS.BATCH_DRAWING) } else { diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index 75db2623..97ba9dd3 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -294,8 +294,13 @@ export default function StuffDetail() { docDownButtonStyle = 'none' } else { if (params?.data?.createSaleStoreId === 'T01' && session?.storeId !== 'T01') { - estimateDetailButtonStyle = 'none' + if(session?.storeId !== params?.data?.saleStoreId){ + if(session?.storeId !== params?.data?.firstAgentId) { + estimateDetailButtonStyle = 'none' + } + } } + if (params?.data?.tempFlg === '1' || !params?.data?.docNo) { docDownButtonStyle = 'none' } @@ -357,7 +362,13 @@ export default function StuffDetail() { if (res?.data?.createSaleStoreId === 'T01') { if (session?.storeId !== 'T01') { - setShowButton('none') + //T01 계정이 작성한 안건 중 해당 판매점 ID가 열람 가능한 것에 한합니다. + if(session?.storeId !== res?.data?.saleStoreId){ + if(session?.storeId !== res?.data?.firstAgentId) { + setShowButton('none') + } + } + } } if (isObjectNotEmpty(res.data)) { @@ -385,7 +396,15 @@ export default function StuffDetail() { }) } if (isNotEmptyArray(res.data.planList)) { - setPlanGridProps({ ...planGridProps, planGridData: res.data.planList }) + + const planGridData = res.data.planList.map(plan => ({ + ...plan, + createSaleStoreId: res.data.createSaleStoreId, + saleStoreId: res.data.saleStoreId, + firstAgentId: res.data.firstAgentId + })) + + setPlanGridProps({ ...planGridProps, planGridData }) } else { setPlanGridProps({ ...planGridProps, planGridData: [] }) } @@ -1658,7 +1677,13 @@ export default function StuffDetail() { const getCellDoubleClicked = (params) => { if (managementState?.createSaleStoreId === 'T01') { if (session?.storeId !== 'T01') { - return false + //T01 계정이 작성한 안건 중 해당 판매점 ID가 열람 가능한 것에 한합니다. + if(session?.storeId !== managementState?.saleStoreId){ + if(session?.storeId !== managementState?.firstAgentId) { + return false + } + } + } } @@ -1723,7 +1748,7 @@ export default function StuffDetail() { -
- +
@@ -2734,7 +2759,7 @@ export default function StuffDetail() { onChange={(value) => form.setValue('verticalSnowCover', value)} options={{ allowNegative: false, - allowDecimal: false + allowDecimal: false, }} />
@@ -2810,7 +2835,7 @@ export default function StuffDetail() { onChange={(value) => form.setValue('installHeight', value)} options={{ allowNegative: false, - allowDecimal: false + allowDecimal: false, }} />
@@ -2941,7 +2966,7 @@ export default function StuffDetail() { )} - {estimatePopupOpen && } + {estimatePopupOpen && } ) } diff --git a/src/components/management/StuffSubHeader.jsx b/src/components/management/StuffSubHeader.jsx index 6cbfd862..b0ff2725 100644 --- a/src/components/management/StuffSubHeader.jsx +++ b/src/components/management/StuffSubHeader.jsx @@ -39,7 +39,12 @@ export default function StuffSubHeader({ type }) { if (isObjectNotEmpty(managementState)) { if (managementState?.createSaleStoreId === 'T01') { if (session?.storeId !== 'T01') { - setButtonStyle('none') + //T01 계정이 작성한 안건 중 해당 판매점 ID가 열람 가능한 것에 한합니다. + if(session?.storeId !== managementState?.saleStoreId){ + if(session?.storeId !== managementState?.firstAgentId) { + setButtonStyle('none') + } + } } } } diff --git a/src/hooks/module/useModule.js b/src/hooks/module/useModule.js index 8e59a1e7..c81c54cb 100644 --- a/src/hooks/module/useModule.js +++ b/src/hooks/module/useModule.js @@ -1059,7 +1059,7 @@ export function useModule() { } const isOutsideSurface = (module, moduleSetupSurface) => { - return !checkModuleDisjointSurface(polygonToTurfPolygon(module, true), polygonToTurfPolygon(moduleSetupSurface, true)) + return !checkModuleDisjointSurface(polygonToTurfPolygon(module, true), polygonToTurfPolygon(moduleSetupSurface, true), 0.5) } const getRowModules = (target) => { diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index e4ee52a8..f8bc9901 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -90,9 +90,13 @@ export const useTrestle = () => { let rackInfos = [] if (rack) { + console.log('Original rack data:', rack) rackInfos = Object.keys(rack).map((key) => { return { key, value: rack[key] } }) + console.log('Processed rackInfos:', rackInfos) + } else { + console.log('Rack is null or undefined') } // 모듈들의 centerPoint들을 이용해 각 모듈의 정보(가장 아랫라인 모듈, 가장 윗라인 모듈, 접면, 반접면 등 계산) @@ -451,6 +455,16 @@ export const useTrestle = () => { })?.value.racks mostRowsModule = Math.max(leftRows, rightRows, centerRows, mostRowsModule) + console.log('=== Debug rackInfos ===') + console.log('rackInfos:', rackInfos) + console.log('leftRowsInfo:', leftRowsInfo) + console.log('rightRowsInfo:', rightRowsInfo) + console.log('centerRowsInfo:', centerRowsInfo) + console.log('leftRacks:', leftRacks) + console.log('rightRacks:', rightRacks) + console.log('centerRacks:', centerRacks) + console.log('rackYn:', rackYn) + console.log('========================') if (rackYn === 'Y') { drawRacks(leftRacks, rackQty, rackIntvlPct, module, direction, 'L', rackYn) @@ -1082,14 +1096,19 @@ export const useTrestle = () => { const drawRacks = (rackInfos, rackQty, rackIntvlPct, module, direction, l, rackYn) => { const { width, height, left, top, lastX, lastY, surfaceId } = module const surface = canvas.getObjects().find((obj) => obj.id === surfaceId) - if (!rackInfos) { - const maxRows = surface.trestleDetail.moduleMaxRows - const maxCols = surface.trestleDetail.moduleMaxCols - const msg = `段数の上限は${maxRows}段です。 上限より上の段には設置できません` - swalFire({ title: msg, type: 'alert' }) - throw new Error('rackInfos is null') - } + // if (!rackInfos) { + // const maxRows = surface.trestleDetail.moduleMaxRows + // const maxCols = surface.trestleDetail.moduleMaxCols + // const msg = `段数の上限は${maxRows}段です。 上限より上の段には設置できません` + // swalFire({ title: msg, type: 'alert' }) + // throw new Error('rackInfos is null') + // } + if (!rackInfos) { + const msg = '該当モジュールタイプに関するラック情報がありません。' + swalFire({ title: msg, type: 'alert' }) + return + } const roof = canvas.getObjects().find((obj) => obj.id === surface.parentId) const degree = getDegreeByChon(roof.roofMaterial.pitch) rackIntvlPct = rackIntvlPct === 0 ? 1 : rackIntvlPct // 0인 경우 1로 변경 diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index edd74c22..fc80fe70 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -441,15 +441,19 @@ export function useCanvasSetting(executeEffect = true) { setAddedRoofs(addRoofs) if (openPoint !== 'basicSettingSave') { - setCanvasSetting({ - ...basicSetting, - roofMaterials: addRoofs[0], - planNo: roofsRow[0].planNo, - roofSizeSet: roofsRow[0].roofSizeSet, - roofAngleSet: roofsRow[0].roofAngleSet, - roofsData: roofsArray, - selectedRoofMaterial: addRoofs.find((roof) => roof.selected), - }) + // canvasSetting은 현재 값을 유지하고 basicSetting만 업데이트 + // 새로고침 시 canvasSetting이 바뀌는 문제 방지 + if (!canvasSetting?.roofSizeSet) { + setCanvasSetting({ + ...basicSetting, + roofMaterials: addRoofs[0], + planNo: roofsRow[0].planNo, + roofSizeSet: roofsRow[0].roofSizeSet, + roofAngleSet: roofsRow[0].roofAngleSet, + roofsData: roofsArray, + selectedRoofMaterial: addRoofs.find((roof) => roof.selected), + }) + } } } }) @@ -566,12 +570,44 @@ export function useCanvasSetting(executeEffect = true) { */ const basicSettingCopySave = async (params) => { try { - const patternData = { - objectNo: correntObjectNo, - planNo: Number(params.planNo), - roofSizeSet: Number(params.roofSizeSet), - roofAngleSet: params.roofAngleSet, - roofMaterialsAddList: params.roofsData.map((item) => ({ + // roofsData가 단일 항목인 경우, 모든 추가된 지붕재(addedRoofs)를 사용하여 다중 항목으로 확장 + let roofMaterialsList = [] + + if (params.roofsData && params.roofsData.length === 1) { + // 단일 항목인 경우 addedRoofs의 모든 항목을 사용 + if (addedRoofs && addedRoofs.length > 0) { + roofMaterialsList = addedRoofs.map((roof, index) => ({ + planNo: Number(params.planNo), + roofApply: roof.selected || index === 0, // 첫 번째 또는 선택된 항목 + roofSeq: index, + roofMatlCd: roof.roofMatlCd, + roofWidth: roof.width || roof.roofWidth, + roofHeight: roof.length || roof.roofHeight, + roofHajebichi: roof.hajebichi || 0, + roofGap: roof.raft || roof.roofGap, + roofLayout: roof.layout || 'P', + roofPitch: roof.pitch || 0, + roofAngle: roof.angle || 0, + })) + } else { + // addedRoofs가 비어있을 경우 원래 단일 항목 사용 + roofMaterialsList = params.roofsData.map((item) => ({ + planNo: Number(item.planNo), + roofApply: item.roofApply, + roofSeq: item.roofSeq, + roofMatlCd: item.roofMatlCd, + roofWidth: item.roofWidth, + roofHeight: item.roofHeight, + roofHajebichi: item.roofHajebichi, + roofGap: item.roofGap, + roofLayout: item.roofLayout, + roofPitch: item.roofPitch, + roofAngle: item.roofAngle, + })) + } + } else { + // 다중 항목인 경우 기존 방식 사용 + roofMaterialsList = params.roofsData.map((item) => ({ planNo: Number(item.planNo), roofApply: item.roofApply, roofSeq: item.roofSeq, @@ -583,7 +619,15 @@ export function useCanvasSetting(executeEffect = true) { roofLayout: item.roofLayout, roofPitch: item.roofPitch, roofAngle: item.roofAngle, - })), + })) + } + + const patternData = { + objectNo: correntObjectNo, + planNo: Number(params.planNo), + roofSizeSet: Number(params.roofSizeSet), + roofAngleSet: params.roofAngleSet, + roofMaterialsAddList: roofMaterialsList, } await post({ url: `/api/canvas-management/canvas-basic-settings`, data: patternData }).then((res) => { diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 9481af67..40a3d85f 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -1955,7 +1955,7 @@ export const usePolygon = () => { forceUpdate = true } - if (polygon.from !== 'surface') { + /*if (polygon.from !== 'surface') { // createdRoofs들의 모든 lines를 확인해서 length값이 1이하인 차이가 있으면 통일 시킨다. const allRoofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) const allRoofLines = allRoofs.flatMap((roof) => roof.lines) @@ -1978,7 +1978,7 @@ export const usePolygon = () => { } } } - } + }*/ polygon.lines.forEach((line, index) => { if (line.attributes.isCalculated && !forceUpdate) { diff --git a/src/locales/ja.json b/src/locales/ja.json index 2953688e..feb9945f 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -76,7 +76,7 @@ "common.setting.rollback": "前に戻る", "modal.cover.outline.remove": "外壁の取り外し", "modal.cover.outline.select.move": "外壁選択の移動", - "plan.menu.placement.surface": "実測値入力", + "plan.menu.placement.surface": "配置面", "plan.menu.placement.surface.slope.setting": "傾斜設定", "plan.menu.placement.surface.drawing": "配置面の描画", "modal.placement.surface.drawing.straight.line": "直線", diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index aeb63ffa..6a3c567e 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -394,83 +394,87 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const isClosedPolygon = (points) => points.length > 1 && isSamePoint(points[0], points[points.length - 1]) - const roofLinePoints = isClosedPolygon(roof.points) ? roof.points.slice(0, -1) : roof.points const orderedBaseLinePoints = isClosedPolygon(baseLinePoints) ? baseLinePoints.slice(0, -1) : baseLinePoints - console.log('roofLinePoints:', roofLinePoints) - console.log('baseLinePoints:', orderedBaseLinePoints) - - // baseLinePoint에서 roofLinePoint 방향으로 45도 대각선 확장 후 가장 가까운 접점 계산 - // 각 포인트의 dx, dy, sign 정보 수집 - const contactData = orderedBaseLinePoints.map((point, index) => { - const roofPoint = roofLinePoints[index] - if (!roofPoint) return { dx: 0, dy: 0, signDx: 0, signDy: 0 } - - const dx = roofPoint.x - point.x - const dy = roofPoint.y - point.y - return { dx, dy, signDx: Math.sign(dx), signDy: Math.sign(dy) } - }) - - // maxStep: 모든 포인트 중 가장 큰 45도 step - const maxStep = contactData.reduce((max, data) => { - return Math.max(max, Math.min(Math.abs(data.dx), Math.abs(data.dy))) - }, 0) - - // baseLine 폴리곤의 중심 계산 (확장 방향 결정용) + // baseLine 폴리곤의 중심 계산 (offset 방향 결정용) const centroid = orderedBaseLinePoints.reduce((acc, p) => { acc.x += p.x / orderedBaseLinePoints.length acc.y += p.y / orderedBaseLinePoints.length return acc }, { x: 0, y: 0 }) - // 모든 포인트를 동일한 maxStep으로 45도 확장 - let roofLineContactPoints = orderedBaseLinePoints.map((point, index) => { - const data = contactData[index] - const { dx, dy } = data - - // dx 또는 dy가 0인 경우 중심에서 바깥쪽 방향으로 확장 - let signDx = data.signDx - let signDy = data.signDy - if (signDx === 0) { - signDx = point.x >= centroid.x ? 1 : -1 - } - if (signDy === 0) { - signDy = point.y >= centroid.y ? 1 : -1 - } - + // baseLine을 offset만큼 바깥으로 평행이동 + const offsetLine = (line) => { + const sp = line.startPoint, ep = line.endPoint + const offset = line.attributes?.offset ?? 0 + const edx = ep.x - sp.x, edy = ep.y - sp.y + const len = Math.hypot(edx, edy) + if (len === 0) return { sp: { ...sp }, ep: { ...ep } } + let nx = -edy / len, ny = edx / len + const mid = { x: (sp.x + ep.x) / 2, y: (sp.y + ep.y) / 2 } + if (nx * (centroid.x - mid.x) + ny * (centroid.y - mid.y) > 0) { nx = -nx; ny = -ny } return { - x: point.x + signDx * maxStep, - y: point.y + signDy * maxStep + sp: { x: sp.x + nx * offset, y: sp.y + ny * offset }, + ep: { x: ep.x + nx * offset, y: ep.y + ny * offset } } + } + + // 두 직선의 교차점 + const lineIntersect = (a1, a2, b1, b2) => { + const denom = (a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x) + if (Math.abs(denom) < 0.001) return null + const t = ((a1.x - b1.x) * (b1.y - b2.y) - (a1.y - b1.y) * (b1.x - b2.x)) / denom + return { x: a1.x + t * (a2.x - a1.x), y: a1.y + t * (a2.y - a1.y) } + } + + // 각 꼭짓점에서 인접 두 baseLine의 offset 교차점으로 확장 좌표 계산 + let changRoofLinePoints = orderedBaseLinePoints.map((point) => { + const adjLines = baseLines.filter(line => + isSamePoint(point, line.startPoint) || isSamePoint(point, line.endPoint) + ) + if (adjLines.length < 2) return { ...point } + const oL1 = offsetLine(adjLines[0]), oL2 = offsetLine(adjLines[1]) + return lineIntersect(oL1.sp, oL1.ep, oL2.sp, oL2.ep) || { ...point } }) - const maxContactDistance = Math.hypot(maxStep, maxStep) - - let changRoofLinePoints = orderedBaseLinePoints.map((point, index) => { - const contactPoint = roofLineContactPoints[index] - if (!contactPoint) return point - - const dx = contactPoint.x - point.x - const dy = contactPoint.y - point.y - const len = Math.hypot(dx, dy) - - // 거리가 0이면 이미 같은 위치 - if (len === 0) return point - - const step = maxContactDistance - if (step === 0) return point - - const nextX = point.x + (dx / len) * step - const nextY = point.y + (dy / len) * step - return { - x: nextX, - y: nextY + // 중복 좌표 및 일직선 위의 불필요한 점 제거 (L자 확장 시 사각형으로 단순화) + const simplifyPolygon = (pts) => { + let result = [...pts] + let changed = true + while (changed) { + changed = false + for (let i = result.length - 1; i >= 0; i--) { + const next = result[(i + 1) % result.length] + if (Math.abs(result[i].x - next.x) < 0.5 && Math.abs(result[i].y - next.y) < 0.5) { + result.splice(i, 1) + changed = true + } + } + for (let i = result.length - 1; i >= 0 && result.length > 3; i--) { + const prev = result[(i - 1 + result.length) % result.length] + const curr = result[i] + const next = result[(i + 1) % result.length] + const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x) + if (Math.abs(cross) < 1) { + result.splice(i, 1) + changed = true + } + } } - }) + return result + } + changRoofLinePoints = simplifyPolygon(changRoofLinePoints) + + let roofLineContactPoints = [...changRoofLinePoints] + + console.log('baseLinePoints:', orderedBaseLinePoints) + console.log('changRoofLinePoints:', changRoofLinePoints) //마루이동 if (moveFlowLine !== 0 || moveUpDown !== 0) { - roofLineContactPoints = movingLineFromSkeleton(roofId, canvas) + const movedPoints = movingLineFromSkeleton(roofId, canvas) + roofLineContactPoints = movedPoints + changRoofLinePoints = movedPoints } // changRoofLinePoints 좌표를 roof.skeletonPoints에 저장 (원본 roof.points는 유지) @@ -487,7 +491,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 스켈레톤 데이터를 기반으로 내부선 생성 roof.innerLines = roof.innerLines || [] roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) - //console.log("roofInnerLines:::", roof.innerLines); + console.log("roofInnerLines:::", roof.innerLines); // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {}