From 2101d30d98546fe8729b4118f6d07fe8ebb3d656 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Wed, 1 Apr 2026 17:11:58 +0900 Subject: [PATCH 01/22] =?UTF-8?q?1830=20=EB=AA=A8=EB=93=88=20=EC=84=A4?= =?UTF-8?q?=EC=B9=98=EC=98=81=EC=97=AD=20=EB=8C=80=EA=B0=81=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=ED=98=84=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/useMode.js | 48 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 8492042a..d014b25f 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1756,6 +1756,11 @@ export function useMode() { offsetEdges.push(createOffsetEdge(edge, dx, dy)) }) + // 폴리곤의 감김 방향(signed area)을 구한다 + const signedArea = bevelJoin + ? polygon.edges.reduce((sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y), 0) + : 0 + const vertices = [] offsetEdges.forEach((thisEdge, i) => { @@ -1765,9 +1770,22 @@ export function useMode() { 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 }) + // 꼭짓점의 볼록/오목 판별 (cross product와 signed area 비교) + const origPrev = polygon.edges[(i + polygon.edges.length - 1) % polygon.edges.length] + const origThis = polygon.edges[i] + const cross = + (origPrev.vertex2.x - origPrev.vertex1.x) * (origThis.vertex2.y - origThis.vertex1.y) - + (origPrev.vertex2.y - origPrev.vertex1.y) * (origThis.vertex2.x - origThis.vertex1.x) + const isConvex = cross * signedArea > 0 + + if (isConvex) { + // 볼록 꼭짓점: bevel 적용 (offset edge 끝점 사용) + vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y }) + vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y }) + } else { + // 오목 꼭짓점: 교차점 사용 (90도 유지) + vertices.push({ x: vertex.x, y: vertex.y }) + } } } }) @@ -1795,6 +1813,11 @@ export function useMode() { offsetEdges.push(createOffsetEdge(edge, dx, dy)) }) + // 폴리곤의 감김 방향(signed area)을 구한다 + const signedArea = bevelJoin + ? polygon.edges.reduce((sum, edge) => sum + (edge.vertex1.x * edge.vertex2.y - edge.vertex2.x * edge.vertex1.y), 0) + : 0 + const vertices = [] offsetEdges.forEach((thisEdge, i) => { @@ -1804,9 +1827,22 @@ export function useMode() { 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 }) + // 꼭짓점의 볼록/오목 판별 (cross product와 signed area 비교) + const origPrev = polygon.edges[(i + polygon.edges.length - 1) % polygon.edges.length] + const origThis = polygon.edges[i] + const cross = + (origPrev.vertex2.x - origPrev.vertex1.x) * (origThis.vertex2.y - origThis.vertex1.y) - + (origPrev.vertex2.y - origPrev.vertex1.y) * (origThis.vertex2.x - origThis.vertex1.x) + const isConvex = cross * signedArea > 0 + + if (isConvex) { + // 볼록 꼭짓점: bevel 적용 (offset edge 끝점 사용) + vertices.push({ x: prevEdge.vertex2.x, y: prevEdge.vertex2.y }) + vertices.push({ x: thisEdge.vertex1.x, y: thisEdge.vertex1.y }) + } else { + // 오목 꼭짓점: 교차점 사용 (90도 유지) + vertices.push({ x: vertex.x, y: vertex.y }) + } } } }) From 13f10fd18eb5e9464c9e6b578367b22b978742e3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 1 Apr 2026 17:48:25 +0900 Subject: [PATCH 02/22] =?UTF-8?q?Revert=20"=EC=88=98=EB=8F=99=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=EB=AC=BC=EB=96=BC?= =?UTF-8?q?=EC=84=B8=20=EB=B0=B0=EC=B9=98=20=ED=95=9C=EB=8B=A4=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b1c35376da012c1b685f7b58a0b14b3ffddf5425. --- src/components/floor-plan/modal/basic/BasicSetting.jsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/floor-plan/modal/basic/BasicSetting.jsx b/src/components/floor-plan/modal/basic/BasicSetting.jsx index 04dbbcb0..983492bf 100644 --- a/src/components/floor-plan/modal/basic/BasicSetting.jsx +++ b/src/components/floor-plan/modal/basic/BasicSetting.jsx @@ -17,7 +17,6 @@ import { currentCanvasPlanState, isManualModuleLayoutSetupState, isManualModuleSetupState, - moduleSetupOptionState, toggleManualSetupModeState, } from '@/store/canvasAtom' import { loginUserStore } from '@/store/commonAtom' @@ -48,7 +47,6 @@ 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, @@ -213,12 +211,8 @@ export default function BasicSetting({ id, pos = { x: 50, y: 230 } }) { } const handleManualModuleSetup = () => { - const nextManual = !isManualModuleSetup - setManualSetupMode(`manualSetup_${nextManual}`) - setIsManualModuleSetup(nextManual) - if (nextManual) { - setModuleSetupOption((prev) => ({ ...prev, isChidori: true })) - } + setManualSetupMode(`manualSetup_${!isManualModuleSetup}`) + setIsManualModuleSetup(!isManualModuleSetup) } const handleManualModuleLayoutSetup = () => { From 0e4b0b410f084cc39db72977fa9e0a0cf07281a3 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 1 Apr 2026 18:10:58 +0900 Subject: [PATCH 03/22] =?UTF-8?q?=ED=98=B9=EC=9E=A5=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=20roofLine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 245 ++++++++++++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 33 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 90601c15..17374edd 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -991,7 +991,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPStart.y = wallBaseLine.y1 findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_start' }) - const newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber() + let newPointX = Big(roofLine.x1).plus(moveDist).abs().toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).toNumber() const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() // let idx = 0 > index - 1 ? sortRoofLines.length : index @@ -1005,8 +1005,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // 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') - getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + // Validate: newPointX should not cross over pLineX (adjacent line) + if (newPointX > pLineX) { + console.warn('⚠️ [LEFT_IN_START] Line crossing detected! newPointX:', newPointX, 'exceeds pLineX:', pLineX, '| Clamping to prevent overlap') + newPointX = pLineX + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } } getAddLine(newPStart, newPEnd, 'red') @@ -1022,7 +1028,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.y = wallBaseLine.y2 findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_in_end' }) - const newPointX = Big(roofLine.x1).plus(moveDist).toNumber() + let newPointX = Big(roofLine.x1).plus(moveDist).toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y2).minus(0).toNumber() // let idx = sortRoofLines.length < index + 1 ? 0 : index @@ -1036,14 +1042,84 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { // 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') - getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + // Validate: newPointX should not cross over pLineX (adjacent line) + if (newPointX > pLineX) { + console.warn('⚠️ [LEFT_IN_END] Line crossing detected! newPointX:', newPointX, 'exceeds pLineX:', pLineX, '| Clamping to prevent overlap') + newPointX = pLineX + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') getAddLine(newPStart, newPEnd, 'red') } + + if(!isStartEnd.start && !isStartEnd.end){ + console.log('left_in::::isStartEnd:::::', isStartEnd) + const moveDistY1 = Big(roofLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(roofLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(roofLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(roofLine.y2).minus(wallBaseLine.y2).abs().toNumber() + + const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } + const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + + + if(moveDistY1 > 0) { + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'left_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 }) + 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) + } + + } + if(moveDistY2 > 0) { + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'left_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 created1 - 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 + // 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 created2 - 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 if (condition === 'left_out') { console.log('left_out::::isStartEnd:::::', isStartEnd) + if (isStartEnd.start) { const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y1).minus(moveDist).abs().toNumber() @@ -1103,7 +1179,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { } if (isStartEnd.end) { - console.log('left_in::::isStartEnd:::::', isStartEnd) + console.log('left_out::::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() @@ -1163,7 +1239,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { findPoints.push({ y: newPStart.y, x: newPEnd.x, position: 'left_out_end' }) getAddLine(newPStart, newPEnd, 'red') } + } else if (condition === 'right_in') { + if (isStartEnd.start) { console.log('right_in::::isStartEnd:::::', isStartEnd) newPEnd.y = roofLine.y2 @@ -1174,7 +1252,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPStart.y = wallBaseLine.y1 findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_start' }) - const newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber() + let newPointX = Big(roofLine.x1).minus(moveDist).abs().toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y1).minus(0).abs().toNumber() // let idx = 0 >= index - 1 ? sortRoofLines.length : index @@ -1188,8 +1266,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //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') - getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + // Validate: newPointX should not cross over pLineX (adjacent line) + if (newPointX < pLineX) { + console.warn('⚠️ [RIGHT_IN_START] Line crossing detected! newPointX:', newPointX, 'is less than pLineX:', pLineX, '| Clamping to prevent overlap') + newPointX = pLineX + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } } getAddLine(newPStart, newPEnd, 'red') } @@ -1204,7 +1288,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { newPEnd.y = wallBaseLine.y2 findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_in_end' }) - const newPointX = Big(roofLine.x1).minus(moveDist).toNumber() + let newPointX = Big(roofLine.x1).minus(moveDist).toNumber() const pDist = Big(wallLine.x1).minus(roofLine.x1).abs().toNumber() const pLineY = Big(roofLine.y2).minus(0).abs().toNumber() // let idx = sortRoofLines.length < index + 1 ? 0 : index @@ -1218,12 +1302,82 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { //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') - getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + // Validate: newPointX should not cross over pLineX (adjacent line) + if (newPointX < pLineX) { + console.warn('⚠️ [RIGHT_IN_END] Line crossing detected! newPointX:', newPointX, 'is less than pLineX:', pLineX, '| Clamping to prevent overlap') + newPointX = pLineX + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: newPointX, y: pLineY }, 'green') + getAddLine({ x: newPointX, y: pLineY }, { x: ePoint.x, y: ePoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: newPointX, y: roofLine.y2 }, 'orange') getAddLine(newPStart, newPEnd, 'red') } + + if(!isStartEnd.start && !isStartEnd.end){ + console.log('right_in::::isStartEnd:::::', isStartEnd) + const moveDistY1 = Big(roofLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(roofLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(roofLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(roofLine.y2).minus(wallBaseLine.y2).abs().toNumber() + + const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } + const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } + + + if(moveDistY1 > 0) { + findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'right_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 }) + 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) + } + + } + if(moveDistY2 > 0) { + findPoints.push({ x: ePoint.x, y: ePoint.y, position: 'right_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 created1 - 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 + // 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 created2 - 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 if (condition === 'right_out') { console.log('right_out::::isStartEnd:::::', isStartEnd) if (isStartEnd.start) { @@ -1370,7 +1524,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } newPStart.x = wallBaseLine.x1 - const newPointY = Big(roofLine.y2).plus(moveDist).toNumber() + let newPointY = Big(roofLine.y2).plus(moveDist).toNumber() const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() const pLineX = Big(roofLine.x1).minus(0).toNumber() @@ -1386,8 +1540,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_start' }) if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { - getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') - getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + // Validate: newPointY should not cross over pLineY (adjacent line) + if (newPointY > pLineY) { + console.warn('⚠️ [TOP_IN_START] Line crossing detected! newPointY:', newPointY, 'exceeds pLineY:', pLineY, '| Clamping to prevent overlap') + newPointY = pLineY + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') getAddLine(newPStart, newPEnd, 'red') @@ -1399,7 +1559,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } newPEnd.x = wallBaseLine.x2 - const newPointY = Big(roofLine.y1).plus(moveDist).toNumber() + let newPointY = Big(roofLine.y1).plus(moveDist).toNumber() const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() const pLineX = Big(roofLine.x2).minus(0).toNumber() @@ -1415,20 +1575,27 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'top_in_end' }) if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { - getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') - getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + // Validate: newPointY should not cross over pLineY (adjacent line) + if (newPointY > pLineY) { + console.warn('⚠️ [TOP_IN_END] Line crossing detected! newPointY:', newPointY, 'exceeds pLineY:', pLineY, '| Clamping to prevent overlap') + newPointY = pLineY + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') getAddLine(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 moveDistY1 = Big(roofLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(roofLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(roofLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(roofLine.y2).minus(wallBaseLine.y2).abs().toNumber() const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } @@ -1609,7 +1776,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } newPStart.x = wallBaseLine.x1 - const newPointY = Big(roofLine.y2).minus(moveDist).toNumber() + let newPointY = Big(roofLine.y2).minus(moveDist).toNumber() const pDist = Big(wallLine.y2).minus(roofLine.y2).abs().toNumber() const pLineX = Big(roofLine.x1).minus(0).toNumber() @@ -1625,8 +1792,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_start' }) if (Math.abs(wallBaseLine.x1 - wallLine.x1) < 0.1) { - getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') - getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + // Validate: newPointY should not cross over pLineY (adjacent line) + if (newPointY < pLineY) { + console.warn('⚠️ [BOTTOM_IN_START] Line crossing detected! newPointY:', newPointY, 'is less than pLineY:', pLineY, '| Clamping to prevent overlap') + newPointY = pLineY + } else { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x2, y: roofLine.y2 }, { x: roofLine.x2, y: newPointY }, 'orange') getAddLine(newPStart, newPEnd, 'red') @@ -1638,7 +1811,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { sPoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } newPEnd.x = wallBaseLine.x2 - const newPointY = Big(roofLine.y1).minus(moveDist).toNumber() + let newPointY = Big(roofLine.y1).minus(moveDist).toNumber() const pDist = Big(wallLine.y1).minus(roofLine.y1).abs().toNumber() const pLineX = Big(roofLine.x2).minus(0).toNumber() @@ -1655,8 +1828,14 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { findPoints.push({ x: sPoint.x, y: sPoint.y, position: 'bottom_in_end' }) if (Math.abs(wallBaseLine.x2 - wallLine.x2) < 0.1) { - getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') - getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + // Validate: newPointY should not cross over pLineY (adjacent line) + if (newPointY < pLineY) { + console.warn('⚠️ [BOTTOM_IN_END] Line crossing detected! newPointY:', newPointY, 'is less than pLineY:', pLineY, '| Clamping to prevent overlap') + newPointY = pLineY + }else { + getAddLine({ x: pLineX, y: pLineY }, { x: pLineX, y: newPointY }, 'green') + getAddLine({ x: pLineX, y: newPointY }, { x: sPoint.x, y: sPoint.y }, 'pink') + } } //getAddLine({ x: roofLine.x1, y: roofLine.y1 }, { x: roofLine.x1, y: newPointY }, 'orange') getAddLine(newPStart, newPEnd, 'red') @@ -1666,10 +1845,10 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { 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 moveDistY1 = Big(roofLine.y1).minus(wallBaseLine.y1).abs().toNumber() + const moveDistX1 = Big(roofLine.x1).minus(wallBaseLine.x1).abs().toNumber() + const moveDistX2 = Big(roofLine.x2).minus(wallBaseLine.x2).abs().toNumber() + const moveDistY2 = Big(roofLine.y2).minus(wallBaseLine.y2).abs().toNumber() const sPoint = { x: wallBaseLine.x1, y: wallBaseLine.y1 } const ePoint = { x: wallBaseLine.x2, y: wallBaseLine.y2 } From dffb8c0dfbfc18cb4379a0eb4b7346620ecdba52 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Apr 2026 10:29:10 +0900 Subject: [PATCH 04/22] =?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 --- .../modal/placementShape/PlacementShapeSetting.jsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx index fbf387bb..9ba4e342 100644 --- a/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx +++ b/src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx @@ -266,15 +266,14 @@ 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] - if (firstRoof) { + roofs.forEach((roof) => { /** 모양 패턴 설정 */ - setSurfaceShapePattern(firstRoof, roofDisplay.column, false, { ...roofInfo }) - firstRoof.roofMaterial = { ...roofInfo } - drawDirectionArrow(firstRoof) - setPolygonLinesActualSize(firstRoof, true) - } + setSurfaceShapePattern(roof, roofDisplay.column, false, { ...roofInfo }) + roof.roofMaterial = { ...roofInfo } + drawDirectionArrow(roof) + setPolygonLinesActualSize(roof, true) + }) canvas.renderAll() /** 지붕면 존재 여부에 따라 메뉴 설정 */ From cd5eebd08f4bea43afc075b89dca30cff42dbe53 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Apr 2026 14:45:02 +0900 Subject: [PATCH 05/22] =?UTF-8?q?[1840]=EC=83=9D=EC=84=B1=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=80=20=ED=8C=94=EC=9E=91=EC=A7=80?= =?UTF-8?q?=EB=B6=95=20=EB=8F=84=EB=A9=B4=20=EC=82=AC=EB=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/skeletons/SkeletonBuilder.ts | 13 +++++----- src/util/skeleton-utils.js | 37 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/lib/skeletons/SkeletonBuilder.ts b/src/lib/skeletons/SkeletonBuilder.ts index 01ddb95b..6466782d 100644 --- a/src/lib/skeletons/SkeletonBuilder.ts +++ b/src/lib/skeletons/SkeletonBuilder.ts @@ -253,13 +253,12 @@ export default class SkeletonBuilder { } private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) { - const beginEdge2 = beginNextVertex.PreviousEdge; - const endEdge2 = endPreviousVertex.NextEdge; + // Use the vertex's actual edges — chain edges can become stale + // when previous iterations of MultiSplitEvent modify the LAV + const actualBeginEdge = beginNextVertex.PreviousEdge; + const actualEndEdge = endPreviousVertex.NextEdge; - if (beginEdge !== beginEdge2 || endEdge !== endEdge2) - throw new Error(); - - if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) { + if (actualBeginEdge.Norm.Dot(actualEndEdge.Norm) < -0.97) { const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized(); const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized(); const bisectorPrediction = this.CalcVectorBisector(n1, n2); @@ -887,7 +886,7 @@ export default class SkeletonBuilder { private static GetEdgeInLav(lav: CircularList, oppositeEdge: Edge): Vertex { for (const node of lav.Iterate()) if (oppositeEdge === node.PreviousEdge || - oppositeEdge === node.Previous.Next) + oppositeEdge === (node.Previous as Vertex).NextEdge) return node; return null; diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 17374edd..ae92a224 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -16,6 +16,39 @@ import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine */ const EPSILON = 0.1 +/** + * 오목(concave) 폴리곤인지 판별합니다. + * cross product의 부호가 혼재하면 오목 폴리곤입니다. + */ +const isConcavePolygon = (points) => { + if (!points || points.length < 4) return false + let positive = 0, negative = 0 + for (let i = 0; i < points.length; i++) { + const prev = points[(i - 1 + points.length) % points.length] + const curr = points[i] + const next = points[(i + 1) % points.length] + const cross = (curr.x - prev.x) * (next.y - curr.y) - (curr.y - prev.y) * (next.x - curr.x) + if (cross > 0) positive++ + else if (cross < 0) negative++ + } + return positive > 0 && negative > 0 +} + +/** + * 오목 폴리곤에만 미세한 perturbation을 적용하여 동시 이벤트(MultiSplitEvent)를 방지합니다. + * 대칭적 변 길이(예: 45.5, 45.5)가 동일 거리의 이벤트를 만들어 LAV 불일치 오류를 유발하므로 + * 각 꼭짓점에 인덱스 기반 미세 오프셋을 적용하면 이벤트가 개별 SplitEvent로 처리됩니다. + * 볼록 폴리곤은 이 문제가 없으므로 원본을 그대로 반환합니다. + */ +const perturbPolygonPoints = (points, eps = 1e-4) => { + if (!points || points.length < 3) return points + if (!isConcavePolygon(points)) return points + return points.map((p, i) => ({ + x: p.x + (i + 1) * eps, + y: p.y + (i + 1) * eps + })) +} + export const drawSkeletonRidgeRoof = (roofId, canvas, textMode) => { // 2. 스켈레톤 생성 및 그리기 @@ -503,11 +536,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지) roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y })) - const geoJSONPolygon = toGeoJSON(changRoofLinePoints) + const perturbedPoints = perturbPolygonPoints(changRoofLinePoints) + const geoJSONPolygon = toGeoJSON(perturbedPoints) try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() + console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 From 90228121991215d5d998e6880bb316cf640c7de9 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 2 Apr 2026 18:27:12 +0900 Subject: [PATCH 06/22] =?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/module/useTrestle.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index 19da5b4e..fad9a810 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -799,6 +799,7 @@ export const useTrestle = () => { const parent = canvas.getObjects().find((obj) => obj.id === surface.parentId) const { directionText, roofMaterial, moduleCompass, surfaceCompass } = parent const slope = Number(roofMaterial.pitch) + const angle = Number(roofMaterial.angle) const roofMaterialIndex = parent.roofMaterial.index const { nameJp: roofMaterialIdMulti } = roofMaterial const moduleSelection = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex) @@ -836,7 +837,7 @@ export const useTrestle = () => { supportMeaker, slope: +roofSizeSet === 3 ? 0 : slope, classType: currentAngleType === 'slope' ? '0' : '1', - angle: +roofSizeSet === 3 ? 0 : getDegreeByChon(slope), + angle: +roofSizeSet === 3 ? 0 : angle, azimuth: getAzimuth(parent), moduleList, } From 3eaf3f2c174f8b474c1fe38806e6afa39f694735 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 09:12:43 +0900 Subject: [PATCH 07/22] =?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/module/useTrestle.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/module/useTrestle.js b/src/hooks/module/useTrestle.js index fad9a810..411fc118 100644 --- a/src/hooks/module/useTrestle.js +++ b/src/hooks/module/useTrestle.js @@ -799,7 +799,7 @@ export const useTrestle = () => { const parent = canvas.getObjects().find((obj) => obj.id === surface.parentId) const { directionText, roofMaterial, moduleCompass, surfaceCompass } = parent const slope = Number(roofMaterial.pitch) - const angle = Number(roofMaterial.angle) + const angle = Number(roofMaterial.angle) || getDegreeByChon(slope) const roofMaterialIndex = parent.roofMaterial.index const { nameJp: roofMaterialIdMulti } = roofMaterial const moduleSelection = moduleSelectionData?.roofConstructions?.find((construction) => construction.roofIndex === roofMaterialIndex) @@ -1123,7 +1123,7 @@ export const useTrestle = () => { return } const roof = canvas.getObjects().find((obj) => obj.id === surface.parentId) - const degree = getDegreeByChon(roof.roofMaterial.pitch) + const degree = Number(roof.roofMaterial.angle) || getDegreeByChon(roof.roofMaterial.pitch) rackIntvlPct = rackIntvlPct === 0 ? 1 : rackIntvlPct // 0인 경우 1로 변경 rackIntvlPct = 100 / rackIntvlPct // 퍼센트로 변경 const moduleLeft = lastX ?? left From 138bdbdd35a91c536a2c86f75e3cda55be3a3558 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 11:14:47 +0900 Subject: [PATCH 08/22] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8,=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=B6=94=EC=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/Login.jsx | 1 + src/components/header/Header.jsx | 13 ++++--------- src/components/main/ChangePasswordPop.jsx | 15 ++++++--------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx index 7a6ba957..7ee11682 100644 --- a/src/components/auth/Login.jsx +++ b/src/components/auth/Login.jsx @@ -107,6 +107,7 @@ export default function Login() { if (res) { setIsLoading(false) if (res.data.result.resultCode === 'S') { + Cookies.set('sessionId', res.data.sessionId) setSession(res.data.data) setSessionState(res.data.data) // ID SAVE 체크되어 있는 경우, 쿠키 저장 diff --git a/src/components/header/Header.jsx b/src/components/header/Header.jsx index 561b8e47..9da21fb2 100644 --- a/src/components/header/Header.jsx +++ b/src/components/header/Header.jsx @@ -8,7 +8,8 @@ import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import { dimmedStore, sessionStore } from '@/store/commonAtom' import { useMessage } from '@/hooks/useMessage' -import { checkSession, logout } from '@/lib/authActions' +import { checkSession } from '@/lib/authActions' +import { useLogout } from '@/hooks/useLogout' import QSelectBox from '@/components/common/select/QSelectBox' @@ -77,6 +78,7 @@ export default function Header(props) { const { promiseGet } = useAxios() const { swalFire } = useSwal() + const { logoutProcess } = useLogout() /** * 지붕재 목록 Header에서 조회 @@ -372,14 +374,7 @@ export default function Header(props) {
diff --git a/src/components/main/ChangePasswordPop.jsx b/src/components/main/ChangePasswordPop.jsx index 28dc537c..064b6680 100644 --- a/src/components/main/ChangePasswordPop.jsx +++ b/src/components/main/ChangePasswordPop.jsx @@ -5,7 +5,8 @@ import { sessionStore } from '@/store/commonAtom' import { useRecoilState, useRecoilValue } from 'recoil' import { useAxios } from '@/hooks/useAxios' import { globalLocaleStore } from '@/store/localeAtom' -import { login, logout, setSession } from '@/lib/authActions' +import { login, setSession } from '@/lib/authActions' +import { useLogout } from '@/hooks/useLogout' import { useSwal } from '@/hooks/useSwal' import { QcastContext } from '@/app/QcastProvider' import { useRouter } from 'next/navigation' @@ -17,6 +18,7 @@ export default function ChangePasswordPop(props) { const { swalFire } = useSwal() const { patch } = useAxios(globalLocaleState) + const { logoutProcess } = useLogout() const { getMessage } = useMessage() const [sessionState, setSessionState] = useRecoilState(sessionStore) const router = useRouter() @@ -129,15 +131,13 @@ export default function ChangePasswordPop(props) { } } else { setIsGlobalLoading(false) - logout() - router.replace('/login', undefined, { shallow: true }) + logoutProcess() console.log('code not 200 error') } }) .catch((error) => { setIsGlobalLoading(false) - logout() - router.replace('/login', undefined, { shallow: true }) + logoutProcess() console.log('catch::::::::', error) }) } @@ -209,10 +209,7 @@ export default function ChangePasswordPop(props) { From ccf60ee7846a9e3c72091e27b259eb62e164279e Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 13:17:58 +0900 Subject: [PATCH 09/22] =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/AutoLogin.jsx | 2 ++ src/components/auth/Login.jsx | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/auth/AutoLogin.jsx b/src/components/auth/AutoLogin.jsx index b3dcbb6e..c8e42c49 100644 --- a/src/components/auth/AutoLogin.jsx +++ b/src/components/auth/AutoLogin.jsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { useMessage } from '@/hooks/useMessage' import { setSession, login } from '@/lib/authActions' +import Cookies from 'js-cookie' import { sessionStore } from '@/store/commonAtom' import { useRecoilState } from 'recoil' import { useAxios } from '@/hooks/useAxios' @@ -31,6 +32,7 @@ export default function AutoLoginPage({ autoLoginParam }) { setIsLoading(false) if (response.data) { const res = response.data + if (res.sessionId) Cookies.set('sessionId', res.sessionId) const result = { ...res, storeLvl: res.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' } setSession(result) setSessionState(result) diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx index 7ee11682..d20f5320 100644 --- a/src/components/auth/Login.jsx +++ b/src/components/auth/Login.jsx @@ -48,6 +48,7 @@ export default function Login() { post({ url: '/api/login/v1.0/user', data: { loginId: res.data } }).then((response) => { setIsLoading(false) if (response) { + if (response.sessionId) Cookies.set('sessionId', response.sessionId) const result = { ...response, storeLvl: response.groupId === '60000' ? '1' : '2', pwdInitYn: 'Y' } setSession(result) setSessionState(result) @@ -107,7 +108,7 @@ export default function Login() { if (res) { setIsLoading(false) if (res.data.result.resultCode === 'S') { - Cookies.set('sessionId', res.data.sessionId) + if (res.data.sessionId) Cookies.set('sessionId', res.data.sessionId) setSession(res.data.data) setSessionState(res.data.data) // ID SAVE 체크되어 있는 경우, 쿠키 저장 From c6da437a8def16bb5b3de56c5cfb5c93bbe94968 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 13:35:23 +0900 Subject: [PATCH 10/22] =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EC=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useLogout.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/hooks/useLogout.js diff --git a/src/hooks/useLogout.js b/src/hooks/useLogout.js new file mode 100644 index 00000000..cbaed9db --- /dev/null +++ b/src/hooks/useLogout.js @@ -0,0 +1,38 @@ +import { useRecoilState, useRecoilValue } from 'recoil' +import { sessionStore } from '@/store/commonAtom' +import { globalLocaleStore } from '@/store/localeAtom' +import { stuffSearchState } from '@/store/stuffAtom' +import { useAxios } from '@/hooks/useAxios' +import { logout } from '@/lib/authActions' +import Cookies from 'js-cookie' + +export function useLogout() { + const [sessionState] = useRecoilState(sessionStore) + const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState) + const globalLocaleState = useRecoilValue(globalLocaleStore) + const { promisePost } = useAxios(globalLocaleState) + + const logoutProcess = async () => { + const param = { + loginId: sessionState.userId, + requestId: Cookies.get('sessionId'), + } + await promisePost({ url: '/api/login/v1.0/logout', data: param }) + .then(async (res) => { + if (res.data.result.resultCode === 'S') { + setStuffSearch({ + ...stuffSearch, + code: 'DELETE', + }) + Cookies.remove('sessionId') + await logout() + window.location.href = '/login' + } + }) + .catch((error) => { + alert(error.response?.data?.message || 'ログアウトに失敗しました。') + }) + } + + return { logoutProcess } +} From b021b7beeae2006b3d626ca9eb833ed22c6bdf33 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 16:15:13 +0900 Subject: [PATCH 11/22] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EC=97=86=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/auth/AutoLogin.jsx | 2 +- src/components/header/Header.jsx | 2 +- src/components/main/ChangePasswordPop.jsx | 6 ++--- src/hooks/useLogout.js | 29 +++++++++++------------ 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/components/auth/AutoLogin.jsx b/src/components/auth/AutoLogin.jsx index c8e42c49..5de9ea85 100644 --- a/src/components/auth/AutoLogin.jsx +++ b/src/components/auth/AutoLogin.jsx @@ -3,12 +3,12 @@ import { useState } from 'react' import { useMessage } from '@/hooks/useMessage' import { setSession, login } from '@/lib/authActions' -import Cookies from 'js-cookie' import { sessionStore } from '@/store/commonAtom' import { useRecoilState } from 'recoil' import { useAxios } from '@/hooks/useAxios' import { globalLocaleStore } from '@/store/localeAtom' import { useRouter } from 'next/navigation' +import Cookies from 'js-cookie' import GlobalSpinner from '@/components/common/spinner/GlobalSpinner' diff --git a/src/components/header/Header.jsx b/src/components/header/Header.jsx index 9da21fb2..8c2ccbff 100644 --- a/src/components/header/Header.jsx +++ b/src/components/header/Header.jsx @@ -50,6 +50,7 @@ export default function Header(props) { const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState) const { closeAll } = usePopup() + const { logoutProcess } = useLogout() const { userSession } = props const [sessionState, setSessionState] = useRecoilState(sessionStore) @@ -78,7 +79,6 @@ export default function Header(props) { const { promiseGet } = useAxios() const { swalFire } = useSwal() - const { logoutProcess } = useLogout() /** * 지붕재 목록 Header에서 조회 diff --git a/src/components/main/ChangePasswordPop.jsx b/src/components/main/ChangePasswordPop.jsx index 064b6680..58c22d69 100644 --- a/src/components/main/ChangePasswordPop.jsx +++ b/src/components/main/ChangePasswordPop.jsx @@ -6,10 +6,9 @@ import { useRecoilState, useRecoilValue } from 'recoil' import { useAxios } from '@/hooks/useAxios' import { globalLocaleStore } from '@/store/localeAtom' import { login, setSession } from '@/lib/authActions' -import { useLogout } from '@/hooks/useLogout' import { useSwal } from '@/hooks/useSwal' import { QcastContext } from '@/app/QcastProvider' -import { useRouter } from 'next/navigation' +import { useLogout } from '@/hooks/useLogout' export default function ChangePasswordPop(props) { const globalLocaleState = useRecoilValue(globalLocaleStore) @@ -18,10 +17,9 @@ export default function ChangePasswordPop(props) { const { swalFire } = useSwal() const { patch } = useAxios(globalLocaleState) - const { logoutProcess } = useLogout() const { getMessage } = useMessage() const [sessionState, setSessionState] = useRecoilState(sessionStore) - const router = useRouter() + const { logoutProcess } = useLogout() const formInitValue = { password1: '', password2: '', diff --git a/src/hooks/useLogout.js b/src/hooks/useLogout.js index cbaed9db..9b2d638e 100644 --- a/src/hooks/useLogout.js +++ b/src/hooks/useLogout.js @@ -17,21 +17,20 @@ export function useLogout() { loginId: sessionState.userId, requestId: Cookies.get('sessionId'), } - await promisePost({ url: '/api/login/v1.0/logout', data: param }) - .then(async (res) => { - if (res.data.result.resultCode === 'S') { - setStuffSearch({ - ...stuffSearch, - code: 'DELETE', - }) - Cookies.remove('sessionId') - await logout() - window.location.href = '/login' - } - }) - .catch((error) => { - alert(error.response?.data?.message || 'ログアウトに失敗しました。') - }) + + try { + await promisePost({ url: '/api/login/v1.0/logout', data: param }) + } catch (error) { + console.log('logout api error:', error) + } + + setStuffSearch({ + ...stuffSearch, + code: 'DELETE', + }) + Cookies.remove('sessionId') + await logout() + window.location.href = '/login' } return { logoutProcess } From afe0a377db39cd71e3384adff3e72d7e036b8348 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 3 Apr 2026 16:20:12 +0900 Subject: [PATCH 12/22] =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=B6=94?= =?UTF-8?q?=EC=A0=812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useLogout.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/hooks/useLogout.js b/src/hooks/useLogout.js index 9b2d638e..08ef1cc3 100644 --- a/src/hooks/useLogout.js +++ b/src/hooks/useLogout.js @@ -6,6 +6,11 @@ import { useAxios } from '@/hooks/useAxios' import { logout } from '@/lib/authActions' import Cookies from 'js-cookie' +/** + * 로그아웃 처리 Hook + * - 로그아웃 API 호출 후 성공/실패 상관없이 로그아웃 처리 + * - Header, ChangePasswordPop 등에서 공통으로 사용 + */ export function useLogout() { const [sessionState] = useRecoilState(sessionStore) const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState) @@ -15,22 +20,24 @@ export function useLogout() { const logoutProcess = async () => { const param = { loginId: sessionState.userId, - requestId: Cookies.get('sessionId'), + requestId: Cookies.get('sessionId'), // 로그인 시 저장한 sessionId } + // 로그아웃 API 호출 (실패해도 로그아웃 진행) try { await promisePost({ url: '/api/login/v1.0/logout', data: param }) } catch (error) { console.log('logout api error:', error) } + // 물건검색 상태 초기화 setStuffSearch({ ...stuffSearch, code: 'DELETE', }) - Cookies.remove('sessionId') - await logout() - window.location.href = '/login' + Cookies.remove('sessionId') // sessionId 쿠키 삭제 + await logout() // iron-session 세션 파기 + window.location.href = '/login' // 로그인 페이지로 이동 (full reload) } return { logoutProcess } From 71da4d07feafaff4d90edf732d6a4fdecc2b9989 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 8 Apr 2026 10:08:42 +0900 Subject: [PATCH 13/22] =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8F=BC=EC=9D=98=20=EB=AC=BC=EA=B1=B4?= =?UTF-8?q?=EB=AA=85=20=EC=9E=85=EB=A0=A5=2050=EC=9E=90=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/management/StuffDetail.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index 12eef9ff..341e537d 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -1868,7 +1868,7 @@ export default function StuffDetail() { })} {/* 라디오끝 */}
- +
+