From a0cac795743a585437a6af7f93ec679b20ee9016 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 09:34:08 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=E8=A3=9C=E5=8A=A9=E7=B7=9A=E3=81=AE?= =?UTF-8?q?=E4=BD=9C=E6=88=90=20=ED=86=A0=EA=B8=80=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/roofShape/RoofShapePassivitySetting.jsx | 12 +++++++++++- .../floor-plan/modal/roofShape/passivity/Eaves.jsx | 4 +++- .../floor-plan/modal/roofShape/passivity/Gable.jsx | 3 ++- .../floor-plan/modal/roofShape/passivity/Shed.jsx | 3 ++- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/floor-plan/modal/roofShape/RoofShapePassivitySetting.jsx b/src/components/floor-plan/modal/roofShape/RoofShapePassivitySetting.jsx index 7530bcdd..c43c16ce 100644 --- a/src/components/floor-plan/modal/roofShape/RoofShapePassivitySetting.jsx +++ b/src/components/floor-plan/modal/roofShape/RoofShapePassivitySetting.jsx @@ -5,27 +5,34 @@ import Shed from '@/components/floor-plan/modal/roofShape/passivity/Shed' import { useMessage } from '@/hooks/useMessage' import { useRoofShapePassivitySetting } from '@/hooks/roofcover/useRoofShapePassivitySetting' import { usePopup } from '@/hooks/usePopup' +import { useState } from 'react' +import Image from 'next/image' export default function RoofShapePassivitySetting({ id, pos = { x: 50, y: 230 } }) { const { handleSave, handleConfirm, handleRollback, buttons, type, setType, TYPES, offsetRef, pitchRef, pitchText } = useRoofShapePassivitySetting(id) const { getMessage } = useMessage() const { closePopup } = usePopup() + const [useCalcPad, setUseCalcPad] = useState(false) + const disableKeypad = !useCalcPad const eavesProps = { offsetRef, pitchRef, pitchText, + disableKeypad, } const gableProps = { offsetRef, pitchRef, pitchText, + disableKeypad, } const shedProps = { offsetRef, + disableKeypad, } return ( @@ -55,7 +62,10 @@ export default function RoofShapePassivitySetting({ id, pos = { x: 50, y: 230 } -
+
+ diff --git a/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx b/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx index f3dd4052..3b775440 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Eaves.jsx @@ -5,7 +5,7 @@ import { selectedRoofMaterialSelector } from '@/store/settingAtom' import { useEffect } from 'react' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Eaves({ offsetRef, pitchRef, pitchText }) { +export default function Eaves({ offsetRef, pitchRef, pitchText, disableKeypad }) { const { getMessage } = useMessage() const currentAngleType = useRecoilValue(currentAngleTypeSelector) const selectedRoofMaterial = useRecoilValue(selectedRoofMaterialSelector) @@ -30,6 +30,7 @@ export default function Eaves({ offsetRef, pitchRef, pitchText }) { className="input-origin block" ref={pitchRef} value={currentAngleType === ANGLE_TYPE.SLOPE ? selectedRoofMaterial.pitch : selectedRoofMaterial.angle} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: true //(index !== 0), @@ -52,6 +53,7 @@ export default function Eaves({ offsetRef, pitchRef, pitchText }) { value={offsetRef.current?.value ?? 500} // Set default value to 500 ref={offsetRef} onChange={() => {}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false diff --git a/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx b/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx index fe04a65e..a37f7072 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Gable.jsx @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil' import { currentAngleTypeSelector } from '@/store/canvasAtom' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Gable({ offsetRef }) { +export default function Gable({ offsetRef, disableKeypad }) { const { getMessage } = useMessage() const currentAngleType = useRecoilValue(currentAngleTypeSelector) return ( @@ -22,6 +22,7 @@ export default function Gable({ offsetRef }) { value={offsetRef.current?.value ?? 300} // Set default value to 500 ref={offsetRef} onChange={() => {}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false diff --git a/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx b/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx index 67a47bb0..0f33278f 100644 --- a/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx +++ b/src/components/floor-plan/modal/roofShape/passivity/Shed.jsx @@ -1,7 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Shed({ offsetRef }) { +export default function Shed({ offsetRef, disableKeypad }) { const { getMessage } = useMessage() return ( <> @@ -19,6 +19,7 @@ export default function Shed({ offsetRef }) { value={offsetRef.current?.value ?? 300} // Set default value to 500 ref={offsetRef} onChange={() => {}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false From 2c19a208cd5fa763bbc63971cabdabc1dc0bfc4b Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 09:36:50 +0900 Subject: [PATCH 02/21] =?UTF-8?q?=E8=A3=9C=E5=8A=A9=E7=B7=9A=E3=81=AE?= =?UTF-8?q?=E4=BD=9C=E6=88=90=20=ED=86=A0=EA=B8=80=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/auxiliary/AuxiliaryDrawing.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx b/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx index d7a475a0..1bbec893 100644 --- a/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx +++ b/src/components/floor-plan/modal/auxiliary/AuxiliaryDrawing.jsx @@ -8,14 +8,17 @@ import { OUTER_LINE_TYPE } from '@/store/outerLineAtom' import OuterLineWall from '@/components/floor-plan/modal/lineTypes/OuterLineWall' import { useAuxiliaryDrawing } from '@/hooks/roofcover/useAuxiliaryDrawing' import { usePopup } from '@/hooks/usePopup' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { useRecoilValue } from 'recoil' import { canvasState } from '@/store/canvasAtom' +import Image from 'next/image' export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { const canvas = useRecoilValue(canvasState) const { getMessage } = useMessage() const { closePopup } = usePopup() + const [useCalcPad, setUseCalcPad] = useState(false) + const disableKeypad = !useCalcPad const types = [ { id: 1, name: getMessage('straight.line'), type: OUTER_LINE_TYPE.OUTER_LINE }, @@ -71,6 +74,7 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { length1Ref, arrow1, setArrow1, + disableKeypad, } const rightAngleProps = { @@ -84,6 +88,7 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { setArrow1, arrow2, setArrow2, + disableKeypad, } const doublePitchProps = { @@ -105,6 +110,7 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { setArrow2, arrow1Ref, arrow2Ref, + disableKeypad, } const angleProps = { @@ -114,6 +120,7 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { length1, setLength1, length1Ref, + disableKeypad, } const diagonalLineProps = { @@ -130,6 +137,7 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { setArrow1, arrow2, setArrow2, + disableKeypad, } const onClickButton = (button) => { @@ -160,7 +168,10 @@ export default function AuxiliaryDrawing({ id, pos = { x: 50, y: 230 } }) { {buttonAct === 4 && } {buttonAct === 5 && }
-
+
+ From 94f050d917ebf70a39f378544c2c01cab46b120c Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 09:57:28 +0900 Subject: [PATCH 03/21] =?UTF-8?q?=E8=BB=92/=E3=82=B1=E3=83=A9=E3=83=90?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20=ED=86=A0=EA=B8=80=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/eavesGable/EavesGableEdit.jsx | 17 +++++++++++++++-- .../floor-plan/modal/eavesGable/type/Eaves.jsx | 5 ++++- .../floor-plan/modal/eavesGable/type/Gable.jsx | 5 ++++- .../floor-plan/modal/eavesGable/type/Shed.jsx | 3 ++- .../modal/eavesGable/type/WallMerge.jsx | 3 ++- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/components/floor-plan/modal/eavesGable/EavesGableEdit.jsx b/src/components/floor-plan/modal/eavesGable/EavesGableEdit.jsx index a59f8f27..341ecad8 100644 --- a/src/components/floor-plan/modal/eavesGable/EavesGableEdit.jsx +++ b/src/components/floor-plan/modal/eavesGable/EavesGableEdit.jsx @@ -6,10 +6,14 @@ import WallMerge from '@/components/floor-plan/modal/eavesGable/type/WallMerge' import Shed from '@/components/floor-plan/modal/eavesGable/type/Shed' import { useEavesGableEdit } from '@/hooks/roofcover/useEavesGableEdit' import { usePopup } from '@/hooks/usePopup' +import { useState } from 'react' +import Image from 'next/image' export default function EavesGableEdit({ id, pos = { x: 50, y: 230 } }) { const { getMessage } = useMessage() const { closePopup } = usePopup() + const [useCalcPad, setUseCalcPad] = useState(false) + const disableKeypad = !useCalcPad const { type, setType, buttonMenu, TYPES, pitchRef, offsetRef, widthRef, radioTypeRef, pitchText } = useEavesGableEdit(id) const eavesProps = { @@ -18,6 +22,7 @@ export default function EavesGableEdit({ id, pos = { x: 50, y: 230 } }) { widthRef, radioTypeRef, pitchText, + disableKeypad, } const gableProps = { @@ -26,15 +31,18 @@ export default function EavesGableEdit({ id, pos = { x: 50, y: 230 } }) { widthRef, radioTypeRef, pitchText, + disableKeypad, } const wallMergeProps = { offsetRef, radioTypeRef, + disableKeypad, } const shedProps = { offsetRef, + disableKeypad, } return ( @@ -48,8 +56,13 @@ export default function EavesGableEdit({ id, pos = { x: 50, y: 230 } }) { ))}
-
-
{getMessage('setting')}
+
+
+ {getMessage('setting')} + +
{type === TYPES.EAVES && } {type === TYPES.GABLE && } {type === TYPES.WALL_MERGE && } diff --git a/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx b/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx index fb4db8cd..78cbd55e 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Eaves.jsx @@ -6,7 +6,7 @@ import { ANGLE_TYPE, currentAngleTypeSelector } from '@/store/canvasAtom' import { normalizeDecimalLimit, normalizeDigits } from '@/util/input-utils' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText }) { +export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText, disableKeypad }) { const { getMessage } = useMessage() const [type, setType] = useState('1') const onChange = (e) => { @@ -43,6 +43,7 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit onChange={(value) => { if (pitchRef?.current) pitchRef.current.value = value }} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: true //(index !== 0), @@ -77,6 +78,7 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit onChange={(value) => { if (offsetRef?.current) offsetRef.current.value = value }} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), @@ -144,6 +146,7 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit onChange={(value) => { if (widthRef?.current) widthRef.current.value = value }} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), diff --git a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx index b8eab8a7..ba947dec 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Gable.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Gable.jsx @@ -5,7 +5,7 @@ import { useRecoilValue } from 'recoil' import { ANGLE_TYPE, currentAngleTypeSelector } from '@/store/canvasAtom' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText }) { +export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pitchText, disableKeypad }) { const { getMessage } = useMessage() const [type, setType] = useState('1') const onChange = (e) => { @@ -30,6 +30,7 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit className="input-origin block" ref={offsetRef} value={300} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), @@ -96,6 +97,7 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit onChange={(value) => { if (pitchRef?.current) pitchRef.current.value = value }} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: true //(index !== 0), @@ -129,6 +131,7 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit ref={widthRef} value={800} readOnly={type === '1'} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), diff --git a/src/components/floor-plan/modal/eavesGable/type/Shed.jsx b/src/components/floor-plan/modal/eavesGable/type/Shed.jsx index 8ce6038a..724e6c40 100644 --- a/src/components/floor-plan/modal/eavesGable/type/Shed.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/Shed.jsx @@ -1,7 +1,7 @@ import { useMessage } from '@/hooks/useMessage' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Shed({ offsetRef }) { +export default function Shed({ offsetRef, disableKeypad }) { const { getMessage } = useMessage() return ( <> @@ -19,6 +19,7 @@ export default function Shed({ offsetRef }) { className="input-origin block" ref={offsetRef} value={300} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), diff --git a/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx b/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx index e398a2f1..18891015 100644 --- a/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx +++ b/src/components/floor-plan/modal/eavesGable/type/WallMerge.jsx @@ -3,7 +3,7 @@ import Image from 'next/image' import { useState } from 'react' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function WallMerge({ offsetRef, radioTypeRef }) { +export default function WallMerge({ offsetRef, radioTypeRef, disableKeypad }) { const { getMessage } = useMessage() const [type, setType] = useState('1') const onChange = (e) => { @@ -61,6 +61,7 @@ export default function WallMerge({ offsetRef, radioTypeRef }) { ref={offsetRef} value={300} readOnly={type === '1'} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false //(index !== 0), From 84ac6d508fc0007e77275f24d88ceac3d23f11d4 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 10:01:08 +0900 Subject: [PATCH 04/21] =?UTF-8?q?=E6=A3=9F=E7=B7=9A=E7=A7=BB=E5=8B=95?= =?UTF-8?q?=E3=83=BB=E6=A1=81=E4=B8=8A=E3=81=92=E4=B8=8B=E3=81=92=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../floor-plan/modal/movement/MovementSetting.jsx | 11 ++++++++++- .../floor-plan/modal/movement/type/FlowLine.jsx | 3 ++- .../floor-plan/modal/movement/type/Updown.jsx | 3 ++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/floor-plan/modal/movement/MovementSetting.jsx b/src/components/floor-plan/modal/movement/MovementSetting.jsx index 46464538..8de192c3 100644 --- a/src/components/floor-plan/modal/movement/MovementSetting.jsx +++ b/src/components/floor-plan/modal/movement/MovementSetting.jsx @@ -3,16 +3,22 @@ import WithDraggable from '@/components/common/draggable/WithDraggable' import FlowLine from '@/components/floor-plan/modal/movement/type/FlowLine' import Updown from '@/components/floor-plan/modal/movement/type/Updown' import { useMovementSetting } from '@/hooks/roofcover/useMovementSetting' +import { useState } from 'react' +import Image from 'next/image' export default function MovementSetting({ id, pos = { x: 50, y: 230 } }) { const { getMessage } = useMessage() const { TYPE, closePopup, buttonType, type, setType, FLOW_LINE_REF, UP_DOWN_REF, handleSave } = useMovementSetting(id) + const [useCalcPad, setUseCalcPad] = useState(false) + const disableKeypad = !useCalcPad const flowLineProps = { FLOW_LINE_REF, + disableKeypad, } const updownProps = { UP_DOWN_REF, + disableKeypad, } return ( @@ -30,7 +36,10 @@ export default function MovementSetting({ id, pos = { x: 50, y: 230 } }) { {type === TYPE.FLOW_LINE && } {type === TYPE.UP_DOWN && }
-
+
+ diff --git a/src/components/floor-plan/modal/movement/type/FlowLine.jsx b/src/components/floor-plan/modal/movement/type/FlowLine.jsx index d4267a27..0c38638d 100644 --- a/src/components/floor-plan/modal/movement/type/FlowLine.jsx +++ b/src/components/floor-plan/modal/movement/type/FlowLine.jsx @@ -9,7 +9,7 @@ const FLOW_LINE_TYPE = { UP_RIGHT: 'upRight', } -export default function FlowLine({ FLOW_LINE_REF }) { +export default function FlowLine({ FLOW_LINE_REF, disableKeypad }) { const { getMessage } = useMessage() const [type, setType] = useState(FLOW_LINE_TYPE.DOWN_LEFT) const [filledInput, setFilledInput] = useState('') @@ -87,6 +87,7 @@ export default function FlowLine({ FLOW_LINE_REF }) { value={filledInput} onFocus={handleFocus} onChange={(value)=>{setFilledInput(value)}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false diff --git a/src/components/floor-plan/modal/movement/type/Updown.jsx b/src/components/floor-plan/modal/movement/type/Updown.jsx index ab88bbbf..a89ce39d 100644 --- a/src/components/floor-plan/modal/movement/type/Updown.jsx +++ b/src/components/floor-plan/modal/movement/type/Updown.jsx @@ -10,7 +10,7 @@ const UP_DOWN_TYPE = { DOWN: 'down', } -export default function Updown({ UP_DOWN_REF }) { +export default function Updown({ UP_DOWN_REF, disableKeypad }) { const { getMessage } = useMessage() const [type, setType] = useState(UP_DOWN_TYPE.UP) const [filledInput, setFilledInput] = useState('100') @@ -87,6 +87,7 @@ export default function Updown({ UP_DOWN_REF }) { value={filledInput} onFocus={handleFocus} onChange={(value)=>{setFilledInput(value)}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false From 6967b8cda05547308a8d60c3c290807fecebbba3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 10:12:48 +0900 Subject: [PATCH 05/21] =?UTF-8?q?=E5=A4=96=E5=A3=81=E3=81=AE=E7=B7=A8?= =?UTF-8?q?=E9=9B=86=E3=81=A8=E3=82=AA=E3=83=95=E3=82=BB=E3=83=83=E3=83=88?= =?UTF-8?q?=20=ED=86=A0=EA=B8=80=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/wallLineOffset/WallLineOffsetSetting.jsx | 11 ++++++++++- .../floor-plan/modal/wallLineOffset/type/Offset.jsx | 3 ++- .../floor-plan/modal/wallLineOffset/type/WallLine.jsx | 4 +++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/floor-plan/modal/wallLineOffset/WallLineOffsetSetting.jsx b/src/components/floor-plan/modal/wallLineOffset/WallLineOffsetSetting.jsx index 09c847e7..86ab5df6 100644 --- a/src/components/floor-plan/modal/wallLineOffset/WallLineOffsetSetting.jsx +++ b/src/components/floor-plan/modal/wallLineOffset/WallLineOffsetSetting.jsx @@ -4,10 +4,14 @@ import WallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine import Offset from '@/components/floor-plan/modal/wallLineOffset/type/Offset' import { useWallLineOffsetSetting } from '@/hooks/roofcover/useWallLineOffsetSetting' import { usePopup } from '@/hooks/usePopup' +import { useState } from 'react' +import Image from 'next/image' export default function WallLineOffsetSetting({ id, pos = { x: 50, y: 230 } }) { const { getMessage } = useMessage() const { closePopup } = usePopup() + const [useCalcPad, setUseCalcPad] = useState(false) + const disableKeypad = !useCalcPad const { type, setType, @@ -30,12 +34,14 @@ export default function WallLineOffsetSetting({ id, pos = { x: 50, y: 230 } }) { arrow2Ref, radioTypeRef, currentWallLineRef, + disableKeypad, } const offsetProps = { length1Ref, arrow1Ref, currentWallLineRef, + disableKeypad, } return ( @@ -54,7 +60,10 @@ export default function WallLineOffsetSetting({ id, pos = { x: 50, y: 230 } }) { {type === TYPES.WALL_LINE_EDIT && } {type === TYPES.OFFSET && }
-
+
+ diff --git a/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx b/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx index 79ae0898..8c4685ee 100644 --- a/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx +++ b/src/components/floor-plan/modal/wallLineOffset/type/Offset.jsx @@ -3,7 +3,7 @@ import { useEffect, useState } from 'react' import { useEvent } from '@/hooks/useEvent' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default function Offset({ length1Ref, arrow1Ref, currentWallLineRef }) { +export default function Offset({ length1Ref, arrow1Ref, currentWallLineRef, disableKeypad }) { const { getMessage } = useMessage() const { addDocumentEventListener, initEvent } = useEvent() // const { addDocumentEventListener, initEvent } = useContext(EventContext) @@ -84,6 +84,7 @@ export default function Offset({ length1Ref, arrow1Ref, currentWallLineRef }) { value={length1Ref.current?.value ?? 0} ref={length1Ref} onChange={() => {}} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false diff --git a/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx b/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx index d78a202f..610780e6 100644 --- a/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx +++ b/src/components/floor-plan/modal/wallLineOffset/type/WallLine.jsx @@ -3,7 +3,7 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' import { useEvent } from '@/hooks/useEvent' import { CalculatorInput } from '@/components/common/input/CalcInput' -export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref, arrow2Ref, radioTypeRef, currentWallLineRef }, ref) { +export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref, arrow2Ref, radioTypeRef, currentWallLineRef, disableKeypad }, ref) { const { getMessage } = useMessage() const { addDocumentEventListener, initEvent } = useEvent() // const { addDocumentEventListener, initEvent } = useContext(EventContext) @@ -57,6 +57,7 @@ export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref, ref={length1Ref} onChange={() => {}} readOnly={type !== 1} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false @@ -105,6 +106,7 @@ export default forwardRef(function WallLine({ length1Ref, length2Ref, arrow1Ref, ref={length2Ref} onChange={() => {}} readOnly={type !== 2} + disableKeypad={disableKeypad} options={{ allowNegative: false, allowDecimal: false From 76d3637a65720592c83aca2b44ea4978bd14337f Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 26 Mar 2026 10:37:26 +0900 Subject: [PATCH 06/21] =?UTF-8?q?[1798][=EB=B0=B0=EC=B9=98=EB=A9=B4=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B8=B0]=EC=9D=98=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/surface/usePlacementShapeDrawing.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/hooks/surface/usePlacementShapeDrawing.js b/src/hooks/surface/usePlacementShapeDrawing.js index 374616be..3a6d1c05 100644 --- a/src/hooks/surface/usePlacementShapeDrawing.js +++ b/src/hooks/surface/usePlacementShapeDrawing.js @@ -7,6 +7,7 @@ import { useEffect, useRef } from 'react' import { fabric } from 'fabric' import { calculateAngle } from '@/util/qpolygon-utils' import { + OUTER_LINE_TYPE, placementShapeDrawingAngle1State, placementShapeDrawingAngle2State, placementShapeDrawingArrow1State, @@ -92,6 +93,7 @@ export function usePlacementShapeDrawing(id) { useEffect(() => { setPoints([]) + setType(OUTER_LINE_TYPE.OUTER_LINE) return () => { const placementShapeDrawingStartPoint = canvas.getObjects().find((obj) => obj.name === 'placementShapeDrawingStartPoint') From fdfa8d5281837b699f53783d2ae1a3c4fa44c43b Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Thu, 26 Mar 2026 17:38:53 +0900 Subject: [PATCH 07/21] =?UTF-8?q?#1805=20=EA=B2=B9=EC=B9=98=EB=8A=94=20?= =?UTF-8?q?=EC=84=A0=20=EC=9E=88=EC=9D=84=20=EA=B2=BD=EC=9A=B0=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=84=A4=EC=B9=98=EC=98=81=EC=97=AD=20=EC=9D=B4?= =?UTF-8?q?=EC=83=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useModuleBasicSetting.js | 18 +++-- src/hooks/useMode.js | 80 ++++++++++++++++++++--- src/util/qpolygon-utils.js | 34 ++++++++++ 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index c48c2d21..cc315e32 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -17,7 +17,7 @@ import { import { calculateVisibleModuleHeight, getDegreeByChon, polygonToTurfPolygon, rectToPolygon, toFixedWithoutRounding } from '@/util/canvas-util' import '@/util/fabric-extensions' // fabric 객체들에 getCurrentPoints 메서드 추가 import { basicSettingState, roofDisplaySelector } from '@/store/settingAtom' -import offsetPolygon, { calculateAngle, cleanSelfIntersectingPolygon, createLinesFromPolygon } from '@/util/qpolygon-utils' +import offsetPolygon, { calculateAngle, cleanSelfIntersectingPolygon, clipOffsetToOriginal, createLinesFromPolygon } from '@/util/qpolygon-utils' import { QPolygon } from '@/components/fabric/QPolygon' import { useEvent } from '@/hooks/useEvent' import { BATCH_TYPE, LINE_TYPE, MODULE_SETUP_TYPE, POLYGON_TYPE } from '@/common/common' @@ -55,7 +55,7 @@ export function useModuleBasicSetting(tabNum) { const setCurrentObject = useSetRecoilState(currentObjectState) const { setModuleStatisticsData } = useCircuitTrestle() - const { createRoofPolygon, createMarginPolygon, createPaddingPolygon } = useMode() + const { createRoofPolygon, createMarginPolygon, createPaddingPolygon, removeShortEdges } = useMode() const moduleSetupOption = useRecoilValue(moduleSetupOptionState) const setManualSetupMode = useSetRecoilState(toggleManualSetupModeState) @@ -388,10 +388,14 @@ export function useModuleBasicSetting(tabNum) { setSurfaceShapePattern(roof, roofDisplay.column, true, roof.roofMaterial) //패턴 변경 // let offsetPoints = createPaddingPolygon(createRoofPolygon(roof.points), roof.lines).vertices //안쪽 offset let offsetPoints = null - const polygon = createRoofPolygon(roof.getCurrentPoints()) + + // 겹치는 선(같은 방향) 중 짧은 edge를 제거하여 폴리곤 단순화 + const cleaned = removeShortEdges(roof.getCurrentPoints(), roof.lines) + const polygon = createRoofPolygon(cleaned.vertices) + const cleanedLines = cleaned.lines const originPolygon = new QPolygon(roof.getCurrentPoints(), { fontSize: 0 }) - let result = createPaddingPolygon(polygon, roof.lines).vertices + let result = createPaddingPolygon(polygon, cleanedLines).vertices //margin polygon 의 point가 기준 polygon의 밖에 있는지 판단한다. const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) @@ -404,12 +408,14 @@ export function useModuleBasicSetting(tabNum) { } else { //육지붕이 아닐때 if (allPointsOutside) { - offsetPoints = createMarginPolygon(polygon, roof.lines).vertices + offsetPoints = createMarginPolygon(polygon, cleanedLines).vertices } else { - offsetPoints = createPaddingPolygon(polygon, roof.lines).vertices + offsetPoints = createPaddingPolygon(polygon, cleanedLines).vertices } // 자기교차(꼬임) 제거 offsetPoints = cleanSelfIntersectingPolygon(offsetPoints) + // 둔각 꼭짓점에서 offset 선이 지붕 바깥으로 튀어나가는 문제 수정 + offsetPoints = clipOffsetToOriginal(offsetPoints, roof.getCurrentPoints()) } //모듈설치영역?? 생성 diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 8eebe849..69c2db78 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1675,14 +1675,81 @@ export function useMode() { * @param arcSegments * @returns {{vertices, edges: *[], minX: *, minY: *, maxX: *, maxY: *}} */ + /** + * 같은 방향(각도)으로 겹치는 edge들을 감지하여, 가장 긴 선만 유지한다. + * 겹치는 짧은 edge의 꼭짓점을 제거하고 폴리곤을 단순화한다. + * @param {Array} vertices - 폴리곤 꼭짓점 배열 + * @param {Array} lines - 대응하는 QLine 배열 + * @returns {{ vertices: Array, lines: Array }} 정리된 꼭짓점과 lines + */ + function removeShortEdges(vertices, lines) { + if (!vertices || vertices.length < 4) return { vertices, lines } + + // 각 edge의 각도와 길이를 계산 + function edgeAngle(v1, v2) { + const rad = Math.atan2(v2.y - v1.y, v2.x - v1.x) + return Math.round(rad * (180 / Math.PI)) + } + function edgeLength(v1, v2) { + const dx = v2.x - v1.x + const dy = v2.y - v1.y + return Math.sqrt(dx * dx + dy * dy) + } + + // 같은 방향인지 판단 (동일 각도 또는 정반대 180도) + function isSameDirection(angle1, angle2) { + const diff = Math.abs(angle1 - angle2) % 360 + return diff <= 1 || Math.abs(diff - 180) <= 1 + } + + // 연속된 같은 방향 edge를 그룹으로 묶기 + const n = vertices.length + const edges = [] + for (let i = 0; i < n; i++) { + const next = (i + 1) % n + edges.push({ + index: i, + angle: edgeAngle(vertices[i], vertices[next]), + length: edgeLength(vertices[i], vertices[next]), + }) + } + + const removeIndices = new Set() + + for (let i = 0; i < n; i++) { + const next = (i + 1) % n + // 인접한 두 edge가 같은 방향이면 겹치는 선 + if (isSameDirection(edges[i].angle, edges[next].angle)) { + // 짧은 쪽을 제거 대상으로 표시 + if (edges[i].length <= edges[next].length) { + removeIndices.add(i) + } else { + removeIndices.add(next) + } + } + } + + if (removeIndices.size === 0) return { vertices, lines } + + // 폴리곤이 3개 미만이 되지 않도록 보호 + if (n - removeIndices.size < 3) return { vertices, lines } + + const cleanedVertices = [] + const cleanedLines = [] + + for (let i = 0; i < n; i++) { + if (removeIndices.has(i)) continue + cleanedVertices.push(vertices[i]) + cleanedLines.push(lines[i % lines.length]) + } + + return { vertices: cleanedVertices, lines: cleanedLines } + } + function createMarginPolygon(polygon, lines, arcSegments = 0) { const offsetEdges = [] polygon.edges.forEach((edge, i) => { - /* const offset = - lines[i % lines.length].attributes.offset === undefined || lines[i % lines.length].attributes.offset === 0 - ? 0.1 - : lines[i % lines.length].attributes.offset*/ const offset = lines[i % lines.length].attributes.offset const dx = edge.outwardNormal.x * offset const dy = edge.outwardNormal.y * offset @@ -1718,10 +1785,6 @@ export function useMode() { const offsetEdges = [] polygon.edges.forEach((edge, i) => { - /*const offset = - lines[i % lines.length].attributes.offset === undefined || lines[i % lines.length].attributes.offset === 0 - ? 0.1 - : lines[i % lines.length].attributes.offset*/ const offset = lines[i % lines.length].attributes.offset const dx = edge.inwardNormal.x * offset @@ -5774,5 +5837,6 @@ export function useMode() { createRoofPolygon, createMarginPolygon, createPaddingPolygon, + removeShortEdges, } } diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 4937a0ed..bf4c87cb 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -255,6 +255,40 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) { * @param {Array} vertices - [{x, y}, ...] 형태의 점 배열 * @returns {Array} 정리된 점 배열 */ +/** + * offset 폴리곤을 원본 폴리곤 경계 안으로 클리핑한다. + * 둔각 꼭짓점에서 offset 교차점이 원본 바깥으로 튀어나가는 문제를 해결한다. + * @param {Array} offsetVertices - offset된 점 배열 [{x, y}, ...] + * @param {Array} originalVertices - 원본 폴리곤 점 배열 [{x, y}, ...] + * @returns {Array} 클리핑된 점 배열 + */ +export function clipOffsetToOriginal(offsetVertices, originalVertices) { + if (!offsetVertices || offsetVertices.length < 3) return offsetVertices + if (!originalVertices || originalVertices.length < 3) return offsetVertices + + try { + const offsetCoords = offsetVertices.map((p) => [p.x, p.y]) + offsetCoords.push([offsetVertices[0].x, offsetVertices[0].y]) + + const origCoords = originalVertices.map((p) => [p.x, p.y]) + origCoords.push([originalVertices[0].x, originalVertices[0].y]) + + const offsetPoly = turf.polygon([offsetCoords]) + const origPoly = turf.polygon([origCoords]) + + const clipped = turf.intersect(turf.featureCollection([offsetPoly, origPoly])) + + if (clipped) { + const clippedCoords = clipped.geometry.coordinates[0] + return clippedCoords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + } + } catch (e) { + console.warn('Failed to clip offset polygon to original:', e) + } + + return offsetVertices +} + export function cleanSelfIntersectingPolygon(vertices) { if (!vertices || vertices.length < 3) return vertices From 1681fd85121ff5dcf10e89639bf515aa805200c9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 27 Mar 2026 09:22:59 +0900 Subject: [PATCH 08/21] =?UTF-8?q?[1807]=EA=B1=B0=EC=B9=98=EB=8C=80=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=E2=80=98=EB=82=99=EC=84=A4=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20=EA=B8=88=EA=B5=AC=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=E2=80=99=EA=B0=80=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/basic/step/Trestle.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/floor-plan/modal/basic/step/Trestle.jsx b/src/components/floor-plan/modal/basic/step/Trestle.jsx index 16fca772..dd2e64ca 100644 --- a/src/components/floor-plan/modal/basic/step/Trestle.jsx +++ b/src/components/floor-plan/modal/basic/step/Trestle.jsx @@ -428,7 +428,7 @@ const Trestle = forwardRef((props, ref) => { const newCvrYn = constructionList[index].cvrYn const newCvrChecked = newCvrYn === 'Y' const newSnowGdPossYn = constructionList[index].snowGdPossYn - const newSnowGdChecked = newSnowGdPossYn === 'Y' + const newSnowGdChecked = false setCvrYn(newCvrYn) setSnowGdPossYn(newSnowGdPossYn) From 6cf556002eaf2452091aed786a4e14529bc52155 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 27 Mar 2026 09:47:19 +0900 Subject: [PATCH 09/21] =?UTF-8?q?[1807]=EA=B1=B0=EC=B9=98=EB=8C=80=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=E2=80=98=EB=82=99=EC=84=A4=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80=20=EA=B8=88=EA=B5=AC=20=EC=84=A4=EC=B9=98?= =?UTF-8?q?=E2=80=99=EA=B0=80=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useModuleTabContents.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/module/useModuleTabContents.js b/src/hooks/module/useModuleTabContents.js index f7484bdb..5f82f939 100644 --- a/src/hooks/module/useModuleTabContents.js +++ b/src/hooks/module/useModuleTabContents.js @@ -310,7 +310,7 @@ export function useModuleTabContents({ tabIndex, addRoof, setAddedRoofs, roofTab } else { // 최초 선택: 설치 가능하면 디폴트 체크 const defaultCvr = selectedConstruction.cvrYn === 'Y' - const defaultSnowGd = selectedConstruction.snowGdPossYn === 'Y' + const defaultSnowGd = false selectedConstruction.setupCover = defaultCvr selectedConstruction.setupSnowCover = defaultSnowGd setCvrChecked(defaultCvr) From 7b8b995e1cecbb968328cbfba83bcf9bf988d46b Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Fri, 27 Mar 2026 10:46:38 +0900 Subject: [PATCH 10/21] =?UTF-8?q?1805=20=EC=84=A4=EC=B9=98=EC=98=81?= =?UTF-8?q?=EC=97=AD=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=95=88=EB=90=98=EB=8A=94=20=ED=98=84=EC=83=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/module/useModuleBasicSetting.js | 6 +-- src/hooks/useMode.js | 30 ++++++----- src/util/qpolygon-utils.js | 62 +++++++++++++++-------- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/hooks/module/useModuleBasicSetting.js b/src/hooks/module/useModuleBasicSetting.js index cc315e32..ef477e67 100644 --- a/src/hooks/module/useModuleBasicSetting.js +++ b/src/hooks/module/useModuleBasicSetting.js @@ -395,7 +395,7 @@ export function useModuleBasicSetting(tabNum) { const cleanedLines = cleaned.lines const originPolygon = new QPolygon(roof.getCurrentPoints(), { fontSize: 0 }) - let result = createPaddingPolygon(polygon, cleanedLines).vertices + let result = createPaddingPolygon(polygon, cleanedLines, 0, true).vertices //margin polygon 의 point가 기준 polygon의 밖에 있는지 판단한다. const allPointsOutside = result.every((point) => !originPolygon.inPolygon(point)) @@ -408,9 +408,9 @@ export function useModuleBasicSetting(tabNum) { } else { //육지붕이 아닐때 if (allPointsOutside) { - offsetPoints = createMarginPolygon(polygon, cleanedLines).vertices + offsetPoints = createMarginPolygon(polygon, cleanedLines, 0, true).vertices } else { - offsetPoints = createPaddingPolygon(polygon, cleanedLines).vertices + offsetPoints = createPaddingPolygon(polygon, cleanedLines, 0, true).vertices } // 자기교차(꼬임) 제거 offsetPoints = cleanSelfIntersectingPolygon(offsetPoints) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 69c2db78..8492042a 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1746,7 +1746,7 @@ export function useMode() { return { vertices: cleanedVertices, lines: cleanedLines } } - function createMarginPolygon(polygon, lines, arcSegments = 0) { + function createMarginPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) { const offsetEdges = [] polygon.edges.forEach((edge, i) => { @@ -1761,11 +1761,14 @@ export function useMode() { offsetEdges.forEach((thisEdge, i) => { const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] const vertex = edgesIntersection(prevEdge, thisEdge) - if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { - vertices.push({ - x: vertex.x, - y: vertex.y, - }) + if (vertex) { + if (!vertex.isIntersectionOutside || !bevelJoin) { + vertices.push({ x: vertex.x, y: vertex.y }) + } else { + // 둔각 + bevelJoin: offset edge 끝점 사용 + vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y }) + vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y }) + } } }) @@ -1781,7 +1784,7 @@ export function useMode() { * @param arcSegments * @returns {{vertices, edges: *[], minX: *, minY: *, maxX: *, maxY: *}} */ - function createPaddingPolygon(polygon, lines, arcSegments = 0) { + function createPaddingPolygon(polygon, lines, arcSegments = 0, bevelJoin = false) { const offsetEdges = [] polygon.edges.forEach((edge, i) => { @@ -1797,11 +1800,14 @@ export function useMode() { offsetEdges.forEach((thisEdge, i) => { const prevEdge = offsetEdges[(i + offsetEdges.length - 1) % offsetEdges.length] const vertex = edgesIntersection(prevEdge, thisEdge) - if (vertex && (!vertex.isIntersectionOutside || arcSegments < 1)) { - vertices.push({ - x: vertex.x, - y: vertex.y, - }) + if (vertex) { + if (!vertex.isIntersectionOutside || !bevelJoin) { + vertices.push({ x: vertex.x, y: vertex.y }) + } else { + // 둔각 + bevelJoin: offset edge 끝점 사용 + vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y }) + vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y }) + } } }) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index bf4c87cb..adee3ff3 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -256,8 +256,8 @@ function createPaddingPolygon(polygon, offset, arcSegments = 0) { * @returns {Array} 정리된 점 배열 */ /** - * offset 폴리곤을 원본 폴리곤 경계 안으로 클리핑한다. - * 둔각 꼭짓점에서 offset 교차점이 원본 바깥으로 튀어나가는 문제를 해결한다. + * offset 폴리곤의 꼭짓점 중 원본 폴리곤 바깥에 나간 점을 + * 원본 폴리곤 경계 위의 가장 가까운 점으로 투영한다. * @param {Array} offsetVertices - offset된 점 배열 [{x, y}, ...] * @param {Array} originalVertices - 원본 폴리곤 점 배열 [{x, y}, ...] * @returns {Array} 클리핑된 점 배열 @@ -266,27 +266,49 @@ export function clipOffsetToOriginal(offsetVertices, originalVertices) { if (!offsetVertices || offsetVertices.length < 3) return offsetVertices if (!originalVertices || originalVertices.length < 3) return offsetVertices - try { - const offsetCoords = offsetVertices.map((p) => [p.x, p.y]) - offsetCoords.push([offsetVertices[0].x, offsetVertices[0].y]) - - const origCoords = originalVertices.map((p) => [p.x, p.y]) - origCoords.push([originalVertices[0].x, originalVertices[0].y]) - - const offsetPoly = turf.polygon([offsetCoords]) - const origPoly = turf.polygon([origCoords]) - - const clipped = turf.intersect(turf.featureCollection([offsetPoly, origPoly])) - - if (clipped) { - const clippedCoords = clipped.geometry.coordinates[0] - return clippedCoords.slice(0, -1).map((c) => ({ x: c[0], y: c[1] })) + // 점이 폴리곤 내부에 있는지 판단 (ray casting) + function pointInPolygon(point, polygon) { + let inside = false + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const xi = polygon[i].x, yi = polygon[i].y + const xj = polygon[j].x, yj = polygon[j].y + if (((yi > point.y) !== (yj > point.y)) && + (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi)) { + inside = !inside + } } - } catch (e) { - console.warn('Failed to clip offset polygon to original:', e) + return inside } - return offsetVertices + // 점을 선분 위의 가장 가까운 점으로 투영 + function projectOnSegment(p, a, b) { + const dx = b.x - a.x + const dy = b.y - a.y + const lenSq = dx * dx + dy * dy + if (lenSq === 0) return { x: a.x, y: a.y } + let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq + t = Math.max(0, Math.min(1, t)) + return { x: a.x + t * dx, y: a.y + t * dy } + } + + return offsetVertices.map((p) => { + if (pointInPolygon(p, originalVertices)) return p + + // 바깥에 있으면 원본 폴리곤의 가장 가까운 변 위의 점으로 이동 + let minDist = Infinity + let nearest = p + for (let i = 0; i < originalVertices.length; i++) { + const a = originalVertices[i] + const b = originalVertices[(i + 1) % originalVertices.length] + const proj = projectOnSegment(p, a, b) + const dist = (proj.x - p.x) * (proj.x - p.x) + (proj.y - p.y) * (proj.y - p.y) + if (dist < minDist) { + minDist = dist + nearest = proj + } + } + return nearest + }) } export function cleanSelfIntersectingPolygon(vertices) { From f2b527b77c450998d4a349fc6e94c7d838144d81 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 27 Mar 2026 16:23:06 +0900 Subject: [PATCH 11/21] =?UTF-8?q?top,bottom=20=3D=20in,=20=ED=95=A0?= =?UTF-8?q?=EB=8B=B9=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roofcover/useRoofAllocationSetting.js | 23 ++- src/hooks/usePolygon.js | 2 +- src/util/skeleton-utils.js | 155 ++++++++++++++++-- 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index c31797cf..568e2dc8 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -444,15 +444,30 @@ export function useRoofAllocationSetting(id) { const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) roofBases.forEach((roofBase) => { try { - const roofEaveHelpLines = canvas.getObjects().filter((obj) => obj.lineName === 'eaveHelpLine' && obj.roofId === roofBase.id) + // 지붕 할당 로직에 extensionLine 추가 + const roofEaveHelpLines = canvas.getObjects().filter((obj) => + (obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine') && + obj.roofId === roofBase.id + ) + // console.log('roofBase.id:', roofBase.id) + // console.log('roofEaveHelpLines:', roofEaveHelpLines) + // console.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine')) if (roofEaveHelpLines.length > 0) { if (roofBase.lines) { // Filter out any eaveHelpLines that are already in lines to avoid duplicates const existingEaveLineIds = new Set(roofBase.lines.map((line) => line.id)) const newEaveLines = roofEaveHelpLines.filter((line) => !existingEaveLineIds.has(line.id)) // Filter out lines from roofBase.lines that share any points with newEaveLines + + // extensionLine과 일반 eaveHelpLine 분리 + const extensionLines = newEaveLines.filter(line => line.lineName === 'extensionLine') + const normalEaveLines = newEaveLines.filter(line => line.lineName === 'eaveHelpLine') + // console.log('extensionLines count:', extensionLines.length) + // console.log('normalEaveLines count:', normalEaveLines.length) + + // 일반 eaveHelpLine만 Overlap 판단에 사용 const linesToKeep = roofBase.lines.filter(roofLine => { - const shouldRemove = newEaveLines.some(eaveLine => { + const shouldRemove = normalEaveLines.some(eaveLine => { // 1. 기본적인 포인트 일치 확인 const rX1 = roofLine.x1, rY1 = roofLine.y1, rX2 = roofLine.x2, rY2 = roofLine.y2; const eX1 = eaveLine.x1, eY1 = eaveLine.y1, eX2 = eaveLine.x2, eY2 = eaveLine.y2; @@ -501,7 +516,9 @@ export function useRoofAllocationSetting(id) { return !shouldRemove; }); // Combine remaining lines with newEaveLines - roofBase.lines = [...linesToKeep, ...newEaveLines]; + roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines]; + // console.log('Final roofBase.lines count:', roofBase.lines.length) + // console.log('extensionLines in final:', roofBase.lines.filter(l => l.lineName === 'extensionLine').length) } else { roofBase.lines = [...roofEaveHelpLines] } diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 3a83c947..2536b9f0 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -821,7 +821,7 @@ export const usePolygon = () => { if (innerLine.attributes && polygonLine.attributes.type) { // innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경 if (polygonLine.length < innerLine.length) { - if (polygonLine.lineName !== 'eaveHelpLine' || polygonLine.lineName !== 'eaveHelpLine') { + if (polygonLine.lineName !== 'eaveHelpLine' && polygonLine.lineName !== 'extensionLine') { polygonLine.need = false } } diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index a7868622..0daeb1c7 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -465,6 +465,36 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체 if (moveFlowLine !== 0 || moveUpDown !== 0) { const movedPoints = movingLineFromSkeleton(roofId, canvas) + console.log("movedPoints:::", movedPoints); + + // movedPoints를 roof 경계로 확장 + // 이번 이동에서 실제로 변경된 포인트만 구분하여, 변경되지 않은 축만 roof 경계로 확장 + const oldPoints = canvas?.skeleton?.lastPoints ?? roofLinePoints + const tolerance = 0.1 + const correctedPoints = movedPoints.map((mp, i) => { + const rp = roofLinePoints[i] + const op = oldPoints[i] + if (!rp || !op) return mp + const xMatch = Math.abs(mp.x - rp.x) < tolerance + const yMatch = Math.abs(mp.y - rp.y) < tolerance + if (xMatch && yMatch) return mp + // 이번 이동에서 실제로 변경된 축 판별 (oldPoints vs movedPoints) + const xMoved = Math.abs(mp.x - op.x) >= tolerance + const yMoved = Math.abs(mp.y - op.y) >= tolerance + let newX = mp.x + let newY = mp.y + // 이번에 이동되지 않았는데 roof 경계와 다른 축만 교정 + if (!xMoved && !xMatch) newX = rp.x + if (!yMoved && !yMatch) newY = rp.y + if (newX !== mp.x || newY !== mp.y) { + console.log(`point[${i}] 교정: mp=(${mp.x}, ${mp.y}) → (${newX}, ${newY}), op=(${op.x}, ${op.y}), xMoved=${xMoved}, yMoved=${yMoved}`) + } + return { x: newX, y: newY } + }) + + // roofLineContactPoints = correctedPoints + // changRoofLinePoints = correctedPoints + roofLineContactPoints = movedPoints changRoofLinePoints = movedPoints } @@ -828,11 +858,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //roofline 외곽선 설정 - console.log('index::::', index) - console.log('roofLine:', roofLine.x1, roofLine.y1, roofLine.x2, roofLine.y2) - console.log('wallLine:', wallLine.x1, wallLine.y1, wallLine.x2, wallLine.y2) - console.log('wallBaseLine:', wallBaseLine.x1, wallBaseLine.y1, wallBaseLine.x2, wallBaseLine.y2) - console.log('isSamePoint result:', isSameLine2(wallBaseLine, wallLine)) + // console.log('index::::', index) + // console.log('roofLine:', roofLine.x1, roofLine.y1, roofLine.x2, roofLine.y2) + // console.log('wallLine:', wallLine.x1, wallLine.y1, wallLine.x2, wallLine.y2) + // console.log('wallBaseLine:', wallBaseLine.x1, wallBaseLine.y1, wallBaseLine.x2, wallBaseLine.y2) + // console.log('isSamePoint result:', isSameLine2(wallBaseLine, wallLine)) const isCollinear = (l1, l2, tolerance = 0.1) => { const slope1 = Math.abs(l1.x2 - l1.x1) < tolerance ? Infinity : (l1.y2 - l1.y1) / (l1.x2 - l1.x1) @@ -871,7 +901,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //현재 roof는 무조건 시계방향 - const getAddLine = (p1, p2, stroke = '') => { + const getAddLine = (p1, p2, stroke = '', lineType = 'eaveHelpLine') => { movedLines.push({ index, p1, p2 }) const dx = Math.abs(p2.x - p1.x); @@ -884,14 +914,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { fontSize: roof.fontSize, stroke: 'black', strokeWidth: 4, - name: 'eaveHelpLine', - lineName: 'eaveHelpLine', + name: lineType, + lineName: lineType, visible: true, roofId: roofId, selectable: true, hoverCursor: 'pointer', attributes: { - type: 'eaveHelpLine', + type: lineType, isStart: true, pitch: wallLine.attributes.pitch, planeSize: calcLinePlaneSize({ x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y }), @@ -970,7 +1000,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const pLineX = sortRoofLines[prevIndex].x1 getAddLine({ x: newPStart.x, y: newPStart.y }, { x: ePoint.x, y: ePoint.y }, 'blue') - getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') + // getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') if (Math.abs(wallBaseLine.y1 - wallLine.y1) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') @@ -1000,7 +1030,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const pLineX = sortRoofLines[nextIndex].x2 getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') - getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + // getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') @@ -1179,7 +1209,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const pLineX = sortRoofLines[nextIndex].x2 getAddLine({ x: newPEnd.x, y: newPEnd.y }, { x: ePoint.x, y: ePoint.y }, 'blue') - getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: newPointX, y: roofLine.y1 }, 'orange') + //getAddLine({ x: roofLine.x1, y: Big(roofLine.y1).minus(moveDist).toNumber() }, { x: newPointX, y: Big(roofLine.y1).minus(moveDist).toNumber()}, 'orange') if (Math.abs(wallBaseLine.y2 - wallLine.y2) < 0.1) { getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') @@ -1384,6 +1414,56 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') getAddLine(newPStart, newPEnd, 'red') } + if(!isStartEnd.start && !isStartEnd.end){ + console.log('top_in start and end false') + + const moveDistY1 = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(wallLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + + const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } + const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + + + if(moveDistX1 > 0) { + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_start' }) + + if(moveDistY1 > moveDistX1){ + const dist = moveDistY1 - moveDistX1 + // console.log('Creating extensionLine (Y1 > X1):', { sPoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1, y: roofLine.y1 + dist }, 'orange','extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + }else{ + const dist = moveDistX1 - moveDistY1 + // console.log('Creating extensionLine (X1 > Y1):', { sPoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1 - dist, y: roofLine.y1 }, 'orange','extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + } + + } + if(moveDistX2 > 0) { + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'top_in_end' }) + + if(moveDistY2 > moveDistX2){ + const dist = moveDistY2 - moveDistX2 + // getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: roofLine.y2 - dist }, 'red') + // console.log('Creating extensionLine (Y2 > X2):', { ePoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2, y: roofLine.y2 + dist }, 'orange', 'extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + }else{ + const dist = moveDistX2 - moveDistY2 + // getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2 - dist, y: roofLine.y2 }, 'red') + // console.log('Creating extensionLine (X2 > Y2):', { ePoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2 + dist, y: roofLine.y2 }, 'orange', 'extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + } + } + + } } else if (condition === 'top_out') { console.log('top_out isStartEnd:::::::', isStartEnd) @@ -1557,6 +1637,57 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') getAddLine(newPStart, newPEnd, 'red') } + + if(!isStartEnd.start && !isStartEnd.end){ + + console.log('isStartEnd:::::', isStartEnd) + + const moveDistY1 = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(wallLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() + + const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } + const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + + + if(moveDistX1 > 0) { + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_start' }) + + if(moveDistY1 > moveDistX1){ + const dist = moveDistY1 - moveDistX1 + // console.log('Creating extensionLine (Y1 > X1):', { sPoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1, y: roofLine.y1 - dist }, 'orange','extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + }else{ + const dist = moveDistX1 - moveDistY1 + // console.log('Creating extensionLine (X1 > Y1):', { sPoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1 + dist, y: roofLine.y1 }, 'orange','extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + } + + } + if(moveDistX2 > 0) { + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'bottom_in_end' }) + + if(moveDistY2 > moveDistX2){ + const dist = moveDistY2 - moveDistX2 + // getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: roofLine.y2 - dist }, 'red') + // console.log('Creating extensionLine (Y2 > X2):', { ePoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2, y: roofLine.y2 - dist }, 'orange', 'extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + }else{ + const dist = moveDistX2 - moveDistY2 + // getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2 - dist, y: roofLine.y2 }, 'red') + // console.log('Creating extensionLine (X2 > Y2):', { ePoint, roofLine, dist, roofId }) + const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2 - dist, y: roofLine.y2 }, 'orange', 'extensionLine') + console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + + } + } + } } else if (condition === 'bottom_out') { console.log('bottom_out isStartEnd:::::::', isStartEnd) if (isStartEnd.start) { From b1c35376da012c1b685f7b58a0b14b3ffddf5425 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 30 Mar 2026 15:37:12 +0900 Subject: [PATCH 12/21] =?UTF-8?q?=EC=88=98=EB=8F=99=20=EB=B0=B0=EC=B9=98?= =?UTF-8?q?=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=EB=AC=BC=EB=96=BC=EC=84=B8?= =?UTF-8?q?=20=EB=B0=B0=EC=B9=98=20=ED=95=9C=EB=8B=A4=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/floor-plan/modal/basic/BasicSetting.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/floor-plan/modal/basic/BasicSetting.jsx b/src/components/floor-plan/modal/basic/BasicSetting.jsx index 983492bf..04dbbcb0 100644 --- a/src/components/floor-plan/modal/basic/BasicSetting.jsx +++ b/src/components/floor-plan/modal/basic/BasicSetting.jsx @@ -17,6 +17,7 @@ import { currentCanvasPlanState, isManualModuleLayoutSetupState, isManualModuleSetupState, + moduleSetupOptionState, toggleManualSetupModeState, } from '@/store/canvasAtom' import { loginUserStore } from '@/store/commonAtom' @@ -47,6 +48,7 @@ export default function BasicSetting({ id, pos = { x: 50, y: 230 } }) { const [checkedModules, setCheckedModules] = useRecoilState(checkedModuleState) const [roofs, setRoofs] = useState(addedRoofs) const [manualSetupMode, setManualSetupMode] = useRecoilState(toggleManualSetupModeState) + const [moduleSetupOption, setModuleSetupOption] = useRecoilState(moduleSetupOptionState) const [layoutSetup, setLayoutSetup] = useState([{}]) const { selectedModules, @@ -211,8 +213,12 @@ export default function BasicSetting({ id, pos = { x: 50, y: 230 } }) { } const handleManualModuleSetup = () => { - setManualSetupMode(`manualSetup_${!isManualModuleSetup}`) - setIsManualModuleSetup(!isManualModuleSetup) + const nextManual = !isManualModuleSetup + setManualSetupMode(`manualSetup_${nextManual}`) + setIsManualModuleSetup(nextManual) + if (nextManual) { + setModuleSetupOption((prev) => ({ ...prev, isChidori: true })) + } } const handleManualModuleLayoutSetup = () => { From 13cd92114fc337076ec5b48b62744a10deeb568c Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 30 Mar 2026 16:36:46 +0900 Subject: [PATCH 13/21] =?UTF-8?q?top,bottom=20=3D=20in,=20=ED=95=A0?= =?UTF-8?q?=EB=8B=B9=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 0daeb1c7..90601c15 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -463,6 +463,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { }) // 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체 + if (moveFlowLine !== 0 || moveUpDown !== 0) { const movedPoints = movingLineFromSkeleton(roofId, canvas) console.log("movedPoints:::", movedPoints); @@ -981,6 +982,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { isIn = true if (isStartEnd.start) { + console.log('left_in::::isStartEnd:::::', isStartEnd) newPEnd.y = roofLine.y2 newPEnd.x = roofLine.x2 @@ -1011,6 +1013,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('left_in::::isStartEnd:::::', isStartEnd) newPStart.y = roofLine.y1 newPStart.x = roofLine.x1 @@ -1100,6 +1103,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('left_in::::isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() const bStartY = Big(wallLine.y2).plus(moveDist).toNumber() @@ -1161,6 +1165,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } } else if (condition === 'right_in') { if (isStartEnd.start) { + console.log('right_in::::isStartEnd:::::', isStartEnd) newPEnd.y = roofLine.y2 newPEnd.x = roofLine.x2 @@ -1190,6 +1195,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('left_in::::isStartEnd:::::', isStartEnd) newPStart.y = roofLine.y1 newPStart.x = roofLine.x1 @@ -1359,6 +1365,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { if (condition === 'top_in') { if (isStartEnd.start) { + console.log('top_in start') const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } newPStart.x = wallBaseLine.x1 @@ -1387,6 +1394,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('top_in end', isStartEnd) const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } newPEnd.x = wallBaseLine.x2 @@ -1434,12 +1442,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (Y1 > X1):', { sPoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1, y: roofLine.y1 + dist }, 'orange','extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) }else{ const dist = moveDistX1 - moveDistY1 // console.log('Creating extensionLine (X1 > Y1):', { sPoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1 - dist, y: roofLine.y1 }, 'orange','extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) } } @@ -1452,6 +1466,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (Y2 > X2):', { ePoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2, y: roofLine.y2 + dist }, 'orange', 'extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) }else{ const dist = moveDistX2 - moveDistY2 @@ -1459,6 +1476,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (X2 > Y2):', { ePoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2 + dist, y: roofLine.y2 }, 'orange', 'extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) } } @@ -1524,6 +1544,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine(newPStart, newPEnd, 'red') } if (isStartEnd.end) { + console.log('isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x2).minus(moveDist).toNumber() const bStartX = Big(wallLine.x2).minus(moveDist).toNumber() @@ -1581,7 +1602,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { getAddLine(newPStart, newPEnd, 'red') } } else if (condition === 'bottom_in') { + console.log('bottom_in::::isStartEnd:::::', isStartEnd) if (isStartEnd.start) { + console.log('isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } newPStart.x = wallBaseLine.x1 @@ -1610,6 +1633,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.y2).minus(wallBaseLine.y2).abs().toNumber() sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } newPEnd.x = wallBaseLine.x2 @@ -1659,12 +1683,18 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (Y1 > X1):', { sPoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1, y: roofLine.y1 - dist }, 'orange','extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) }else{ const dist = moveDistX1 - moveDistY1 // console.log('Creating extensionLine (X1 > Y1):', { sPoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: sPoint.x, y: sPoint.y }, { x: roofLine.x1 + dist, y: roofLine.y1 }, 'orange','extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) } } @@ -1677,6 +1707,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (Y2 > X2):', { ePoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2, y: roofLine.y2 - dist }, 'orange', 'extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) }else{ const dist = moveDistX2 - moveDistY2 @@ -1684,6 +1717,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // console.log('Creating extensionLine (X2 > Y2):', { ePoint, roofLine, dist, roofId }) const createdLine = getAddLine({ x: ePoint.x, y: ePoint.y }, { x: roofLine.x2 - dist, y: roofLine.y2 }, 'orange', 'extensionLine') console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + createdLine.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#1083E3', strokeWidth: 2 }) + createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } + innerLines.push(createdLine) } } @@ -1691,6 +1727,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } else if (condition === 'bottom_out') { console.log('bottom_out isStartEnd:::::::', isStartEnd) if (isStartEnd.start) { + console.log('isStartEnd:::::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x1).minus(moveDist).toNumber() const bStartX = Big(wallLine.x1).minus(moveDist).toNumber() @@ -1749,6 +1786,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { + console.log('isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x2).plus(moveDist).toNumber() const bStartX = Big(wallLine.x2).plus(moveDist).toNumber() From 2514fc92148f6d8f1e6f63b3dfbc038c66f476e9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 30 Mar 2026 18:34:57 +0900 Subject: [PATCH 14/21] =?UTF-8?q?[1480]=EC=82=AC=EC=9D=98=20=EA=B0=81?= =?UTF-8?q?=EB=8F=84=20=EC=84=A4=EC=A0=95=EC=9D=84=20"=EA=B0=81=EB=8F=84"?= =?UTF-8?q?=EB=A1=9C=20=ED=96=88=EC=9D=84=20=EB=95=8C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useCanvasConfigInitialize.js | 4 ++-- src/hooks/usePolygon.js | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/hooks/common/useCanvasConfigInitialize.js b/src/hooks/common/useCanvasConfigInitialize.js index 46b71588..df4025d8 100644 --- a/src/hooks/common/useCanvasConfigInitialize.js +++ b/src/hooks/common/useCanvasConfigInitialize.js @@ -47,12 +47,12 @@ export function useCanvasConfigInitialize() { offsetTexts.forEach((obj) => { let text = `${!obj.originText ? '' : obj.originText}` if (!obj.onlyOffset) { - text = text + `-∠${getDegreeByChon(obj.pitch)}${angleUnit}` + text = text + `-∠${obj.roofAngle ?? getDegreeByChon(obj.pitch)}${angleUnit}` } obj.set({ text: text }) }) flowTexts.forEach((obj) => { - obj.set({ text: `${!obj.originText ? '' : obj.originText + '-'}∠${getDegreeByChon(obj.pitch)}${pitchText}` }) + obj.set({ text: `${!obj.originText ? '' : obj.originText + '-'}∠${obj.roofAngle ?? getDegreeByChon(obj.pitch)}${pitchText}` }) }) } diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 2536b9f0..9b17419e 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -333,6 +333,7 @@ export const usePolygon = () => { moduleCompass: polygon.moduleCompass, visible: isFlowDisplay, pitch: polygon.roofMaterial?.pitch ?? 4, + roofAngle: polygon.roofMaterial?.angle, parentId: polygon.id, }) arrow.setViewLengthText(false) @@ -348,7 +349,7 @@ export const usePolygon = () => { const drawDirectionStringToArrow2 = (polygon, showDirectionText) => { let { direction, surfaceCompass, moduleCompass, arrow } = polygon if (moduleCompass === null || moduleCompass === undefined) { - const textObj = new fabric.Text(`${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText}`, { + const textObj = new fabric.Text(`${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : (arrow.roofAngle ?? getDegreeByChon(arrow.pitch))}${pitchText}`, { fontFamily: flowFontOptions.fontFamily.value, fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', fontStyle: flowFontOptions.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', @@ -357,6 +358,7 @@ export const usePolygon = () => { originX: 'center', originY: 'center', pitch: arrow.pitch, + roofAngle: arrow.roofAngle, name: 'flowText', selectable: false, left: arrow.stickeyPoint.x, @@ -490,7 +492,7 @@ export const usePolygon = () => { text = text + (sameDirectionCnt.length + 1) polygon.set('directionText', text) const textObj = new fabric.Text( - `${showDirectionText && text} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})`, + `${showDirectionText && text} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : (arrow.roofAngle ?? getDegreeByChon(arrow.pitch))}${pitchText})`, { fontFamily: flowFontOptions.fontFamily.value, fontWeight: flowFontOptions.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', @@ -498,6 +500,7 @@ export const usePolygon = () => { fontSize: flowFontOptions.fontSize.value, fill: flowFontOptions.fontColor.value, pitch: arrow.pitch, + roofAngle: arrow.roofAngle, originX: 'center', originY: 'center', name: 'flowText', @@ -707,7 +710,7 @@ export const usePolygon = () => { const addTextByArrows = (arrows, txt, canvas) => { arrows.forEach((arrow, index) => { // const textStr = `${txt}${index + 1} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})` - const textStr = `${txt} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : getDegreeByChon(arrow.pitch)}${pitchText})` + const textStr = `${txt} (${currentAngleType === ANGLE_TYPE.SLOPE ? arrow.pitch : (arrow.roofAngle ?? getDegreeByChon(arrow.pitch))}${pitchText})` const text = new fabric.Text(`${textStr}`, { fontFamily: flowFontOptions.fontFamily.value, @@ -716,6 +719,7 @@ export const usePolygon = () => { fontSize: flowFontOptions.fontSize.value, fill: flowFontOptions.fontColor.value, pitch: arrow.pitch, + roofAngle: arrow.roofAngle, originX: 'center', originY: 'center', name: 'flowText', From 4a270ba57e06ef3edec35e936a8db2431d59a026 Mon Sep 17 00:00:00 2001 From: ysCha Date: Mon, 30 Mar 2026 18:49:06 +0900 Subject: [PATCH 15/21] =?UTF-8?q?[1480]=EC=82=AC=EC=9D=98=20=EA=B0=81?= =?UTF-8?q?=EB=8F=84=20=EC=84=A4=EC=A0=95=EC=9D=84=20"=EA=B0=81=EB=8F=84"?= =?UTF-8?q?=EB=A1=9C=20=ED=96=88=EC=9D=84=20=EB=95=8C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal/placementShape/PlacementShapeSetting.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index 9ba4e342..fbf387bb 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -266,14 +266,15 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla }, { skipSideEffects: true }) const roofs = canvas.getObjects().filter((obj) => obj.roofMaterial?.index === 0) + const firstRoof = roofs[0] - roofs.forEach((roof) => { + if (firstRoof) { /** 모양 패턴 설정 */ - setSurfaceShapePattern(roof, roofDisplay.column, false, { ...roofInfo }) - roof.roofMaterial = { ...roofInfo } - drawDirectionArrow(roof) - setPolygonLinesActualSize(roof, true) - }) + setSurfaceShapePattern(firstRoof, roofDisplay.column, false, { ...roofInfo }) + firstRoof.roofMaterial = { ...roofInfo } + drawDirectionArrow(firstRoof) + setPolygonLinesActualSize(firstRoof, true) + } canvas.renderAll() /** 지붕면 존재 여부에 따라 메뉴 설정 */ From 910974f81b3b0d720055fa7f09daaa9ad918df7f Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 31 Mar 2026 10:18:46 +0900 Subject: [PATCH 16/21] =?UTF-8?q?getRoofMaterialList=20api=EC=97=90?= =?UTF-8?q?=EB=9F=AC10=EB=B2=88=EC=8B=9C=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useMasterController.js | 124 +++++++++++++++++++++--- src/hooks/option/useCanvasSetting.js | 28 ++++-- src/hooks/useAxios.js | 24 +++-- src/locales/ja.json | 9 ++ src/locales/ko.json | 9 ++ 5 files changed, 164 insertions(+), 30 deletions(-) diff --git a/src/hooks/common/useMasterController.js b/src/hooks/common/useMasterController.js index b46adc81..8eb302d1 100644 --- a/src/hooks/common/useMasterController.js +++ b/src/hooks/common/useMasterController.js @@ -7,7 +7,10 @@ import { atom, useRecoilState, useRecoilValue } from 'recoil' // API 요청을 저장할 모듈 레벨 변수 (Hook 외부) -let roofMaterialPromise = null; +// 전역 Promise 캐시 +const roofMaterialPromiseCache = new Map() +let cacheKey = 'default' + /** * 마스터 컨트롤러 훅 * @returns @@ -22,24 +25,117 @@ export function useMasterController() { * 지붕재 목록 조회 * @returns */ + + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + const getRoofMaterialList = async () => { // 1. 이미 진행 중이거나 완료된 Promise가 있으면 그것을 반환 - if (roofMaterialPromise) { - return roofMaterialPromise; + if (roofMaterialPromiseCache.has(cacheKey)) { + console.log('🔄 getRoofMaterialList: Returning cached promise') + return roofMaterialPromiseCache.get(cacheKey) } - // 2. 처음 호출될 때 Promise를 생성하여 변수에 할당 - roofMaterialPromise = get({ url: '/api/v1/master/getRoofMaterialList' }) - .then((res) => { - return res; - }) - .catch((error) => { - // 에러 발생 시 다음 호출을 위해 초기화 - roofMaterialPromise = null; - throw error; - }); + console.log('🚀 getRoofMaterialList: Creating new promise') - return roofMaterialPromise; + const MAX_RETRY = 15 // 15번으로 증가 (일본 지역 대응) + const RETRY_DELAY = 2000 // 2초 간격 + const MAX_BACKOFF = 10000 // 최대 10초까지 지수 백오프 + + const fetchWithRetry = async () => { + let lastError = null + let retryCount = 0 + + for (let attempt = 0; attempt < MAX_RETRY; attempt++) { + try { + // 지수 백오프 적용: 2초, 4초, 8초, ... 최대 10초 + const backoffDelay = Math.min(RETRY_DELAY * Math.pow(2, attempt), MAX_BACKOFF) + if (attempt > 0) { + console.log(`⏳ Waiting ${backoffDelay/1000}s before retry ${attempt + 1}/${MAX_RETRY}`) + await delay(backoffDelay) + } + + console.log(`🌐 API Request ${attempt + 1}/${MAX_RETRY}: /api/v1/master/getRoofMaterialList`) + const res = await get({ url: '/api/v1/master/getRoofMaterialList' }) + console.log('✅ getRoofMaterialList: Success on attempt', attempt + 1) + + // 재시도 중이었으면 성공 메시지 표시 + if (retryCount > 0) { + swalFire({ + icon: 'success', + title: getMessage('server.restored.title'), + text: `${getMessage('server.restored.text')} (${retryCount}${getMessage('server.retries')})`, + timer: 3000, // 메시지 표시 시간 증가 + showConfirmButton: false + }) + } + + return res + } catch (error) { + lastError = error + retryCount++ + + // 500 계열만 기다렸다가 재시도 + const status = error?.response?.status + if (status && status >= 500) { + console.log(`⚠️ getRoofMaterialList: 500 error on attempt ${attempt + 1}, retrying...`) + + // 첫 번째 500 에러에만 메시지 표시 + if (attempt === 0) { + swalFire({ + icon: 'warning', + title: getMessage('server.error.title'), + text: getMessage('server.error.text'), + timer: 4000, // 경고 메시지 시간 증가 + showConfirmButton: false + }) + } + + continue + } + + // 네트워크 에러(ECONNRESET, ETIMEDOUT 등)도 재시도 + if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') { + console.log(`🌐 Network error on attempt ${attempt + 1}:`, error.code) + if (attempt === 0) { + swalFire({ + icon: 'warning', + title: getMessage('network.error.title'), + text: getMessage('network.error.text'), + timer: 4000, + showConfirmButton: false + }) + } + continue + } + + // 500이나 네트워크 에러가 아니면 즉시 실패 + console.log(`❌ getRoofMaterialList: Non-retryable error on attempt ${attempt + 1}:`, error) + throw error + } + } + + // 모든 재시도 실패 시 + console.log('💥 getRoofMaterialList: All retries failed') + swalFire({ + icon: 'error', + title: getMessage('server.failed.title'), + text: `${getMessage('server.failed.text')} (${MAX_RETRY}${getMessage('server.retries')})`, + confirmButtonText: getMessage('common.ok') + }) + + throw lastError + } + + const promise = fetchWithRetry() + .finally(() => { + // 성공/실패 모두 끝나면 캐시에서 제거 + console.log('🏁 getRoofMaterialList: Promise resolved, removing from cache') + roofMaterialPromiseCache.delete(cacheKey) + }) + + // 캐시에 Promise 저장 + roofMaterialPromiseCache.set(cacheKey, promise) + return promise } /** diff --git a/src/hooks/option/useCanvasSetting.js b/src/hooks/option/useCanvasSetting.js index f80b2902..8f339c47 100644 --- a/src/hooks/option/useCanvasSetting.js +++ b/src/hooks/option/useCanvasSetting.js @@ -153,12 +153,12 @@ export function useCanvasSetting(executeEffect = true) { return } - /** 초 1회만 실행하도록 처리 */ - addRoofMaterials() + // Roof materials are now initialized in Header.jsx to avoid duplicate calls }, [executeEffect]) /** - * 지붕재 초기세팅 + * 지붕재 초기세팅 (fallback - primary initialization is in Header.jsx) + * This function is only called when roof materials are needed but not yet loaded */ const addRoofMaterials = async () => { if (roofMaterials.length !== 0) { @@ -320,12 +320,26 @@ export function useCanvasSetting(executeEffect = true) { // objectNo가 전달되면 사용, 아니면 Recoil state의 correntObjectNo 사용 const targetObjectNo = objectNo || correntObjectNo - // 지붕재 데이터가 없으면 먼저 로드 + // 지붕재 데이터가 없으면 Header.jsx에서 로드될 때까지 기다림 let materials = roofMaterials if (!materials || materials.length === 0) { - logger.log('Waiting for roofMaterials to be loaded...') - materials = await addRoofMaterials() - logger.log('roofMaterials loaded:', materials) + logger.log('Waiting for roofMaterials to be loaded from Header.jsx...') + // 최대 2초간 기다림 (20번 × 100ms) + let waitCount = 0 + while ((!materials || materials.length === 0) && waitCount < 20) { + await new Promise(resolve => setTimeout(resolve, 100)) + materials = roofMaterials + waitCount++ + } + + if (!materials || materials.length === 0) { + logger.log('roofMaterials still not loaded after 2 seconds, proceeding without them') + // 비상시에만 addRoofMaterials 호출 (fallback) + materials = await addRoofMaterials() + logger.log('roofMaterials loaded via fallback:', materials) + } else { + logger.log('roofMaterials loaded from Header.jsx:', materials) + } } try { diff --git a/src/hooks/useAxios.js b/src/hooks/useAxios.js index a5c1708a..a2ddc1dc 100644 --- a/src/hooks/useAxios.js +++ b/src/hooks/useAxios.js @@ -86,15 +86,21 @@ export function useAxios(lang = '') { .get(url, option) .then((res) => res.data) .catch((error) => { - console.error('🚨 GET Error:', { - url, - method: 'GET', - timestamp: new Date().toISOString(), - errorMessage: error.message, - status: error.response?.status, - statusText: error.response?.statusText, - config: error.config - }) + // 500 에러는 재시도되므로 로깅을 줄임 + const status = error?.response?.status + if (status === 500) { + console.warn('🔄 Server Error (500): Will retry...', url) + } else { + console.error('🚨 GET Error:', { + url, + method: 'GET', + timestamp: new Date().toISOString(), + errorMessage: error.message, + status: error.response?.status, + statusText: error.response?.statusText, + config: error.config + }) + } throw error }) } diff --git a/src/locales/ja.json b/src/locales/ja.json index feb9945f..13435c2a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -567,6 +567,15 @@ "common.finish": "完了", "common.ok": "確認", "common.cancel": "キャンセル", + "server.error.title": "サーバーエラー", + "server.error.text": "サーバーで一時的な問題が発生しました。自動的に再試行します...", + "server.restored.title": "サーバー接続復旧", + "server.restored.text": "サーバー接続が復旧しました", + "server.failed.title": "サーバー接続失敗", + "server.failed.text": "サーバー接続に失敗しました。しばらくしてから再度お試しください", + "server.retries": "回再試行", + "network.error.title": "ネットワーク接続不安定", + "network.error.text": "インターネット接続が不安定です。自動的に再試行します...", "commons.west": "西", "commons.east": "東", "commons.south": "南", diff --git a/src/locales/ko.json b/src/locales/ko.json index 7e871e60..e3403f02 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -567,6 +567,15 @@ "common.finish": "완료", "common.ok": "확인", "common.cancel": "취소", + "server.error.title": "서버 오류", + "server.error.text": "서버에 일시적인 문제가 발생했습니다. 자동으로 재시도합니다...", + "server.restored.title": "서버 연결 복구", + "server.restored.text": "서버 연결이 복구되었습니다", + "server.failed.title": "서버 연결 실패", + "server.failed.text": "서버 연결에 실패했습니다. 잠시 후 다시 시도해주세요", + "server.retries": "회 재시도", + "network.error.title": "네트워크 연결 불안", + "network.error.text": "인터넷 연결이 불안정합니다. 자동으로 재시도합니다...", "commons.west": "서", "commons.east": "동", "commons.south": "남", From 8d9379c3df56091d378388f9171e76b2a7253e53 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 31 Mar 2026 10:54:18 +0900 Subject: [PATCH 17/21] =?UTF-8?q?=ED=99=95=EB=8C=80=EC=B6=95=EC=86=8C100%?= =?UTF-8?q?=EA=B0=80=EC=9A=B4=EB=8D=B0=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCanvasEvent.js | 65 +++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index 7808bb5f..0c566020 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -15,6 +15,7 @@ export function useCanvasEvent() { const [currentObject, setCurrentObject] = useRecoilState(currentObjectState) const canvasSize = useRecoilValue(canvasSizeState) const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState) + const [shouldCenter, setShouldCenter] = useState(false) // 중앙 정렬 플래그 추가 const lengthTextOption = useRecoilValue(fontSelector('lengthText')) const currentMenu = useRecoilValue(currentMenuState) const { changeCorridorDimensionText } = useText() @@ -23,6 +24,17 @@ export function useCanvasEvent() { useEffect(() => { canvas?.setZoom(canvasZoom / 100) }, [canvasZoom]) + + // canvasZoom이 100으로 변경되고 shouldCenter가 true일 때만 중앙 정렬 실행 + useEffect(() => { + if (canvasZoom === 100 && canvas && shouldCenter) { + setTimeout(() => { + zoomToAllObjects() + canvas.renderAll() + setShouldCenter(false) // 실행 후 플래그 초기화 + }, 100) + } + }, [canvasZoom, shouldCenter]) useEffect(() => { attachDefaultEventOnCanvas() @@ -416,15 +428,35 @@ export function useCanvasEvent() { } const handleZoomClear = () => { + setShouldCenter(true) // 중앙 정렬 플래그 설정 setCanvasZoom(100) - - zoomToAllObjects() - canvas.renderAll() } const zoomToAllObjects = () => { - const objects = canvas.getObjects().filter((obj) => obj.visible) - if (objects.length === 0) return + const allObjects = canvas.getObjects() + // 주요 도형 객체만 필터링 (roof, arrow 등) + const objects = allObjects.filter((obj) => { + // 주요 도형 객체만 포함 + return obj.name === 'roof' || + obj.name === 'arrow' || + obj.name === 'module' || + obj.name === 'dormer' || + obj.name === 'object' + }) + + console.log('🔍 All objects:', allObjects.length) + console.log('🔍 Main objects for centering:', objects.length) + console.log('🔍 Main object details:', objects.map(obj => ({ + name: obj.name, + type: obj.type, + visible: obj.visible, + id: obj.id + }))) + + if (objects.length === 0) { + console.log('❌ No main objects found for centering') + return + } let minX = Infinity, minY = Infinity @@ -439,11 +471,24 @@ export function useCanvasEvent() { maxY = Math.max(maxY, bounds.top + bounds.height) }) - const centerX = (minX + maxX) / 2 - const centerY = (minY + maxY) / 2 - const centerPoint = new fabric.Point(centerX, centerY) - - canvas.zoomToPoint(centerPoint, 1) + const objectsCenterX = (minX + maxX) / 2 + const objectsCenterY = (minY + maxY) / 2 + + // 화면 중앙 좌표 (메뉴 높이 고려하여 Y 위치 조정) + const screenCenterX = canvas.getWidth() / 2 + const menuHeightOffset = 80 // 위쪽 메뉴 높이만큼 위로 조정 + const screenCenterY = canvas.getHeight() / 2 - menuHeightOffset + + // 객체 중심을 화면 중앙으로 이동 + const deltaX = screenCenterX - objectsCenterX + const deltaY = screenCenterY - objectsCenterY + + // canvas viewport transform 조정 + const viewportTransform = canvas.viewportTransform + viewportTransform[4] += deltaX + viewportTransform[5] += deltaY + + canvas.setViewportTransform(viewportTransform) canvas.renderAll() } From bb775b2847a7727cbe85f0df8adf6a669c957e12 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 31 Mar 2026 16:27:26 +0900 Subject: [PATCH 18/21] =?UTF-8?q?errSave=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/common/useMasterController.js | 5 +++++ src/hooks/module/useTrestle.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/hooks/common/useMasterController.js b/src/hooks/common/useMasterController.js index 8eb302d1..36f5bd61 100644 --- a/src/hooks/common/useMasterController.js +++ b/src/hooks/common/useMasterController.js @@ -359,6 +359,10 @@ export function useMasterController() { }) } + const quotationErrSave = async (params) => { + return await post({ url: '/api/v1/master/quotationErrSave', data: params }) + } + return { getRoofMaterialList, getModuleTypeItemList, @@ -374,5 +378,6 @@ export function useMasterController() { updateObjectDate, getQuotationItem, getPcsConnOptionItemList, + quotationErrSave, } } diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 173a5cd2..19da5b4e 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -1,5 +1,5 @@ import { useRecoilValue } from 'recoil' -import { canvasState, currentAngleTypeSelector } from '@/store/canvasAtom' +import { canvasState, currentAngleTypeSelector, currentCanvasPlanState } from '@/store/canvasAtom' import { POLYGON_TYPE, TRESTLE_MATERIAL } from '@/common/common' import { moduleSelectionDataState } from '@/store/selectedModuleOptions' import { getDegreeByChon } from '@/util/canvas-util' @@ -11,6 +11,7 @@ import { useContext } from 'react' import { QcastContext } from '@/app/QcastProvider' import { useCircuitTrestle } from '@/hooks/useCirCuitTrestle' import { useMessage } from '@/hooks/useMessage' +import { loginUserStore } from '@/store/commonAtom' // 모듈간 같은 행, 열의 마진이 10 이하인 경우는 같은 행, 열로 간주 const MODULE_MARGIN = 10 @@ -18,7 +19,9 @@ const MODULE_MARGIN = 10 export const useTrestle = () => { const canvas = useRecoilValue(canvasState) const moduleSelectionData = useRecoilValue(moduleSelectionDataState) //다음으로 넘어가는 최종 데이터 - const { getQuotationItem } = useMasterController() + const { getQuotationItem, quotationErrSave } = useMasterController() + const currentCanvasPlan = useRecoilValue(currentCanvasPlanState) + const loginUserState = useRecoilValue(loginUserStore) const currentAngleType = useRecoilValue(currentAngleTypeSelector) const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet const isTrestleDisplay = useRecoilValue(trestleDisplaySelector) @@ -757,6 +760,12 @@ export const useTrestle = () => { text: result.resultMsg || getMessage('common.message.noData'), icon: 'error' }) + quotationErrSave({ + objectNo: currentCanvasPlan.objectNo, + planNo: currentCanvasPlan.planNo, + rejectRsn: result.resultMsg, + regId: loginUserState.userId, + }) clear() setViewCircuitNumberTexts(true) setIsGlobalLoading(false) From 984401881d17f65504daeb5a7abbcf53b79053e8 Mon Sep 17 00:00:00 2001 From: ysCha Date: Tue, 31 Mar 2026 16:34:54 +0900 Subject: [PATCH 19/21] =?UTF-8?q?=ED=99=95=EB=8C=80=EC=B6=95=EC=86=8C100%?= =?UTF-8?q?=EA=B0=80=EC=9A=B4=EB=8D=B0=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCanvasEvent.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/hooks/useCanvasEvent.js b/src/hooks/useCanvasEvent.js index 0c566020..0a1de7c4 100644 --- a/src/hooks/useCanvasEvent.js +++ b/src/hooks/useCanvasEvent.js @@ -489,6 +489,34 @@ export function useCanvasEvent() { viewportTransform[5] += deltaY canvas.setViewportTransform(viewportTransform) + + // 브라우저 스크롤을 중앙으로 조정하여 전체 canvas를 모니터 중앙에 위치 + const canvasElement = canvas.getElement() + const canvasRect = canvasElement.getBoundingClientRect() + + // 현재 스크롤 위치 + const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop + const currentScrollLeft = window.pageXOffset || document.documentElement.scrollLeft + + // canvas의 현재 화면 위치 + const canvasScreenTop = canvasRect.top + currentScrollTop + const canvasScreenLeft = canvasRect.left + currentScrollLeft + + // 모니터 중앙 + const monitorCenterX = window.innerWidth / 2 + const monitorCenterY = window.innerHeight / 2 + + // canvas가 모니터 중앙에 오도록 스크롤 위치 계산 + const targetScrollLeft = canvasScreenLeft + (canvasRect.width / 2) - monitorCenterX + const targetScrollTop = canvasScreenTop + (canvasRect.height / 2) - monitorCenterY + menuHeightOffset + + // 스크롤을 중앙으로 이동 + window.scrollTo({ + top: targetScrollTop, + left: targetScrollLeft, + behavior: 'smooth' // 부드러운 스크롤 + }) + canvas.renderAll() } From 44aca56d7565b6508b2235f01aec94bdd9e0139f Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 1 Apr 2026 11:28:28 +0900 Subject: [PATCH 20/21] =?UTF-8?q?=EB=AC=BC=EA=B1=B4=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=97=90=201=EC=B0=A8=EC=A0=90=20=EC=88=A8=EA=B9=80(2=EC=B0=A8?= =?UTF-8?q?=EC=A0=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/management/StuffSearchCondition.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/management/StuffSearchCondition.jsx b/src/components/management/StuffSearchCondition.jsx index 3774fbfe..85563672 100644 --- a/src/components/management/StuffSearchCondition.jsx +++ b/src/components/management/StuffSearchCondition.jsx @@ -1166,7 +1166,7 @@ export default function StuffSearchCondition() { {getMessage('stuff.search.schSelSaleStoreId')}
-
+
{session?.storeId === 'T01' && ( )}
-
+