From 4f120e6e11f9f6648d56b35a5b23b4143bbd69c2 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 13 May 2026 10:35:16 +0900 Subject: [PATCH 01/19] =?UTF-8?q?[1956-RACK-TILT]=20=EB=B9=A8=EA=B0=95=20?= =?UTF-8?q?=EC=A0=90=EC=84=A0(=EA=B0=80=EB=8C=80=EC=84=A0/=E3=83=A2?= =?UTF-8?q?=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E9=85=8D=E7=BD=AE=E9=A0=98?= =?UTF-8?q?=E5=9F=9F)=20=EA=B0=80=EB=A1=9C/=EC=84=B8=EB=A1=9C=20=EC=B6=95?= =?UTF-8?q?=20=EC=8A=A4=EB=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closing edge 의 FP drift 누적(15mm/576mm ≈ 1.5°) 으로 가로/세로 라인이 기울어 보이는 문제. snapNearAxisEdges(angleTolDeg=2) 각도 기반 스냅을 4종 폴리곤(trestle/dormerTrestle/trestlePolygon/moduleSetupSurface) 에 적용. - QPolygon.initialize name 가드: load/new 대부분 케이스 커버 - handleOuterlinesTest 끝 snap: trestlePolygon 은 makePolygon 후 .set 이라 별도 #1956 (vertical tilt, 2026-04-08) 의 horizontal 형제 케이스. --- src/components/fabric/QPolygon.js | 16 +++++++++- src/hooks/useMode.js | 14 +++++++-- src/util/qpolygon-utils.js | 51 +++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 29868836..a0fcb57f 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -2,7 +2,7 @@ import { fabric } from 'fabric' import { v4 as uuidv4 } from 'uuid' import { QLine } from '@/components/fabric/QLine' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util' -import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, toGeoJSON } from '@/util/qpolygon-utils' +import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, snapNearAxisEdges, toGeoJSON } from '@/util/qpolygon-utils' import * as turf from '@turf/turf' import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import Big from 'big.js' @@ -120,6 +120,20 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.adjustRoofLines = [] // this.colorLines = [] + // [1956-RACK-TILT 2026-05-13] 빨강 점선 폴리곤군(가대선/모듈설치면) FP drift 보정. + // - trestle, dormerTrestle: createRoofRack 출력 + // - trestlePolygon: useMode.handleOuterlinesTest 출력 + // - moduleSetupSurface: useModuleBasicSetting (eavesMargin/ridgeMargin/kerabaMargin 적용) + // 가로/세로 edge 의 FP drift 가 저장된 JSON 에 박혀있는 경우 복원 시 라인이 기울어 보이는 문제. + if ( + options.name === 'trestle' || + options.name === 'dormerTrestle' || + options.name === 'trestlePolygon' || + options.name === 'moduleSetupSurface' + ) { + points = snapNearAxisEdges(points, 2) + } + // 소수점 전부 제거 points.forEach((point) => { point.x = Number(point.x.toFixed(this.toFixed)) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index c1b5524c..8a338f5b 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -33,7 +33,7 @@ import { import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' import { QPolygon } from '@/components/fabric/QPolygon' -import offsetPolygon, { calculateAngle } from '@/util/qpolygon-utils' +import offsetPolygon, { calculateAngle, snapNearAxisEdges } from '@/util/qpolygon-utils' import { isObjectNotEmpty } from '@/util/common-utils' import * as turf from '@turf/turf' import { INPUT_TYPE, LINE_TYPE, Mode, POLYGON_TYPE } from '@/common/common' @@ -1505,7 +1505,16 @@ export function useMode() { offsetPoints.push(offsetPoint) } - return makePolygon(offsetPoints, false) + // [1956-RACK-TILT 2026-05-13] trestlePolygon(モジュール配置領域 빨강점선) FP drift 보정. + // 입력 polygon.lines 의 미세 drift 가 평균법선 offset 으로 전파되어 가로/세로 라인이 + // 기울어 보이는 문제 차단. tolerance 0.5mm 보다 작은 dy/dx 만 축으로 스냅. + const _snapped = snapNearAxisEdges( + offsetPoints.map((p) => ({ x: p.x1, y: p.y1 })), + 2, + ) + const snappedOffsetPoints = _snapped.map((p) => ({ x1: p.x, y1: p.y })) + + return makePolygon(snappedOffsetPoints, false) } /** @@ -5100,6 +5109,7 @@ export function useMode() { roofs.forEach((roof, index) => { // const offsetPolygonPoint = offsetPolygon(roof.points(), -20) //이동되서 찍을라고 바꿈 + // [1956-RACK-TILT 2026-05-13] trestle name 으로 QPolygon.initialize 가 축-스냅 처리 const offsetPolygonPoint = offsetPolygon(roof.getCurrentPoints(), -20) const trestlePoly = new QPolygon(offsetPolygonPoint, { diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index dccd3414..5fc1a2e7 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -405,6 +405,57 @@ export function cleanSelfIntersectingPolygon(vertices) { return vertices } +/** + * [1956-RACK-TILT 2026-05-13] 거의 축에 평행한 edge 를 정확히 수평/수직으로 스냅. + * 빨강 점선(가대선/모듈설치면) edge 가 FP drift 누적으로 미세 기울어져 보이는 문제 보정. + * **각도 기반 판정**: |dy|/len < sin(angleTolDeg) 면 수평으로 간주. + * 절대값 tolerance 가 아닌 각도 임계를 쓰는 이유 — 긴 edge(예: 576mm) 에 작은 누적 + * drift(15mm) 가 실려 closing edge 가 흡수하면 절대값은 크지만 각도는 작음(1.5°). + * vertex 단위로 인접 edge 의 축 목표값을 적용해 코너 어긋남 방지. + * 대각(hip/ridge, 45° 등) 은 임계 2° 보다 훨씬 크니 무영향. + */ +export function snapNearAxisEdges(vertices, angleTolDeg = 2) { + if (!vertices || vertices.length < 3) return vertices + const n = vertices.length + const sinTol = Math.sin((angleTolDeg * Math.PI) / 180) + + const edgeY = new Array(n).fill(null) // 거의 수평 → 목표 y + const edgeX = new Array(n).fill(null) // 거의 수직 → 목표 x + + for (let i = 0; i < n; i++) { + const a = vertices[i] + const b = vertices[(i + 1) % n] + const dx = b.x - a.x + const dy = b.y - a.y + const len = Math.sqrt(dx * dx + dy * dy) + if (len < 1) continue // 0-length edge 안전장치 + const absSinDy = Math.abs(dy) / len // sin(angle from horizontal) + const absSinDx = Math.abs(dx) / len // sin(angle from vertical) + if (absSinDy < sinTol && absSinDx >= sinTol) { + edgeY[i] = (a.y + b.y) / 2 // 거의 수평 + } else if (absSinDx < sinTol && absSinDy >= sinTol) { + edgeX[i] = (a.x + b.x) / 2 // 거의 수직 + } + } + + return vertices.map((v, i) => { + const prev = (i - 1 + n) % n + let nx = v.x + let ny = v.y + const yPrev = edgeY[prev] + const yCur = edgeY[i] + if (yPrev !== null && yCur !== null) ny = (yPrev + yCur) / 2 + else if (yPrev !== null) ny = yPrev + else if (yCur !== null) ny = yCur + const xPrev = edgeX[prev] + const xCur = edgeX[i] + if (xPrev !== null && xCur !== null) nx = (xPrev + xCur) / 2 + else if (xPrev !== null) nx = xPrev + else if (xCur !== null) nx = xCur + return { x: nx, y: ny } + }) +} + export default function offsetPolygon(vertices, offset) { const polygon = createPolygon(vertices) const arcSegments = 0 From a332737654dad905747b730d2ce0789f7d0eb060 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 13 May 2026 12:14:42 +0900 Subject: [PATCH 02/19] =?UTF-8?q?[RACK-TILT]=20removeShortEdges=20?= =?UTF-8?q?=EC=A7=81=EA=B0=81=20=EB=85=B8=EC=B9=98=20=EB=B3=80=ED=98=95=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8=20=E2=80=94=20tiny=20edge=20=EB=A8=B8?= =?UTF-8?q?=EC=A7=80=20=EC=8B=9C=20axis-aligned=20=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useMode.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index c1b5524c..4bb91ba8 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -1730,10 +1730,25 @@ export function useMode() { // edge[i] 가 tiny 이면 시작 정점 vertex[i] 를 제거해 앞쪽 edge 와 병합한다. // 이렇게 하면 남는 cleaned edge 의 line 매핑(cleanedLines[k]=lines[kept[k]]) 이 // edge[i+1] (실제 긴 edge) 의 line 을 쓰게 되므로 offset 계산이 안정해진다. + // [tiny edge axis guard 2026-05-13] 단, 제거 후 새로 생기는 엣지가 axis-aligned 인 경우에만 안전. + // 직각 노치(예: H 91mm + V 24.5mm 코너) 의 24.5mm edge 를 단순 제거하면 v[i-1]→v[i+1] 가 + // 15° 대각선이 되어 inset+clipOffsetToOriginal 후 mm 단위 zigzag 토막으로 보임 (RACK-TILT). + // 원본 데이터가 wallBaseLine 의 axis-aligned 직각 노치를 보존해야 하므로, 대각선 결과가 되면 skip. const TINY_EDGE_THRESHOLD = 30 + const AXIS_TOL = 0.5 for (let i = 0; i < n; i++) { if (edges[i].length < TINY_EDGE_THRESHOLD) { - removeIndices.add(i) + const prev = (i + n - 1) % n + const nxt = (i + 1) % n + const vPrev = vertices[prev] + const vNext = vertices[nxt] + // 새 엣지 v[prev]→v[nxt] 가 수평 또는 수직인 경우만 머지 (drift artifact 흡수). + // 그 외(직각 노치 사이의 short step) 는 진짜 geometry 이므로 보존. + const isAxisAligned = + Math.abs(vPrev.y - vNext.y) < AXIS_TOL || Math.abs(vPrev.x - vNext.x) < AXIS_TOL + if (isAxisAligned) { + removeIndices.add(i) + } } } From 77bca24e7c09e22eccaf156c3e7ad0908b576b3c Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 13 May 2026 14:27:06 +0900 Subject: [PATCH 03/19] =?UTF-8?q?[SK-EXTENDED-PERSIST]=20hip=20=5F=5Fexten?= =?UTF-8?q?ded=20=ED=94=8C=EB=9E=98=EA=B7=B8=20save/load=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B4=20=E2=80=94=20attributes.extended=20=EB=8F=99?= =?UTF-8?q?=EA=B8=B0=20+=20console=E2=86=92logger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roofcover/useRoofAllocationSetting.js | 44 +- src/util/skeleton-utils.js | 392 +++++++++--------- 2 files changed, 222 insertions(+), 214 deletions(-) diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index e99c122b..88e7162e 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -36,6 +36,7 @@ import { calcLineActualSize2 } from '@/util/qpolygon-utils' import { notifyLowPitchRestrictionForRoofs, isLowPitchRestricted, LOW_PITCH_RESTRICTED_ROOF_IDS } from '@/util/roof-pitch-warning' // [LOW-PITCH-DIAG 2026-05-06 TEMP] 진단용 — 검증 후 import + 호출 함께 제거 import { debugCapture } from '@/util/debugCapture' +import { logger } from '@/util/logger' export function useRoofAllocationSetting(id) { const canvas = useRecoilValue(canvasState) @@ -217,7 +218,7 @@ export function useRoofAllocationSetting(id) { })) setCurrentRoofList(normalizedRoofs) } catch (error) { - console.error('Data fetching error:', error) + logger.error('Data fetching error:', error) } } @@ -521,7 +522,7 @@ export function useRoofAllocationSetting(id) { const extLines = roofBase.lines.filter((l) => l.lineName === 'extensionLine') if (extLines.length === 0) { - console.log(`[INTEGRATE] roofBase.id=${roofBase.id} extensionLine 없음 → skip`) + logger.log(`[INTEGRATE] roofBase.id=${roofBase.id} extensionLine 없음 → skip`) return } @@ -563,7 +564,7 @@ export function useRoofAllocationSetting(id) { }) if (!sk) { - console.log( + logger.log( `[INTEGRATE] ext 짝없음 ` + `(${extP1.x.toFixed(1)},${extP1.y.toFixed(1)})→(${extP2.x.toFixed(1)},${extP2.y.toFixed(1)})` ) @@ -571,8 +572,9 @@ export function useRoofAllocationSetting(id) { } // [2026-04-30] sk 가 이미 SK 빌드 단계에서 연장된 경우 → merge 스킵. - if (sk.__extended) { - console.log( + // [save/load 보존 2026-05-13] runtime __extended 는 저장 시 사라지므로 attributes.extended 도 함께 검사. + if (sk.__extended || sk.attributes?.extended) { + logger.log( `[INTEGRATE] sk 이미 연장됨(id=${sk.id}) → merge 스킵, ext 는 lines 에서만 제거(canvas 유지)` ) extLinesOnly.push(ext) @@ -617,7 +619,7 @@ export function useRoofAllocationSetting(id) { }) mergedLine.length = totalLen - console.log( + logger.log( `[INTEGRATE] merge ` + `ext=(${extP1.x.toFixed(1)},${extP1.y.toFixed(1)})→(${extP2.x.toFixed(1)},${extP2.y.toFixed(1)}) len=${extLen.toFixed(1)} ` + `sk[${sk.lineName || sk.name}]=(${skP1.x.toFixed(1)},${skP1.y.toFixed(1)})→(${skP2.x.toFixed(1)},${skP2.y.toFixed(1)}) len=${skLen.toFixed(1)} ` + @@ -630,7 +632,7 @@ export function useRoofAllocationSetting(id) { }) if (merged.length === 0 && extLinesOnly.length === 0) { - console.log(`[INTEGRATE] 통합 대상 없음 ext=${extLines.length}`) + logger.log(`[INTEGRATE] 통합 대상 없음 ext=${extLines.length}`) return } @@ -646,7 +648,7 @@ export function useRoofAllocationSetting(id) { removedExt.forEach((l) => canvas.remove(l)) removedSk.forEach((l) => canvas.remove(l)) - console.log( + logger.log( `[INTEGRATE] 완료 roofBase.id=${roofBase.id} ` + `ext제거=${removedExt.length} sk제거=${removedSk.length} merged=${merged.length} ` + `extKeptInCanvas=${extLinesOnly.length} ` + @@ -660,7 +662,7 @@ export function useRoofAllocationSetting(id) { const apply = () => { const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && !obj.roofMaterial) const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) - console.log(`[ALLOC] apply() 진입. roofBases=${roofBases.length}`) + logger.log(`[ALLOC] apply() 진입. roofBases=${roofBases.length}`) roofBases.forEach((roofBase) => { try { // 지붕 할당 로직에 extensionLine 추가 @@ -675,7 +677,7 @@ export function useRoofAllocationSetting(id) { snapByLineName[o.lineName] = (snapByLineName[o.lineName] || 0) + 1 snapByName[o.name || '?'] = (snapByName[o.name || '?'] || 0) + 1 }) - console.log( + logger.log( `[ALLOC-DEBUG] roofBase.id=${roofBase.id} ` + `roofBase.lines=${roofBase.lines?.length || 0} ` + `roofBase.innerLines=${roofBase.innerLines?.length || 0} ` + @@ -684,15 +686,15 @@ export function useRoofAllocationSetting(id) { `byName=${JSON.stringify(snapByName)}` ) roofEaveHelpLines.forEach((o, i) => { - console.log( + logger.log( ` [ALLOC-DEBUG] picked[${i}] name=${o.name} lineName=${o.lineName} ` + `(${o.x1?.toFixed(1)},${o.y1?.toFixed(1)})→(${o.x2?.toFixed(1)},${o.y2?.toFixed(1)}) ` + `len=${Math.hypot((o.x2 || 0) - (o.x1 || 0), (o.y2 || 0) - (o.y1 || 0)).toFixed(1)}` ) }) - // console.log('roofBase.id:', roofBase.id) - // console.log('roofEaveHelpLines:', roofEaveHelpLines) - // console.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine')) + // logger.log('roofBase.id:', roofBase.id) + // logger.log('roofEaveHelpLines:', roofEaveHelpLines) + // logger.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 @@ -703,8 +705,8 @@ export function useRoofAllocationSetting(id) { // 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) + // logger.log('extensionLines count:', extensionLines.length) + // logger.log('normalEaveLines count:', normalEaveLines.length) // 일반 eaveHelpLine만 Overlap 판단에 사용 const linesToKeep = roofBase.lines.filter(roofLine => { @@ -747,7 +749,7 @@ export function useRoofAllocationSetting(id) { isPointInside(eX2, eY2, rX1, rY1, rX2, rY2); if (isOverlapping) { - console.log('Removing overlapping line:', roofLine); + logger.log('Removing overlapping line:', roofLine); return true; // 포개지는 경우에만 삭제 } } @@ -759,7 +761,7 @@ export function useRoofAllocationSetting(id) { // Combine remaining lines with newEaveLines roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines]; // [ALLOC-DEBUG] 병합 결과 breakdown - console.log( + logger.log( `[ALLOC-DEBUG] 병합완료 roofBase.id=${roofBase.id} ` + `linesToKeep=${linesToKeep.length} ` + `normalEave=${normalEaveLines.length} ` + @@ -767,7 +769,7 @@ export function useRoofAllocationSetting(id) { `total=${roofBase.lines.length}` ) roofBase.lines.forEach((ln, i) => { - console.log( + logger.log( ` [ALLOC-DEBUG] merged[${i}] name=${ln.name || '?'} lineName=${ln.lineName || '?'} ` + `(${ln.x1?.toFixed(1)},${ln.y1?.toFixed(1)})→(${ln.x2?.toFixed(1)},${ln.y2?.toFixed(1)})` ) @@ -812,7 +814,7 @@ export function useRoofAllocationSetting(id) { integrateExtensionLines(roofBase) // [ALLOC-DEBUG] split 직전 최종 입력 - console.log( + logger.log( `[ALLOC-DEBUG] split 직전 roofBase.id=${roofBase.id} ` + `separatePolygon=${roofBase.separatePolygon?.length || 0} ` + `lines=${roofBase.lines?.length || 0} ` + @@ -826,7 +828,7 @@ export function useRoofAllocationSetting(id) { splitPolygonWithLines(roofBase) } } catch (e) { - console.log(e) + logger.log(e) canvas.discardActiveObject() return } diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 5a3e89d1..6ba11e2c 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -6,6 +6,7 @@ import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' import { QPolygon } from '@/components/fabric/QPolygon' import wallLine from '@/components/floor-plan/modal/wallLineOffset/type/WallLine' +import { logger } from '@/util/logger' /** * 지붕 폴리곤의 스켈레톤(중심선)을 생성하고 캔버스에 그립니다. @@ -95,7 +96,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { const _lp = canvas?.skeleton?.lastPoints const _src = _lp ? 'lastPoints' : 'orgRoofPoints' const _p7 = oldPoints?.[7] - console.log(`[LP-TRACE] movingLineFromSkeleton.in src=${_src} oldPoints[7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) pos=${movePosition} dir=${moveDirection}`) + logger.log(`[LP-TRACE] movingLineFromSkeleton.in src=${_src} oldPoints[7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) pos=${movePosition} dir=${moveDirection}`) } catch (_e) {} const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); @@ -107,18 +108,18 @@ const movingLineFromSkeleton = (roofId, canvas) => { const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId) if (oppositeLine) { - console.log('Opposite line found:', oppositeLine); + logger.log('Opposite line found:', oppositeLine); } else { - console.log('No opposite line found'); + logger.log('No opposite line found'); } if(moveFlowLine !== 0) { return oldPoints.map((point, index) => { - console.log('Point:', point); + logger.log('Point:', point); const newPoint = { ...point }; const absMove = Big(moveFlowLine).times(2).div(10); - console.log('skeletonBuilder moveDirection:', moveDirection); + logger.log('skeletonBuilder moveDirection:', moveDirection); switch (moveDirection) { case 'left': @@ -179,7 +180,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { if (line.position === 'bottom') { - console.log('oldPoint:', point); + logger.log('oldPoint:', point); if (isSamePoint(newPoint, line.start)) { newPoint.y = Big(line.start.y).minus(absMove).toNumber(); @@ -198,7 +199,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { // 사용 예시 } - console.log('newPoint:', newPoint); + logger.log('newPoint:', newPoint); //baseline 변경 return newPoint; }) @@ -207,14 +208,14 @@ const movingLineFromSkeleton = (roofId, canvas) => { // const selectLine = getSelectLine(); // - // console.log("wall::::", wall.points) - // console.log("저장된 3333moveSelectLine:", roof.moveSelectLine); - // console.log("저장된 3moveSelectLine:", selectLine); + // logger.log("wall::::", wall.points) + // logger.log("저장된 3333moveSelectLine:", roof.moveSelectLine); + // logger.log("저장된 3moveSelectLine:", selectLine); // const result = getSelectLinePosition(wall, selectLine, { // testDistance: 5, // 테스트 거리 // debug: true // 디버깅 로그 출력 // }); - // console.log("3333linePosition:::::", result.position); + // logger.log("3333linePosition:::::", result.position); const position = movePosition //result.position; const moveUpDownLength = Big(moveUpDown).times(1).div(10); @@ -255,10 +256,10 @@ const movingLineFromSkeleton = (roofId, canvas) => { affected.add(k); affected.add((k + 1) % nPts); }); - console.log('absMove::', moveUpDownLength); + logger.log('absMove::', moveUpDownLength); // [BR-TRACE local] movingLineFromSkeleton position/direction 분기 const __isLocalMS = process.env.NEXT_PUBLIC_RUN_MODE === 'local' - if (__isLocalMS) console.log(`[BR-TRACE] movingLineFromSkeleton position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) + if (__isLocalMS) logger.log(`[BR-TRACE] movingLineFromSkeleton position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) affected.forEach((i) => { const point = newPoints[i]; if (!point) return; @@ -266,21 +267,21 @@ const movingLineFromSkeleton = (roofId, canvas) => { if (position === 'bottom') { if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); - if (__isLocalMS) console.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) + if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'top') { if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); - if (__isLocalMS) console.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) + if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'left') { if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); - if (__isLocalMS) console.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) + if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) } else if (position === 'right') { if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); - if (__isLocalMS) console.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) + if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalMS) { - console.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }); @@ -343,7 +344,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { const cleaned = removeNonOrthogonalPoints(newPoints); - // console.log(cleaned) + // logger.log(cleaned) return cleaned; } @@ -408,7 +409,7 @@ const buildRawMovedPoints = (roofId, canvas) => { // [LP-TRACE] buildRawMovedPoints 진입 — prevLast 유무 및 [7] 현재 값 try { const _pl7 = prevLast?.[7] - console.log(`[LP-TRACE] buildRaw.in prevLast=${prevLast ? 'Y' : 'N'} prevLast[7]=(${_pl7?.x?.toFixed(1)},${_pl7?.y?.toFixed(1)}) oldPoints[7]=(${oldPoints[7]?.x?.toFixed(1)},${oldPoints[7]?.y?.toFixed(1)})`) + logger.log(`[LP-TRACE] buildRaw.in prevLast=${prevLast ? 'Y' : 'N'} prevLast[7]=(${_pl7?.x?.toFixed(1)},${_pl7?.y?.toFixed(1)}) oldPoints[7]=(${oldPoints[7]?.x?.toFixed(1)},${oldPoints[7]?.y?.toFixed(1)})`) } catch (_e) {} const selectLine = roof.moveSelectLine @@ -446,7 +447,7 @@ const buildRawMovedPoints = (roofId, canvas) => { }) // [BR-TRACE local] buildRawMovedPoints position/direction 분기 const __isLocalBR = process.env.NEXT_PUBLIC_RUN_MODE === 'local' - if (__isLocalBR) console.log(`[BR-TRACE] buildRawMovedPoints position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) + if (__isLocalBR) logger.log(`[BR-TRACE] buildRawMovedPoints position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) affected.forEach((i) => { const point = newPoints[i] if (!point) return @@ -454,21 +455,21 @@ const buildRawMovedPoints = (roofId, canvas) => { if (position === 'bottom') { if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber() - if (__isLocalBR) console.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) + if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'top') { if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber() - if (__isLocalBR) console.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) + if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'left') { if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber() - if (__isLocalBR) console.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) + if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) } else if (position === 'right') { if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber() - if (__isLocalBR) console.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) + if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalBR) { - console.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }) @@ -477,12 +478,12 @@ const buildRawMovedPoints = (roofId, canvas) => { const _sel = selectLine const _sp = _sel?.startPoint const _ep = _sel?.endPoint - console.log(`[LP-TRACE] buildRaw.out matchingLines=${matchingLines.length} selSP=(${_sp?.x?.toFixed(1)},${_sp?.y?.toFixed(1)}) selEP=(${_ep?.x?.toFixed(1)},${_ep?.y?.toFixed(1)}) bp7=(${basePoints[7]?.x?.toFixed(1)},${basePoints[7]?.y?.toFixed(1)}) newPoints[7]=(${newPoints[7]?.x?.toFixed(1)},${newPoints[7]?.y?.toFixed(1)})`) + logger.log(`[LP-TRACE] buildRaw.out matchingLines=${matchingLines.length} selSP=(${_sp?.x?.toFixed(1)},${_sp?.y?.toFixed(1)}) selEP=(${_ep?.x?.toFixed(1)},${_ep?.y?.toFixed(1)}) bp7=(${basePoints[7]?.x?.toFixed(1)},${basePoints[7]?.y?.toFixed(1)}) newPoints[7]=(${newPoints[7]?.x?.toFixed(1)},${newPoints[7]?.y?.toFixed(1)})`) } catch (_e) {} return newPoints } catch (e) { - console.warn('[buildRawMovedPoints] 실패 → null fallback:', e) + logger.warn('[buildRawMovedPoints] 실패 → null fallback:', e) return null } } @@ -511,7 +512,7 @@ export const verifyMoveBoundary = (roofId, canvas) => { try { const roof = canvas?.getObjects().find((o) => o.id === roofId) if (!roof || !Array.isArray(roof.points)) { - console.log('[verifyMoveBoundary] roof/points 없음 → unknown') + logger.log('[verifyMoveBoundary] roof/points 없음 → unknown') return 'unknown' } @@ -519,18 +520,18 @@ export const verifyMoveBoundary = (roofId, canvas) => { const moveUpDown = roof.moveUpDown ?? 0 const position = roof.movePosition const direction = roof.moveDirect - console.log( + logger.log( `[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}` ) if (moveFlowLine === 0 && moveUpDown === 0) { - console.log('[verifyMoveBoundary] 이동 없음 → ok') + logger.log('[verifyMoveBoundary] 이동 없음 → ok') return 'ok' } const proposed = buildRawMovedPoints(roofId, canvas) const orig = roof.points if (!Array.isArray(proposed) || proposed.length !== orig.length) { - console.log( + logger.log( `[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown` ) return 'unknown' @@ -555,7 +556,7 @@ export const verifyMoveBoundary = (roofId, canvas) => { const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next]) const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next]) - console.log( + logger.log( `[verifyMoveBoundary] [${i}] orig=(${Math.round(orig[i].x)},${Math.round(orig[i].y)}) → proposed=(${Math.round(proposed[i].x)},${Math.round(proposed[i].y)}) crossOrig=${crossOrig.toFixed(1)} crossNew=${crossNew.toFixed(1)}` ) @@ -565,13 +566,13 @@ export const verifyMoveBoundary = (roofId, canvas) => { const boundaryTol = Math.max(1.0, Math.abs(crossOrig) * 0.05) if (crossNew < -boundaryTol) { - console.warn( + logger.warn( `[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}` ) return 'crossed' } if (Math.abs(crossNew) <= boundaryTol) { - console.log( + logger.log( `[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})` ) if (worst === 'ok') worst = 'on-boundary' @@ -580,12 +581,12 @@ export const verifyMoveBoundary = (roofId, canvas) => { } if (movedIdx.length === 0) { - console.log('[verifyMoveBoundary] 이동된 꼭짓점 0개 (buildRawMovedPoints 매칭 실패 가능성)') + logger.log('[verifyMoveBoundary] 이동된 꼭짓점 0개 (buildRawMovedPoints 매칭 실패 가능성)') } - console.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) + logger.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) return worst } catch (e) { - console.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) + logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) return 'unknown' } } @@ -600,7 +601,7 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { const movedPoints = movingLineFromSkeleton(roofId, canvas) if (!Array.isArray(movedPoints) || movedPoints.length === 0) { - console.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') + logger.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') return null } @@ -650,7 +651,7 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { } if (zeroLenEdges > 0 || severeReduction || selfIntersect) { - console.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { + logger.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { movedLen: movedPoints.length, oldLen: oldPoints.length, zeroLenEdges, @@ -734,7 +735,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi if (!il || !isFinite(il.x1) || !isFinite(il.y1) || !isFinite(il.x2) || !isFinite(il.y2)) return false return il.lineName === 'hip' || il.name === 'hip' }) - console.log(`[v1 ext] HIP 추출 ${hipLines.length}개 / 전체 innerLines ${roof.innerLines.length}`) + logger.log(`[v1 ext] HIP 추출 ${hipLines.length}개 / 전체 innerLines ${roof.innerLines.length}`) // 절삭 대상: roofLine seg + canvas 의 다른 보조라인 (eaveHelpLine 등). // 기존 helper 자체(BASELINE_TO_ROOFLINE_HELPER_TYPE) / wall/roof 본체 / 텍스트 등은 제외. @@ -764,7 +765,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi const s = segs[k] auxList.push(`${s.__name || '?'}=(${Math.round(s.A.x)},${Math.round(s.A.y)})→(${Math.round(s.B.x)},${Math.round(s.B.y)}) len=${Math.round(Math.hypot(s.B.x - s.A.x, s.B.y - s.A.y))}`) } - console.log(`[BR-SEGS] roofLine=${m} aux=${segs.length - m}`, auxList) + logger.log(`[BR-SEGS] roofLine=${m} aux=${segs.length - m}`, auxList) } // [v1 ext 정정] ext 는 wallbaseLine 의 꼭지점(corner) 에 outer 끝점이 일치하는 hip 만 대상. @@ -783,7 +784,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // outer 끝이 wallbaseLine corner 에 일치하지 않으면 ext 대상 아님. if (outerCornerDist > CORNER_EPS) { - console.log( + logger.log( `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + `corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip` ) @@ -792,7 +793,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // 이미 처마(roofLine) 위에 있으면 ext 불필요. if (isPointOnRoofLine(outerEnd, 1.0)) { - console.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 이미 roofLine 위 → skip`) + logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 이미 roofLine 위 → skip`) continue } @@ -814,7 +815,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // [BR-RAY] local only — finite t 인 모든 후보 hit 찍기 (어떤 seg 가 가장 빠른지 비교용) if (__isLocalRayLog && isFinite(t)) { const __label = (k < m) ? `roofLine#${k}` : (segs[k].__name || `aux#${k - m}`) - console.log(`[BR-RAY] hip outer=(${Math.round(outerEnd.x)},${Math.round(outerEnd.y)}) ${__label} t=${t.toFixed(2)} seg=(${Math.round(A.x)},${Math.round(A.y)})→(${Math.round(B.x)},${Math.round(B.y)})`) + logger.log(`[BR-RAY] hip outer=(${Math.round(outerEnd.x)},${Math.round(outerEnd.y)}) ${__label} t=${t.toFixed(2)} seg=(${Math.round(A.x)},${Math.round(A.y)})→(${Math.round(B.x)},${Math.round(B.y)})`) } if (t < bestT) { bestT = t @@ -823,7 +824,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi } } if (!isFinite(bestT) || bestT < 0.5) { - console.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) NO_HIT or 너무 가까움`) + logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) NO_HIT or 너무 가까움`) continue } @@ -832,14 +833,14 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // eaveHelpLine 이 ray 경로를 가로막으면 이미 eave/aux 영역에 도달한 것이므로 더 그릴 필요 없음. // roofLine 승자만 진짜 "처마 도달" 로 인정 → ext 그림. if (bestSegName === 'eaveHelpLine') { - console.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 승자=eaveHelpLine bestT=${bestT.toFixed(2)} → skip`) + logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 승자=eaveHelpLine bestT=${bestT.toFixed(2)} → skip`) continue } const hitX = outerEnd.x + ux * bestT const hitY = outerEnd.y + uy * bestT - console.log( + logger.log( `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + `dir=(${ux.toFixed(3)},${uy.toFixed(3)}) bestT=${bestT.toFixed(2)} src=${bestSrc} ` + `→ ext=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)})→(${hitX.toFixed(1)},${hitY.toFixed(1)})` @@ -871,14 +872,19 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi hip.attributes.actualSize = Math.round(oldActual * ratio * 100) / 100 // integrateExtensionLines 가 이 hip 의 좌표를 되돌리지 않게 가드. + // [save/load 보존 2026-05-13] runtime 플래그(__extended) 외에 attributes.extended 도 동기화. + // attributes 는 SAVE_KEY 에 포함 → JSON 저장/로드 후에도 살아남음. + // 저장 후 재로드 시 sk.__extended 가 사라져 INTEGRATE merge 가 다시 실행되어 + // 확장된 hip 끝점이 옛 좌표로 되돌아가는 회귀 차단. hip.__extended = true + hip.attributes.extended = true // Fabric bounding/length/text 갱신. if (typeof hip.setCoords === 'function') hip.setCoords() if (typeof hip.setLength === 'function') hip.setLength() if (typeof hip.addLengthText === 'function') hip.addLengthText() - console.log( + logger.log( `[v1 ext] hip 연장 oldLen=${oldLen.toFixed(1)} +extLen=${extLen.toFixed(1)} ratio=${ratio.toFixed(3)} ` + `(plane ${oldPlane}→${hip.attributes.planeSize}, actual ${oldActual}→${hip.attributes.actualSize})` ) @@ -947,16 +953,16 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // // 디버그: baseLines의 offset/type/좌표 확인 // baseLines.forEach((bl, i) => { - // console.log(`[skeleton] baseLine[${i}] type=${bl.attributes?.type} offset=${bl.attributes?.offset} (${Math.round(bl.x1)},${Math.round(bl.y1)})→(${Math.round(bl.x2)},${Math.round(bl.y2)})`) + // logger.log(`[skeleton] baseLine[${i}] type=${bl.attributes?.type} offset=${bl.attributes?.offset} (${Math.round(bl.x1)},${Math.round(bl.y1)})→(${Math.round(bl.x2)},${Math.round(bl.y2)})`) // }) // roof.points.forEach((p, i) => { - // console.log(`[skeleton] roof.points[${i}] (${Math.round(p.x)},${Math.round(p.y)})`) + // logger.log(`[skeleton] roof.points[${i}] (${Math.round(p.x)},${Math.round(p.y)})`) // }) // roof.points 순서와 맞춰야 offset 매핑이 정확함 const baseLinePoints = createOrderedBasePoints(roof.points, baseLines) // baseLinePoints.forEach((p, i) => { - // console.log(`[skeleton] orderedBase[${i}] (${Math.round(p.x)},${Math.round(p.y)})`) + // logger.log(`[skeleton] orderedBase[${i}] (${Math.round(p.x)},${Math.round(p.y)})`) // }) @@ -969,7 +975,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 1. 지붕 폴리곤 유효성 검사 const coordinates = preprocessPolygonCoordinates(roof.points) if (coordinates.length < 3) { - console.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') + logger.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') return } @@ -977,10 +983,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const moveUpDown = roof.moveUpDown || 0 // 디버그: offset 변경 + moveLine 상태 확인 - console.log('[DEBUG] moveFlowLine:', moveFlowLine, 'moveUpDown:', moveUpDown) - console.log('[DEBUG] wall.lines:', wall.lines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) - console.log('[DEBUG] wall.baseLines:', wall.baseLines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) - console.log('[DEBUG] roof.points:', roof.points.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[DEBUG] moveFlowLine:', moveFlowLine, 'moveUpDown:', moveUpDown) + logger.log('[DEBUG] wall.lines:', wall.lines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) + logger.log('[DEBUG] wall.baseLines:', wall.baseLines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) + logger.log('[DEBUG] roof.points:', roof.points.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) // 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거 const isClosedPolygon = (points) => @@ -1039,10 +1045,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // - 45도 대각 방향(signDx, signDy 각 ±1)이므로 실제 x·y 이동량은 각 축으로 maxStep const maxContactDistance = Math.hypot(maxStep, maxStep) - // console.log("orderedBaseLinePoints",orderedBaseLinePoints) - // console.log("contactData",contactData) - // console.log("roofLineContactPoints",roofLineContactPoints) - // console.log("maxContactDistance",maxContactDistance) + // logger.log("orderedBaseLinePoints",orderedBaseLinePoints) + // logger.log("contactData",contactData) + // logger.log("roofLineContactPoints",roofLineContactPoints) + // logger.log("maxContactDistance",maxContactDistance) let changRoofLinePoints = orderedBaseLinePoints.map((point, index) => { const contactPoint = roofLineContactPoints[index] @@ -1059,7 +1065,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { } }) - // console.log("changRoofLinePoints1:::", changRoofLinePoints) + // logger.log("changRoofLinePoints1:::", changRoofLinePoints) // 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체 if (moveFlowLine !== 0 || moveUpDown !== 0) { @@ -1070,14 +1076,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { try { const __moveVerdict = verifyMoveBoundary(roofId, canvas) if (__moveVerdict === 'crossed') { - console.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') + logger.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') } } catch (e) { - console.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) + logger.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) } const movedPoints = safeMovedPointsWithFallback(roofId, canvas) - // console.log("movedPoints:::", movedPoints); + // logger.log("movedPoints:::", movedPoints); // movedPoints를 roof 경계로 확장 // 이번 이동에서 실제로 변경된 포인트만 구분하여, 변경되지 않은 축만 roof 경계로 확장 @@ -1122,7 +1128,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { if (SK_INPUT_USE_WALL_BASELINE_DIRECT) { changRoofLinePoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y })) roofLineContactPoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y })) - console.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):', + logger.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) // [v1 평행오버 corner 흡수 2026-04-29] @@ -1156,23 +1162,23 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { kept = next } if (absorbedAll.length > 0 && kept.length >= 3) { - console.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`) - console.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`, + logger.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`) + logger.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`, kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) changRoofLinePoints = kept roofLineContactPoints = kept.map((p) => ({ x: p.x, y: p.y })) } } - console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) // 좌표 유효성 검증 const invalidPoints = changRoofLinePoints.filter((p, i) => p == null || isNaN(p.x) || isNaN(p.y) || !isFinite(p.x) || !isFinite(p.y) ) if (invalidPoints.length > 0) { - console.error('[SK_ERROR] 유효하지 않은 좌표 발견:', invalidPoints) - console.error('[SK_ERROR] 전체 좌표:', JSON.stringify(changRoofLinePoints)) + logger.error('[SK_ERROR] 유효하지 않은 좌표 발견:', invalidPoints) + logger.error('[SK_ERROR] 전체 좌표:', JSON.stringify(changRoofLinePoints)) } // 인접 중복점 검출 (거리 < 0.5) @@ -1181,7 +1187,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const dist = Math.hypot(next.x - curr.x, next.y - curr.y) if (dist < 0.5) { - console.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) + logger.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) } } @@ -1192,7 +1198,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x) if (Math.abs(cross) < 1.0) { - console.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) + logger.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) } } @@ -1200,9 +1206,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { let isOverDetected = false const origPoints = roof.points || [] const n = changRoofLinePoints.length - console.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) - console.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) - console.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => { + logger.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => { const o = origPoints[i] if (!o) return null const dist = Math.hypot(p.x - o.x, p.y - o.y) @@ -1222,9 +1228,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { if (crossOrig * crossNew < 0) { isOverDetected = true - console.error(`[SK_OVER] 꼭짓점[${i}] 방향 뒤집힘! cross: ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}`) - console.error(`[SK_OVER] 원본: [${(i-1+n)%n}](${Math.round(prevOrig.x)},${Math.round(prevOrig.y)}) → [${i}](${Math.round(currOrig.x)},${Math.round(currOrig.y)}) → [${(i+1)%n}](${Math.round(nextOrig.x)},${Math.round(nextOrig.y)})`) - console.error(`[SK_OVER] 변경: [${(i-1+n)%n}](${Math.round(prevNew.x)},${Math.round(prevNew.y)}) → [${i}](${Math.round(currNew.x)},${Math.round(currNew.y)}) → [${(i+1)%n}](${Math.round(nextNew.x)},${Math.round(nextNew.y)})`) + logger.error(`[SK_OVER] 꼭짓점[${i}] 방향 뒤집힘! cross: ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}`) + logger.error(`[SK_OVER] 원본: [${(i-1+n)%n}](${Math.round(prevOrig.x)},${Math.round(prevOrig.y)}) → [${i}](${Math.round(currOrig.x)},${Math.round(currOrig.y)}) → [${(i+1)%n}](${Math.round(nextOrig.x)},${Math.round(nextOrig.y)})`) + logger.error(`[SK_OVER] 변경: [${(i-1+n)%n}](${Math.round(prevNew.x)},${Math.round(prevNew.y)}) → [${i}](${Math.round(currNew.x)},${Math.round(currNew.y)}) → [${(i+1)%n}](${Math.round(nextNew.x)},${Math.round(nextNew.y)})`) } } } @@ -1251,10 +1257,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { ) if (corrected && corrected.length >= 3) { skeletonInputPoints = corrected - console.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) } } catch (e) { - console.error('[SK_OVER] 보정 실패 → fallback:', e) + logger.error('[SK_OVER] 보정 실패 → fallback:', e) } } @@ -1264,14 +1270,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() - console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) - console.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length) + logger.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) + logger.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 roof.innerLines = roof.innerLines || [] roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected, changRoofLinePoints) - //console.log("roofInnerLines:::", roof.innerLines); + //logger.log("roofInnerLines:::", roof.innerLines); // [v1 helper 2026-04-29 B 경로] SK 입력 (offset 확장 baseLine 또는 movedPoints) 과 roofLinePoints 간 // gap 을 시각화. innerLines 와 별개 collection. 기존 working code (processInBoth 등) 무영향. @@ -1318,20 +1324,20 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // [LP-TRACE] 실제 저장되는 lastPoints[7] — 이 값이 다음 이동의 prevLast[7] try { const _p7 = rawMovedFull[7] - console.log(`[LP-TRACE] lastPoints.save [7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) len=${rawMovedFull.length}`) + logger.log(`[LP-TRACE] lastPoints.save [7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) len=${rawMovedFull.length}`) } catch (_e) {} - console.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') + logger.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') } else { canvas.skeleton.lastPoints = roofLineContactPoints } canvas.set('skeleton', cleanSkeleton) canvas.renderAll() - //console.log('skeleton rendered.', canvas) + //logger.log('skeleton rendered.', canvas) } catch (e) { - console.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e) - console.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`)) - console.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon)) + logger.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e) + logger.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`)) + logger.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon)) // [보호] 예외 시 상태 플래그만 복구하고, 기존 SK/innerLines는 유지한다. // → 사용자가 힘들게 추가한 라인들이 순간적으로 사라지는 현상 방지. if (canvas.skeletonStates) { @@ -1357,12 +1363,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => { const roof = canvas?.getObjects().find((o) => o.id === roofId) if (!roof) { - console.warn('[SK_OVER_FN] roof 없음 → 중단') + logger.warn('[SK_OVER_FN] roof 없음 → 중단') return } const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes?.roofId === roofId) if (!wall || !wall.baseLines?.length) { - console.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') + logger.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') return } @@ -1372,22 +1378,22 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => const bl = wall.baseLines[i] const planeSize = bl?.attributes?.planeSize ?? Infinity if (planeSize < 1) { - console.log(`[SK_OVER_FN] baseLines[${i}] SHOULDER_ABSORBED skip (planeSize=${planeSize})`) + logger.log(`[SK_OVER_FN] baseLines[${i}] SHOULDER_ABSORBED skip (planeSize=${planeSize})`) continue } if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) { - console.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) + logger.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) continue } rawPoints.push({ x: bl.x1, y: bl.y1 }) } if (rawPoints.length < 3) { - console.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) + logger.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) return } - console.log('[SK_OVER_FN] wall.baseLines polygon:', rawPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + logger.log('[SK_OVER_FN] wall.baseLines polygon:', rawPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) // skeletonPoints 마킹 (오버 좌표 그대로) roof.skeletonPoints = rawPoints.map((p) => ({ x: p.x, y: p.y })) @@ -1397,8 +1403,8 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => try { geoJSONPolygon.pop() - console.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) - console.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length) + logger.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) + logger.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) roof.innerLines = roof.innerLines || [] @@ -1431,8 +1437,8 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => canvas.set('skeleton', cleanSkeleton) canvas.renderAll() } catch (e) { - console.error('[SK_OVER_FN] 스켈레톤 생성 오류:', e) - console.error('[SK_OVER_FN] 입력 좌표:', rawPoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`)) + logger.error('[SK_OVER_FN] 스켈레톤 생성 오류:', e) + logger.error('[SK_OVER_FN] 입력 좌표:', rawPoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`)) if (canvas.skeletonStates) canvas.skeletonStates[roofId] = false } } @@ -1571,7 +1577,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver if (rotIdx > 0) { wallLines = [...wallLines.slice(rotIdx), ...wallLines.slice(0, rotIdx)] if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - console.log('[WL-ROT-FIX] wall.lines rotated by', -rotIdx, 'to align with wall.baseLines[0].startPoint') + logger.log('[WL-ROT-FIX] wall.lines rotated by', -rotIdx, 'to align with wall.baseLines[0].startPoint') } } } @@ -1717,7 +1723,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // [PAIR-DIAG] local only — top in 시 페어 인덱스 어긋남 진단 (2026-04-30) // 1) 길이 미스매치 / 2) _keyOfEdge 충돌 / 3) raw 단계 1:1 무너짐 가려내기 위함 if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - console.log('[PAIR-DIAG] lengths', { + logger.log('[PAIR-DIAG] lengths', { roofLines: roofLines.length, wallLines: wallLines.length, wallBaseLines: wall.baseLines.length, @@ -1725,13 +1731,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const _seen = new Map() roofLines.forEach((l, i) => { const k = _keyOfEdge(l) - if (_seen.has(k)) console.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) + if (_seen.has(k)) logger.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) else _seen.set(k, i) }) roofLines.forEach((rl, i) => { const wl = wallLines[i] const wb = wall.baseLines[i] - console.log(`[PAIR-DIAG] raw#${i}`, + logger.log(`[PAIR-DIAG] raw#${i}`, `roof=(${Math.round(rl.x1)},${Math.round(rl.y1)})→(${Math.round(rl.x2)},${Math.round(rl.y2)})`, wl ? `wall=(${Math.round(wl.x1)},${Math.round(wl.y1)})→(${Math.round(wl.x2)},${Math.round(wl.y2)})` : 'wall=∅', wb ? `base=(${Math.round(wb.x1)},${Math.round(wb.y1)})→(${Math.round(wb.x2)},${Math.round(wb.y2)})` : 'base=∅') @@ -1739,11 +1745,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } // [MOVE-TRACE] 진입 스냅샷 (필요 시 주석 해제) - // console.log('[MOVE-TRACE] === getMoveUpDownLine entry ===', + // logger.log('[MOVE-TRACE] === getMoveUpDownLine entry ===', // 'moveUpDown=', roof.moveUpDown, // 'moveFlowLine=', roof.moveFlowLine) // sortWallBaseLines.forEach((bl, i) => { - // console.log(`[MOVE-TRACE] sortWallBaseLines[${i}]`, + // logger.log(`[MOVE-TRACE] sortWallBaseLines[${i}]`, // `(${Math.round(bl?.x1)},${Math.round(bl?.y1)})→(${Math.round(bl?.x2)},${Math.round(bl?.y2)})`, // 'vs sortWallLines', // `(${Math.round(sortWallLines[i]?.x1)},${Math.round(sortWallLines[i]?.y1)})→(${Math.round(sortWallLines[i]?.x2)},${Math.round(sortWallLines[i]?.y2)})`, @@ -1768,11 +1774,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const l = `${lineAxis}${k}` // y1 or x1 const otherL = `${lineAxis}${otherK}` // y2 or x2 - console.log(`${condition}::::isStartEnd:::::`) + logger.log(`${condition}::::isStartEnd:::::`) // [MOVE-TRACE] index 0 전용 (필요 시 주석 해제) // if (index === 0) { - // console.log('[MOVE-TRACE] processInStartEnd idx=0', + // logger.log('[MOVE-TRACE] processInStartEnd idx=0', // 'condition=', condition, // 'isStart=', isStart, // 'inSign=', inSign, @@ -1820,7 +1826,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const wbL = wallBaseLine[l] const wL = wallLine[l] if (Math.abs(wbL - wL) < 0.1) { - console.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) + logger.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) if (moveAxis === 'x') { getAddLine({ x: pLineM, y: pLineL }, { x: newPointM, y: pLineL }, 'green') getAddLine({ x: newPointM, y: pLineL }, ePoint, 'pink') @@ -1852,12 +1858,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } - console.log(`${condition}::::isStartEnd (both):::::`) + logger.log(`${condition}::::isStartEnd (both):::::`) // 원본 기준: moveDistY = abs(roofLine.y - wallBaseLine.y), moveDistX = abs(roofLine.x - wallBaseLine.x) const moveDistY1 = Math.abs(Big(roofLine.y1).minus(wallBaseLine.y1).toNumber()) @@ -1883,7 +1889,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const createHipLine = (fromPoint, toPoint) => { const createdLine = getAddLine(fromPoint, toPoint, 'orange', 'extensionLine') - console.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + logger.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: '#FF0000', strokeWidth: 2 }) createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } innerLines.push(createdLine) @@ -1909,7 +1915,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver target = { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 } } if (!__isNearSkVertex(target)) { - console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS start target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) + logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS start target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) } else { createHipLine(sPoint, target) } @@ -1925,7 +1931,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver target = { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 } } if (!__isNearSkVertex(target)) { - console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS end target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) + logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS end target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) } else { createHipLine(ePoint, target) } @@ -1982,7 +1988,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // }) // } - console.log('wallBaseLines', wall.baseLines) + logger.log('wallBaseLines', wall.baseLines) //wall.baseLine은 움직인라인 let movedLines = [] @@ -1990,7 +1996,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // 조건에 맞는 라인들만 필터링 const validWallLines = [...wallLines].sort((a, b) => a.idx - b.idx).filter((wallLine, index) => wallLine.idx - 1 === index) - console.log('', sortRoofLines, sortWallLines, sortWallBaseLines); + logger.log('', sortRoofLines, sortWallLines, sortWallBaseLines); (sortWallLines.length === sortWallBaseLines.length && sortWallBaseLines.length > 3) && sortWallLines.forEach((wallLine, index) => { @@ -2003,7 +2009,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // 임계 = 10: useMovementSetting OVER_EPS=0.5 (canvas unit) 시 잔여 길이=0.5 → planeSize=5 (calcLinePlaneSize × 10). // 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음. if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 10) { - console.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`) + logger.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`) return } @@ -2016,16 +2022,16 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const wallIdWl = wallLine.attributes?.wallId ?? wallLine.attributes?.idx ?? '?' const wallIdWb = wallBaseLine.attributes?.wallId ?? wallBaseLine.attributes?.idx ?? '?' const dirMatch = wlDir === wbDir - console.log(`🔗 [pair#${index}] wallId wl=${wallIdWl} wb=${wallIdWb} ${dirMatch ? '' : '⚠️DIR_MISMATCH '}wlDir=${wlDir} wbDir=${wbDir} wl=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wb=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + logger.log(`🔗 [pair#${index}] wallId wl=${wallIdWl} wb=${wallIdWb} ${dirMatch ? '' : '⚠️DIR_MISMATCH '}wlDir=${wlDir} wbDir=${wbDir} wl=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wb=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) } //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)) + // logger.log('index::::', index) + // logger.log('roofLine:', roofLine.x1, roofLine.y1, roofLine.x2, roofLine.y2) + // logger.log('wallLine:', wallLine.x1, wallLine.y1, wallLine.x2, wallLine.y2) + // logger.log('wallBaseLine:', wallBaseLine.x1, wallBaseLine.y1, wallBaseLine.x2, wallBaseLine.y2) + // logger.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) @@ -2060,7 +2066,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } for (const [p1, p2] of __cgPairs) { if (Math.hypot(p2.x - p1.x, p2.y - p1.y) < 0.5) continue - console.log(`🎯 [corner-gap eaveHelpLine] index=${index} p1=(${Math.round(p1.x)},${Math.round(p1.y)})→p2=(${Math.round(p2.x)},${Math.round(p2.y)}) wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + logger.log(`🎯 [corner-gap eaveHelpLine] index=${index} p1=(${Math.round(p1.x)},${Math.round(p1.y)})→p2=(${Math.round(p2.x)},${Math.round(p2.y)}) wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) const __cgLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, @@ -2106,7 +2112,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver movedLines.push({ index, p1, p2 }) // [진단] eaveHelpLine 생성 추적: 어느 index/어느 wallBaseLine-wallLine 쌍에서 만들어지는지 - console.log('🎯 [getAddLine]', { + logger.log('🎯 [getAddLine]', { index, lineType, p1: { x: Math.round(p1.x), y: Math.round(p1.y) }, @@ -2120,7 +2126,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const dy = Math.abs(p2.y - p1.y); const isDiagonal = dx > 0.5 && dy > 0.5; // x, y 변화가 모두 있으면 대각선 - //console.log("mergeLines:::::::", mergeLines); + //logger.log("mergeLines:::::::", mergeLines); const line = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, @@ -2184,7 +2190,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const midY = (wallBaseLine.y1 + wallBaseLine.y2) / 2 const isH = Math.abs(wallBaseLine.y1 - wallBaseLine.y2) < 0.5 const isV = Math.abs(wallBaseLine.x1 - wallBaseLine.x2) < 0.5 - console.log(`🧭 [getSelectLinePosition 결과] idx=${index} position=${mLine.position} orient=${mLine.orientation} mid=(${Math.round(midX)},${Math.round(midY)}) isH=${isH} isV=${isV} wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + logger.log(`🧭 [getSelectLinePosition 결과] idx=${index} position=${mLine.position} orient=${mLine.orientation} mid=(${Math.round(midX)},${Math.round(midY)}) isH=${isH} isV=${isV} wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) } if (getOrientation(roofLine) === 'vertical') { @@ -2241,7 +2247,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } @@ -2300,7 +2306,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } if (isStartEnd.end) { - console.log('left_out::::isStartEnd:::::', isStartEnd) + logger.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() @@ -2365,13 +2371,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - console.log('right_out::::isStartEnd:::::', isStartEnd) + logger.log('right_out::::isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y1).plus(moveDist).toNumber() const bStartY = Big(wallLine.y1).plus(moveDist).toNumber() @@ -2526,13 +2532,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - console.log('top_out isStartEnd:::::::', isStartEnd) + logger.log('top_out isStartEnd:::::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() const bStartX = Big(wallLine.x1).plus(moveDist).toNumber() @@ -2585,7 +2591,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver getAddLine(newPStart, newPEnd, 'red') } if (isStartEnd.end) { - console.log('isStartEnd:::::', isStartEnd) + logger.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() @@ -2637,7 +2643,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver getAddLine(newPStart, newPEnd, 'red') } } else if (condition === 'bottom_out') { - console.log('bottom_out isStartEnd:::::::', isStartEnd) + logger.log('bottom_out isStartEnd:::::::', isStartEnd) // [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨. // roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현. @@ -2649,13 +2655,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - console.log('isStartEnd:::::::', isStartEnd) + logger.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() @@ -2708,7 +2714,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } if (isStartEnd.end) { - console.log('isStartEnd:::::', isStartEnd) + logger.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() @@ -2845,7 +2851,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver if (toRemove.length > 0) { if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - console.log( + logger.log( `[B안 cull] dead-end ridge 제거 ${toRemove.length}개`, toRemove.map( (l) => `(${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})` @@ -2909,7 +2915,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); - console.log('Has matching line:', outerLine); + logger.log('Has matching line:', outerLine); //if(outerLine === null) return } // [hip pitch fallback 2026-04-30] @@ -2956,7 +2962,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { return false } - // console.log('📐 [processEavesEdge] face 분석:', { + // logger.log('📐 [processEavesEdge] face 분석:', { // hasOuterLine: !!outerLine, // outerLineType: outerLine?.attributes?.type, // pitch, @@ -2976,19 +2982,19 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { // 확장된 외곽선에 해당하는 edge는 스킵 if (_isSkipOuter) { - // console.log(' [edge', i, '] SKIP(outerEdge)', { p1: {x: Math.round(p1.x), y: Math.round(p1.y)}, p2: {x: Math.round(p2.x), y: Math.round(p2.y)} }) + // logger.log(' [edge', i, '] SKIP(outerEdge)', { p1: {x: Math.round(p1.x), y: Math.round(p1.y)}, p2: {x: Math.round(p2.x), y: Math.round(p2.y)} }) continue } // 지붕 경계선과 교차 확인 및 클리핑 const clippedLine = clipLineToRoofBoundary(p1, p2, roof.lines, roof.moveSelectLine); - //console.log('clipped line', clippedLine.p1, clippedLine.p2); + //logger.log('clipped line', clippedLine.p1, clippedLine.p2); const isOuterLine = isOuterEdge(clippedLine.p1, clippedLine.p2, [edgeResult.Edge]) // const _dx = Math.abs(clippedLine.p2.x - clippedLine.p1.x) // const _dy = Math.abs(clippedLine.p2.y - clippedLine.p1.y) // const _lineType = (_dx < 0.5 && _dy > 0.5) ? '⚠️수직' : (_dy < 0.5 && _dx > 0.5) ? '수평' : '대각' - // console.log(` [edge ${i}] ${_lineType} ${isOuterLine ? 'OUTER' : 'INNER'} (${Math.round(clippedLine.p1.x)},${Math.round(clippedLine.p1.y)})→(${Math.round(clippedLine.p2.x)},${Math.round(clippedLine.p2.y)}) dx:${Math.round(_dx)} dy:${Math.round(_dy)}`) + // logger.log(` [edge ${i}] ${_lineType} ${isOuterLine ? 'OUTER' : 'INNER'} (${Math.round(clippedLine.p1.x)},${Math.round(clippedLine.p1.y)})→(${Math.round(clippedLine.p2.x)},${Math.round(clippedLine.p2.y)}) dx:${Math.round(_dx)} dy:${Math.round(_dy)}`) addRawLine(roof.id, skeletonLines, clippedLine.p1, clippedLine.p2, 'ridge', '#1083E3', 4, pitch, isOuterLine, targetWallId); // } @@ -3068,7 +3074,7 @@ function logDeadEndLines(skeletonLines, roof) { const dy = Math.abs(line.p2.y - line.p1.y); if (dx < 0.5 && dy < 0.5) { - // console.log('⚠️ [logDeadEndLines] 제로 길이:', { idx, p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y) } }); + // logger.log('⚠️ [logDeadEndLines] 제로 길이:', { idx, p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y) } }); return; } @@ -3078,7 +3084,7 @@ function logDeadEndLines(skeletonLines, roof) { const p2Dead = deadEndVertices.has(k2); if (p1Dead || p2Dead) { - console.log('⚠️ [logDeadEndLines] 삭제 후보:', { + logger.log('⚠️ [logDeadEndLines] 삭제 후보:', { idx, p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y), deadEnd: p1Dead, isRoof: isRoofVertex(line.p1) }, p2: { x: Math.round(line.p2.x), y: Math.round(line.p2.y), deadEnd: p2Dead, isRoof: isRoofVertex(line.p2) }, @@ -3087,7 +3093,7 @@ function logDeadEndLines(skeletonLines, roof) { } }); - // console.log(`🔍 [logDeadEndLines] 전체: ${skeletonLines.length}, 유니크: ${uniqueLines.length}, dead end 꼭짓점: ${deadEndVertices.size}`); + // logger.log(`🔍 [logDeadEndLines] 전체: ${skeletonLines.length}, 유니크: ${uniqueLines.length}, dead end 꼭짓점: ${deadEndVertices.size}`); } function findMatchingLine(edgePolygon, roof, roofPoints) { @@ -3126,7 +3132,7 @@ function findMatchingLine(edgePolygon, roof, roofPoints) { function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, lastSkeletonLines) { const edgePoints = edgeResult.Polygon.map(p => ({ x: p.X, y: p.Y })); //const polygons = createPolygonsFromSkeletonLines(skeletonLines, selectBaseLine); - //console.log("edgePoints::::::", edgePoints) + //logger.log("edgePoints::::::", edgePoints) // 1. Initialize processedLines with a deep copy of lastSkeletonLines let processedLines = [] // 1. 케라바 면과 관련된 불필요한 스켈레톤 선을 제거합니다. @@ -3141,8 +3147,8 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, } } - //console.log("skeletonLines::::::", skeletonLines) - //console.log("lastSkeletonLines", lastSkeletonLines) + //logger.log("skeletonLines::::::", skeletonLines) + //logger.log("lastSkeletonLines", lastSkeletonLines) // 2. Find common lines between skeletonLines and lastSkeletonLines skeletonLines.forEach(line => { @@ -3168,9 +3174,9 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine, // return !isEdgeLine; // }); - //console.log("skeletonLines::::::", skeletonLines); - //console.log("lastSkeletonLines", lastSkeletonLines); - //console.log("processedLines after filtering", processedLines); + //logger.log("skeletonLines::::::", skeletonLines); + //logger.log("lastSkeletonLines", lastSkeletonLines); + //logger.log("processedLines after filtering", processedLines); return processedLines; @@ -3242,7 +3248,7 @@ const calcOverCorrectedPointsSafe = (points, origPoints, lastPoints) => { changedNow[i] ? op : (lastPoints[i] ?? op) ) - console.log('[calcOverCorrectedPointsSafe] changedNow:', + logger.log('[calcOverCorrectedPointsSafe] changedNow:', changedNow.map((v, i) => v ? i : null).filter(v => v !== null)) return calcOverCorrectedPoints(points, virtualOrig) @@ -3265,7 +3271,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x if (origDir * newDir < 0) { - console.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)} → ${Math.round(skPoints[fixedIdx].x)}`) + logger.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)} → ${Math.round(skPoints[fixedIdx].x)}`) skPoints[movedIdx].x = skPoints[fixedIdx].x didClamp = true } @@ -3275,7 +3281,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y if (origDir * newDir < 0) { - console.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)} → ${Math.round(skPoints[fixedIdx].y)}`) + logger.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)} → ${Math.round(skPoints[fixedIdx].y)}`) skPoints[movedIdx].y = skPoints[fixedIdx].y didClamp = true } @@ -3336,7 +3342,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { // 보정 실패 시(꼭짓점 부족) 원본 반환 → 기존 동작 보장 if (cleaned.length >= 3) return cleaned if (deduped.length >= 3) return deduped - console.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') + logger.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') return points } @@ -3419,7 +3425,7 @@ function addRawLine(id, skeletonLines, p1, p2, lineType, color, width, pitch, is }; skeletonLines.push(newLine); - //console.log('skeletonLines', skeletonLines); + //logger.log('skeletonLines', skeletonLines); } /** @@ -4214,14 +4220,14 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { // p2가 다각형 내부에 있는지 확인 const p2Inside = isPointInsidePolygon(p2, roofLines); - //console.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); + //logger.log('p1Inside:', p1Inside, 'p2Inside:', p2Inside); // 두 점 모두 내부에 있으면 그대로 반환 if (p1Inside && p2Inside) { if(!selectLine || isDiagonal){ return { p1: clippedP1, p2: clippedP2 }; } - //console.log('평행선::', clippedP1, clippedP2) + //logger.log('평행선::', clippedP1, clippedP2) return { p1: clippedP1, p2: clippedP2 }; } @@ -4246,7 +4252,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { } } - //console.log('Found intersections:', intersections.length); + //logger.log('Found intersections:', intersections.length); // 교차점들을 t 값으로 정렬 intersections.sort((a, b) => a.t - b.t); @@ -4254,20 +4260,20 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { if (!p1Inside && !p2Inside) { // 두 점 모두 외부에 있는 경우 if (intersections.length >= 2) { - //console.log('Both outside, using intersection points'); + //logger.log('Both outside, using intersection points'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; clippedP2.x = intersections[1].point.x; clippedP2.y = intersections[1].point.y; } else { - //console.log('Both outside, no valid intersections - returning original'); + //logger.log('Both outside, no valid intersections - returning original'); // 교차점이 충분하지 않으면 원본 반환 return { p1: clippedP1, p2: clippedP2 }; } } else if (!p1Inside && p2Inside) { // p1이 외부, p2가 내부 if (intersections.length > 0) { - //console.log('p1 outside, p2 inside - moving p1 to intersection'); + //logger.log('p1 outside, p2 inside - moving p1 to intersection'); clippedP1.x = intersections[0].point.x; clippedP1.y = intersections[0].point.y; // p2는 이미 내부에 있으므로 원본 유지 @@ -4277,7 +4283,7 @@ function clipLineToRoofBoundary(p1, p2, roofLines, selectLine) { } else if (p1Inside && !p2Inside) { // p1이 내부, p2가 외부 if (intersections.length > 0) { - //console.log('p1 inside, p2 outside - moving p2 to intersection'); + //logger.log('p1 inside, p2 outside - moving p2 to intersection'); // p1은 이미 내부에 있으므로 원본 유지 clippedP1.x = p1.x; clippedP1.y = p1.y; @@ -4528,32 +4534,32 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { testDistance = 10, epsilon = 0.5, debug = false } = options; if (!wall || !selectLine) { - if (debug) console.log('ERROR: wall 또는 selectLine이 없음'); + if (debug) logger.log('ERROR: wall 또는 selectLine이 없음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' }; } // selectLine의 좌표 추출 const lineCoords = extractLineCoords(selectLine); if (!lineCoords.valid) { - if (debug) console.log('ERROR: selectLine 좌표가 유효하지 않음'); + if (debug) logger.log('ERROR: selectLine 좌표가 유효하지 않음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' }; } const { x1, y1, x2, y2 } = lineCoords; - //console.log('wall.points', wall.baseLines); + //logger.log('wall.points', wall.baseLines); for(const line of wall.baseLines) { - //console.log('line', line); + //logger.log('line', line); const basePoint = extractLineCoords(line); const { x1: bx1, y1: by1, x2: bx2, y2: by2 } = basePoint; - //console.log('x1, y1, x2, y2', bx1, by1, bx2, by2); + //logger.log('x1, y1, x2, y2', bx1, by1, bx2, by2); // 객체 비교 대신 좌표값 비교 if (Math.abs(bx1 - x1) < 0.1 && Math.abs(by1 - y1) < 0.1 && Math.abs(bx2 - x2) < 0.1 && Math.abs(by2 - y2) < 0.1) { - //console.log('basePoint 일치!!!', basePoint); + //logger.log('basePoint 일치!!!', basePoint); } } @@ -4562,9 +4568,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const lineInfo = analyzeLineOrientation(x1, y1, x2, y2, epsilon); // if (debug) { - // console.log('=== getSelectLinePosition ==='); - // console.log('selectLine 좌표:', lineCoords); - // console.log('라인 방향:', lineInfo.orientation); + // logger.log('=== getSelectLinePosition ==='); + // logger.log('selectLine 좌표:', lineCoords); + // logger.log('라인 방향:', lineInfo.orientation); // } // 라인의 중점 @@ -4585,9 +4591,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const bottomIsInside = checkPointInPolygon(bottomTestPoint, wall); // if (debug) { - // console.log('수평선 테스트:'); - // console.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); - // console.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); + // logger.log('수평선 테스트:'); + // logger.log(' 위쪽 포인트:', topTestPoint, '-> 내부:', topIsInside); + // logger.log(' 아래쪽 포인트:', bottomTestPoint, '-> 내부:', bottomIsInside); // } // top 조건: 위쪽이 외부, 아래쪽이 내부 @@ -4611,9 +4617,9 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const rightIsInside = checkPointInPolygon(rightTestPoint, wall); // if (debug) { - // console.log('수직선 테스트:'); - // console.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); - // console.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); + // logger.log('수직선 테스트:'); + // logger.log(' 왼쪽 포인트:', leftTestPoint, '-> 내부:', leftIsInside); + // logger.log(' 오른쪽 포인트:', rightTestPoint, '-> 내부:', rightIsInside); // } // left 조건: 왼쪽이 외부, 오른쪽이 내부 @@ -4627,7 +4633,7 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { } else { // 대각선 - if (debug) console.log('대각선은 지원하지 않음'); + if (debug) logger.log('대각선은 지원하지 않음'); return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' }; } @@ -4647,7 +4653,7 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { }; // if (debug) { - // console.log('최종 결과:', result); + // logger.log('최종 결과:', result); // } return result; @@ -4658,7 +4664,7 @@ const checkPointInPolygon = (point, wall) => { // 2. wall.baseLines를 이용한 Ray Casting Algorithm if (!wall.baseLines || !Array.isArray(wall.baseLines)) { - console.warn('wall.baseLines가 없습니다'); + logger.warn('wall.baseLines가 없습니다'); return false; } @@ -4801,7 +4807,7 @@ function pointToLineDistance(point, lineP1, lineP2) { const getOrientation = (line, eps = 0.1) => { if (!line) { - console.error('line 객체가 유효하지 않습니다:', line); + logger.error('line 객체가 유효하지 않습니다:', line); return null; // 또는 적절한 기본값 반환 } @@ -4823,7 +4829,7 @@ const getOrientation = (line, eps = 0.1) => { if (dx < eps && dy < eps) return 'point'; return 'diagonal'; } catch (e) { - console.error('방향 계산 중 오류 발생:', e); + logger.error('방향 계산 중 오류 발생:', e); return null; } } @@ -4842,9 +4848,9 @@ export const processEaveHelpLines = (lines) => { const mergedHorizontal = mergeLines(horizontalLines, 'horizontal'); // 결과 확인용 로그 - console.log('Original lines:', lines.length); - console.log('Merged vertical:', mergedVertical.length); - console.log('Merged horizontal:', mergedHorizontal.length); + logger.log('Original lines:', lines.length); + logger.log('Merged vertical:', mergedVertical.length); + logger.log('Merged horizontal:', mergedHorizontal.length); return [...mergedVertical, ...mergedHorizontal]; }; @@ -4887,7 +4893,7 @@ const mergeLines = (lines, direction) => { merged.push(current); // 병합 결과 로그 - console.log(`Merged ${direction} lines:`, merged); + logger.log(`Merged ${direction} lines:`, merged); return merged; }; @@ -4953,11 +4959,11 @@ function updateAndAddLine(innerLines, targetPoint) { if (!foundLine) { foundLine = findLineContainingPoint(innerLines, targetPoint); if (foundLine) { - console.log(`[MOVE-TRACE] HIP fallback matched: position=${targetPoint.position} target=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)}) → (${foundLine.x1.toFixed(1)},${foundLine.y1.toFixed(1)})→(${foundLine.x2.toFixed(1)},${foundLine.y2.toFixed(1)}) type=${foundLine.attributes?.type||'?'}`) + logger.log(`[MOVE-TRACE] HIP fallback matched: position=${targetPoint.position} target=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)}) → (${foundLine.x1.toFixed(1)},${foundLine.y1.toFixed(1)})→(${foundLine.x2.toFixed(1)},${foundLine.y2.toFixed(1)}) type=${foundLine.attributes?.type||'?'}`) } } if (!foundLine) { - console.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); + logger.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); return [...innerLines]; } @@ -5110,7 +5116,7 @@ function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) { const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; if (isOnSegment) { - console.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); + logger.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); } return isOnSegment; @@ -5194,7 +5200,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { } const crossProduct = getTurnDirection(p1, p2, p3); - console.log('crossProduct:', crossProduct); + logger.log('crossProduct:', crossProduct); return crossProduct > 0; } From 25d2fbe4196f678205a86a4cef712ba15aa5bb15 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 13 May 2026 16:39:30 +0900 Subject: [PATCH 04/19] =?UTF-8?q?[2210]=20=EC=B4=88=EA=B8=B0=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=84=A4=EC=A0=95=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/ja.json | 2 +- src/locales/ko.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/locales/ja.json b/src/locales/ja.json index 443b1d8f..79c548b9 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -936,7 +936,7 @@ "main.popup.login.newPassword1": "新しいパスワードを入力", "main.popup.login.newPassword2": "新しいパスワードの再入力", "main.popup.login.placeholder": "半角10文字以内", - "main.popup.login.guide1": "初期化されたパスワードでログインした場合、パスワードを変更しなければサイト利用が可能です。", + "main.popup.login.guide1": "初回ログイン時には、パスワード変更後にサイトをご利用いただけます。", "main.popup.login.guide2": "パスワードを変更しない場合は、ログイン画面に進みます。", "main.popup.login.btn1": "変更", "main.popup.login.btn2": "変更しない", diff --git a/src/locales/ko.json b/src/locales/ko.json index 3e5ed786..84c95a6b 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -936,7 +936,7 @@ "main.popup.login.newPassword1": "새 비밀번호 입력", "main.popup.login.newPassword2": "새 비밀번호 재입력", "main.popup.login.placeholder": "반각 10자 이내", - "main.popup.login.guide1": "초기화된 비밀번호로 로그인한 경우 비밀번호를 변경해야 사이트 이용이 가능합니다.", + "main.popup.login.guide1": "첫 로그인 시에는 비밀번호 변경 후 사이트를 이용하실 수 있습니다。", "main.popup.login.guide2": "비밀번호를 변경하지 않을 경우 로그인 화면으로 이동합니다.", "main.popup.login.btn1": "변경", "main.popup.login.btn2": "변경안함", From aac4f1fb505cb16e6c57ad1f852530aec8974d81 Mon Sep 17 00:00:00 2001 From: ysCha Date: Wed, 13 May 2026 16:43:13 +0900 Subject: [PATCH 05/19] =?UTF-8?q?[VALLEY-SKIP]=20isValleyVertex=20?= =?UTF-8?q?=EB=85=B8=EC=B9=98=20top=5Fin=20=EC=9E=98=EB=AA=BB=EB=90=9C=20e?= =?UTF-8?q?aveHelpLine=204=EA=B0=9C=20=EC=B0=A8=EB=8B=A8=20=E2=80=94=20zer?= =?UTF-8?q?o-length=20neighbor=20+=20collinear=20continuation=202=EB=8B=A8?= =?UTF-8?q?=20=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 6ba11e2c..ce02181c 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -5153,6 +5153,12 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { let neighborLine = null; + // [valley degenerate skip 2026-05-13] + // SHOULDER_ABSORBED 로 인접 baseLine 이 zero-length 가 되면 (p1==p2) + // cross product 가 노이즈 작은 값이 되어 valley 판정이 우연에 좌우됨 + // (notch 케이스: cross=59.15 → valley=true 오판 → eaveHelpLine 4개 잘못 생성). + // length < 1.0 (관측치 0.10, 0.50) 이면 skip 하고 그 너머 진짜 인접 라인 탐색. + const DEGEN_LEN = 1.0; if (isStartVertex) { neighborLine = allLines.find(l => { if (l === connectedLine) return false; @@ -5160,6 +5166,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { const ly1 = l.y1 ?? l.get?.('y1'); const lx2 = l.x2 ?? l.get?.('x2'); const ly2 = l.y2 ?? l.get?.('y2'); + if (Math.hypot(lx2 - lx1, ly2 - ly1) < DEGEN_LEN) return false; // [valley degenerate skip 2026-05-13] const end = l.endPoint || { x: lx2, y: ly2 }; return isSamePoint(end, targetPoint, tolerance); }); @@ -5170,6 +5177,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { const ly1 = l.y1 ?? l.get?.('y1'); const lx2 = l.x2 ?? l.get?.('x2'); const ly2 = l.y2 ?? l.get?.('y2'); + if (Math.hypot(lx2 - lx1, ly2 - ly1) < DEGEN_LEN) return false; // [valley degenerate skip 2026-05-13] const start = l.startPoint || { x: lx1, y: ly1 }; return isSamePoint(start, targetPoint, tolerance); }); @@ -5200,7 +5208,29 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { } const crossProduct = getTurnDirection(p1, p2, p3); - logger.log('crossProduct:', crossProduct); + // [collinear continuation 2026-05-13] + // neigh→target→conn 이 거의 직선이면 (노치 꼬리/연속 segment) + // drift (round 0.1) 로 cross 가 미세한 양/음수 노이즈 → valley 오판. + // |cross| / (|v1|*|v2|) = |sin(theta)|. < 0.05 (≈ 2.9°) 면 직선 연속으로 간주. + const v1len = Math.hypot(p2.x - p1.x, p2.y - p1.y); + const v2len = Math.hypot(p3.x - p2.x, p3.y - p2.y); + let collinearSkip = false; + if (v1len > 0 && v2len > 0) { + const sinTheta = Math.abs(crossProduct) / (v1len * v2len); + if (sinTheta < 0.05) collinearSkip = true; + } + // [valley diag 2026-05-13] L자(정상) vs notch(E-1~E-4 발생) 비교용 + const fmt = (p) => `(${Math.round(p.x)},${Math.round(p.y)})`; + const cLen = Math.hypot(clx2 - clx1, cly2 - cly1); + const nLen = Math.hypot(nlx2 - nlx1, nly2 - nly1); + logger.log( + `[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` + + `conn=${fmt({x:clx1,y:cly1})}→${fmt({x:clx2,y:cly2})}[len=${cLen.toFixed(2)}] ` + + `neigh=${fmt({x:nlx1,y:nly1})}→${fmt({x:nlx2,y:nly2})}[len=${nLen.toFixed(2)}] ` + + `p1=${fmt(p1)} p2=${fmt(p2)} p3=${fmt(p3)} cross=${crossProduct.toFixed(2)} ` + + `valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}` + ); + if (collinearSkip) return false; return crossProduct > 0; } From 490ab6c49f84775ecddc8c33f7d8c18eb1b2eb03 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 10:36:11 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[1961]=20split=20=EA=B8=B8=EC=9D=B4=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=20Math.round(hypot)*10=20=E2=86=92=20calcLin?= =?UTF-8?q?ePlaneSize=20=ED=86=B5=EC=9D=BC=20=E2=80=94=20=EC=A2=8C?= =?UTF-8?q?=EC=9A=B0=EB=8C=80=EC=B9=AD=20hip=20=EB=9D=BC=EB=B2=A8=201mm=20?= =?UTF-8?q?=EA=B0=88=EB=A6=BC=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roofcover/useRoofAllocationSetting.js | 45 ------------------- src/hooks/usePolygon.js | 18 +++++--- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/src/hooks/roofcover/useRoofAllocationSetting.js b/src/hooks/roofcover/useRoofAllocationSetting.js index 88e7162e..87c6ae06 100644 --- a/src/hooks/roofcover/useRoofAllocationSetting.js +++ b/src/hooks/roofcover/useRoofAllocationSetting.js @@ -670,28 +670,6 @@ export function useRoofAllocationSetting(id) { (obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine') && obj.roofId === roofBase.id ) - // [ALLOC-DEBUG] canvas 픽업 스냅샷 (삭제 직전 상태) - const snapByLineName = {} - const snapByName = {} - roofEaveHelpLines.forEach((o) => { - snapByLineName[o.lineName] = (snapByLineName[o.lineName] || 0) + 1 - snapByName[o.name || '?'] = (snapByName[o.name || '?'] || 0) + 1 - }) - logger.log( - `[ALLOC-DEBUG] roofBase.id=${roofBase.id} ` + - `roofBase.lines=${roofBase.lines?.length || 0} ` + - `roofBase.innerLines=${roofBase.innerLines?.length || 0} ` + - `canvas픽업=${roofEaveHelpLines.length} ` + - `byLineName=${JSON.stringify(snapByLineName)} ` + - `byName=${JSON.stringify(snapByName)}` - ) - roofEaveHelpLines.forEach((o, i) => { - logger.log( - ` [ALLOC-DEBUG] picked[${i}] name=${o.name} lineName=${o.lineName} ` + - `(${o.x1?.toFixed(1)},${o.y1?.toFixed(1)})→(${o.x2?.toFixed(1)},${o.y2?.toFixed(1)}) ` + - `len=${Math.hypot((o.x2 || 0) - (o.x1 || 0), (o.y2 || 0) - (o.y1 || 0)).toFixed(1)}` - ) - }) // logger.log('roofBase.id:', roofBase.id) // logger.log('roofEaveHelpLines:', roofEaveHelpLines) // logger.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine')) @@ -760,20 +738,6 @@ export function useRoofAllocationSetting(id) { }); // Combine remaining lines with newEaveLines roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines]; - // [ALLOC-DEBUG] 병합 결과 breakdown - logger.log( - `[ALLOC-DEBUG] 병합완료 roofBase.id=${roofBase.id} ` + - `linesToKeep=${linesToKeep.length} ` + - `normalEave=${normalEaveLines.length} ` + - `extension=${extensionLines.length} ` + - `total=${roofBase.lines.length}` - ) - roofBase.lines.forEach((ln, i) => { - logger.log( - ` [ALLOC-DEBUG] merged[${i}] name=${ln.name || '?'} lineName=${ln.lineName || '?'} ` + - `(${ln.x1?.toFixed(1)},${ln.y1?.toFixed(1)})→(${ln.x2?.toFixed(1)},${ln.y2?.toFixed(1)})` - ) - }) } else { roofBase.lines = [...roofEaveHelpLines] } @@ -813,15 +777,6 @@ export function useRoofAllocationSetting(id) { // extensionLine + 동일직선 SK 1:1 통합 (대각선 단일길이/각도 면적 산출) integrateExtensionLines(roofBase) - // [ALLOC-DEBUG] split 직전 최종 입력 - logger.log( - `[ALLOC-DEBUG] split 직전 roofBase.id=${roofBase.id} ` + - `separatePolygon=${roofBase.separatePolygon?.length || 0} ` + - `lines=${roofBase.lines?.length || 0} ` + - `innerLines=${roofBase.innerLines?.length || 0} ` + - `points=${roofBase.points?.length || 0} ` + - `→ ${roofBase.separatePolygon?.length > 0 ? 'splitPolygonWithSeparate' : 'splitPolygonWithLines'}` - ) if (roofBase.separatePolygon.length > 0) { splitPolygonWithSeparate(roofBase.separatePolygon) } else { diff --git a/src/hooks/usePolygon.js b/src/hooks/usePolygon.js index 8b2f7c37..595d63cd 100644 --- a/src/hooks/usePolygon.js +++ b/src/hooks/usePolygon.js @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil' import { fabric } from 'fabric' import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, isPointOnLine } from '@/util/canvas-util' import { QPolygon } from '@/components/fabric/QPolygon' -import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils' +import { calcLinePlaneSize, isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils' import { basicSettingState, flowDisplaySelector } from '@/store/settingAtom' import { fontSelector } from '@/store/fontAtom' import { QLine } from '@/components/fabric/QLine' @@ -1028,7 +1028,10 @@ export const usePolygon = () => { const { intersections, startPoint, endPoint } = line // 원본 라인의 기하학적 길이 (비율 계산용) - const originalGeomLength = Math.round(Math.hypot(line.x2 - line.x1, line.y2 - line.y1)) * 10 + // [ROUND-PRECISION 2026-05-14] Math.round(hypot)*10 → calcLinePlaneSize (Big.js, ×10 후 round). + // 기존: 45.6 → Math.round(45.6)*10 = 460. 伏せ図入力 의 Big.js 경로는 456. + // 배치면 split 결과와 伏せ図入力 SK 빌더의 0.5mm boundary 가 갈리는 근본 원인. + const originalGeomLength = calcLinePlaneSize({ x1: line.x1, y1: line.y1, x2: line.x2, y2: line.y2 }) if (intersections.length === 1) { const newLinePoint1 = [line.x1, line.y1, intersections[0].x, intersections[0].y] @@ -1050,8 +1053,9 @@ export const usePolygon = () => { }) // 분할된 각 세그먼트의 기하학적 길이 - const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10 - const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10 + // [ROUND-PRECISION 2026-05-14] calcLinePlaneSize 사용 — 위 originalGeomLength 와 동일 이유. + const length1 = calcLinePlaneSize({ x1: newLine1.x1, y1: newLine1.y1, x2: newLine1.x2, y2: newLine1.y2 }) + const length2 = calcLinePlaneSize({ x1: newLine2.x1, y1: newLine2.y1, x2: newLine2.x2, y2: newLine2.y2 }) // 분할 시 새 sub-segment 의 planeSize/actualSize 는 새 기하학으로 직접 계산. // 부모 비율을 쓰면 부모 좌표 drift 가 그대로 전파되어 사용자 기대 round 값과 어긋남. @@ -1131,7 +1135,8 @@ export const usePolygon = () => { name: 'newLine', }) - const calcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 + // [ROUND-PRECISION 2026-05-14] calcLinePlaneSize 사용 — Big.js ×10 후 round. + const calcLength = calcLinePlaneSize({ x1: newLine.x1, y1: newLine.y1, x2: newLine.x2, y2: newLine.y2 }) let segPlaneSize, segActualSize if (line.attributes.planeSize && originalGeomLength > 0) { @@ -1164,7 +1169,8 @@ export const usePolygon = () => { attributes: line.attributes, name: 'newLine', }) - const lastCalcLength = Math.round(Math.hypot(newLine.x1 - newLine.x2, newLine.y1 - newLine.y2)) * 10 + // [ROUND-PRECISION 2026-05-14] calcLinePlaneSize 사용 — Big.js ×10 후 round. + const lastCalcLength = calcLinePlaneSize({ x1: newLine.x1, y1: newLine.y1, x2: newLine.x2, y2: newLine.y2 }) let lastPlaneSize, lastActualSize if (line.attributes.planeSize && originalGeomLength > 0) { From 9a10186c17fd254405f2d5dd927dd4cfa3d01d99 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 13:21:54 +0900 Subject: [PATCH 07/19] =?UTF-8?q?[2218]=20=EC=B4=88=EA=B8=B0=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=84=A4=EC=A0=95=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/main/ChangePasswordPop.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/main/ChangePasswordPop.jsx b/src/components/main/ChangePasswordPop.jsx index 58c22d69..3367506b 100644 --- a/src/components/main/ChangePasswordPop.jsx +++ b/src/components/main/ChangePasswordPop.jsx @@ -197,7 +197,7 @@ export default function ChangePasswordPop(props) {
{getMessage('main.popup.login.guide1')} - {getMessage('main.popup.login.guide2')} + {/*{getMessage('main.popup.login.guide2')}*/}
From ccca89fef65c20491b3f46ef8fd96a7a964acbd8 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:32:07 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[2218]=20=EC=B4=88=EA=B8=B0=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=84=A4=EC=A0=95=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/ja.json b/src/locales/ja.json index 79c548b9..ec6d7331 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -936,7 +936,7 @@ "main.popup.login.newPassword1": "新しいパスワードを入力", "main.popup.login.newPassword2": "新しいパスワードの再入力", "main.popup.login.placeholder": "半角10文字以内", - "main.popup.login.guide1": "初回ログイン時には、パスワード変更後にサイトをご利用いただけます。", + "main.popup.login.guide1": "初回ログイン時は、パスワード変更後にサイトをご利用いただけます。", "main.popup.login.guide2": "パスワードを変更しない場合は、ログイン画面に進みます。", "main.popup.login.btn1": "変更", "main.popup.login.btn2": "変更しない", From 281d9bb583e9b6a1cbfbb87f4dc8182627400f22 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:38:27 +0900 Subject: [PATCH 09/19] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.js | 4 +++ src/util/logger.js | 70 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/app/layout.js b/src/app/layout.js index 40386d49..59b1ddcc 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -10,6 +10,8 @@ import Header from '@/components/header/Header' import QModal from '@/components/common/modal/QModal' import PopupManager from '@/components/common/popupManager/PopupManager' import ErrorBoundary from '@/components/common/ErrorBoundary' +import PageTracker from '@/components/common/PageTracker' // [PAGE-TRACKER 2026-05-14] 라우트 변경 시 콘솔/탭타이틀에 경로 표시 +import ModalTracker from '@/components/common/ModalTracker' // [MODAL-TRACKER 2026-05-14] 모달/팝업 open·close 추적 import './globals.css' import '../styles/style.scss' @@ -73,6 +75,8 @@ export default async function RootLayout({ children }) { + + {headerPathname === '/login' || headerPathname === '/join' ? ( {children} ) : ( diff --git a/src/util/logger.js b/src/util/logger.js index 5830c8db..e88b23ff 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -1,30 +1,88 @@ // utils/logger.js const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING; +// [LOGGER-CALLER 2026-05-14] stack 에서 logger.js 이외의 첫 frame 을 추출해 prefix 로 붙임. +// 페이지/hook/컴포넌트 어디서 찍힌 로그인지 콘솔에서 즉시 식별 가능. webpack chunk 매칭 위해 파일명만 추출. +const FILE_LINE_RE = /([\w\-\.]+\.(?:jsx?|tsx?)):(\d+)/ + +// [LOGGER-FORMAT 2026-05-14] 통일 포맷: [HH:mm:ss.SSS] [LEVEL] [caller] +// 레벨별 색상 배지로 콘솔에서 한눈에 구분. +const STYLE_TS = 'color:#888' +const STYLE_CALLER = 'color:#888;font-style:italic' +const STYLE_LEVEL = { + log: 'background:#616161;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + info: 'background:#1e88e5;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + warn: 'background:#fb8c00;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + error: 'background:#e53935;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + debug: 'background:#8e24aa;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', +} + +function formatTime() { + const d = new Date() + const hh = String(d.getHours()).padStart(2, '0') + const mm = String(d.getMinutes()).padStart(2, '0') + const ss = String(d.getSeconds()).padStart(2, '0') + const ms = String(d.getMilliseconds()).padStart(3, '0') + return `${hh}:${mm}:${ss}.${ms}` +} + +function getCallerTag() { + try { + const stack = new Error().stack + if (!stack) return '' + const lines = stack.split('\n') + for (let i = 1; i < lines.length; i++) { + const m = lines[i].match(FILE_LINE_RE) + if (m && m[1] !== 'logger.js') return `${m[1]}:${m[2]}` + } + } catch (_) {} + return '' +} + +function emit(level, args) { + const ts = formatTime() + const caller = getCallerTag() + const levelStyle = STYLE_LEVEL[level] || STYLE_LEVEL.log + const levelLabel = level.toUpperCase() + + // prefix: '[ts] %cLEVEL%c [caller] ' → styled level + dimmed caller + // user 첫 인자가 string 이면 prefix 와 합쳐 %c 호환 유지. + const prefixText = caller + ? `%c[${ts}] %c${levelLabel}%c [${caller}]` + : `%c[${ts}] %c${levelLabel}%c` + const prefixStyles = [STYLE_TS, levelStyle, STYLE_CALLER] + + if (typeof args[0] === 'string') { + console[level](`${prefixText} ${args[0]}`, ...prefixStyles, ...args.slice(1)) + } else { + console[level](prefixText, ...prefixStyles, ...args) + } +} + export const logger = { log: (...args) => { if (isLoggingEnabled) { - console.log(...args); + emit('log', args); } }, error: (...args) => { // 에러는 항상 로깅하거나, 또는 환경에 따라 다르게 처리 - console.error(...args); + emit('error', args); // 운영 환경에서는 서버로 에러를 보내는 코드를 추가할 수도 있음 }, warn: (...args) => { if (isLoggingEnabled) { - console.warn(...args); + emit('warn', args); } }, info: (...args) => { if (isLoggingEnabled) { - console.info(...args); + emit('info', args); } }, debug: (...args) => { if (isLoggingEnabled) { - console.debug(...args); + emit('debug', args); } } -}; \ No newline at end of file +}; From d60bf27778fcd7a84b8aa0dcf5c5acbdd0832248 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:49:41 +0900 Subject: [PATCH 10/19] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/ModalTracker.jsx | 88 ++++++++++++++++++++++++++ src/components/common/PageTracker.jsx | 28 ++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/components/common/ModalTracker.jsx create mode 100644 src/components/common/PageTracker.jsx diff --git a/src/components/common/ModalTracker.jsx b/src/components/common/ModalTracker.jsx new file mode 100644 index 00000000..4ea2957c --- /dev/null +++ b/src/components/common/ModalTracker.jsx @@ -0,0 +1,88 @@ +'use client' + +// [MODAL-TRACKER 2026-05-14] QModal / PopupManager / contextPopup 의 atom 을 구독해 +// 모달·팝업이 열리고 닫힐 때 콘솔에 컴포넌트명/id 를 출력. 모달 컴포넌트 개별 수정 없음. +import { useEffect, useRef } from 'react' +import { useRecoilValue } from 'recoil' +import { modalState, modalContent } from '@/store/modalAtom' +import { popupState, contextPopupState } from '@/store/popupAtom' +import { logger } from '@/util/logger' + +const TRACK_ENABLED = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true' + +const BADGE_OPEN = 'background:#7b1fa2;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' +const BADGE_CLOSE = 'background:#9e9e9e;color:#fff;padding:2px 8px;border-radius:3px' +const BADGE_POPUP = 'background:#00897b;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' +const BADGE_CTX = 'background:#ef6c00;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' + +function describeNode(node) { + if (!node) return 'null' + if (Array.isArray(node)) return `Array(${node.length})` + const t = node?.type + if (!t) return typeof node + return typeof t === 'string' ? t : t.displayName || t.name || 'Anonymous' +} + +export default function ModalTracker() { + const open = useRecoilValue(modalState) + const content = useRecoilValue(modalContent) + const popup = useRecoilValue(popupState) + const ctx = useRecoilValue(contextPopupState) + + const firstModal = useRef(true) + const prevPopupIds = useRef({ config: [], other: [] }) + const prevCtx = useRef(null) + + // QModal open/close + useEffect(() => { + if (!TRACK_ENABLED) return + if (firstModal.current) { + firstModal.current = false + return + } + if (open) { + logger.info(`%c[MODAL OPEN] ${describeNode(content)}`, BADGE_OPEN) + } else { + logger.info(`%c[MODAL CLOSE]`, BADGE_CLOSE) + } + }, [open]) + + // PopupManager push/pop (config + other 두 버킷) + useEffect(() => { + if (!TRACK_ENABLED) return + const prev = prevPopupIds.current + for (const bucket of ['config', 'other']) { + const cur = popup?.[bucket] || [] + const curIds = cur.map((p) => p.id) + const prevIds = prev[bucket] || [] + curIds + .filter((id) => !prevIds.includes(id)) + .forEach((id) => { + const item = cur.find((p) => p.id === id) + logger.info(`%c[POPUP+ ${bucket}] ${describeNode(item?.component)} (id=${id})`, BADGE_POPUP) + }) + prevIds + .filter((id) => !curIds.includes(id)) + .forEach((id) => { + logger.info(`%c[POPUP- ${bucket}] (id=${id})`, BADGE_CLOSE) + }) + } + prevPopupIds.current = { + config: (popup?.config || []).map((p) => p.id), + other: (popup?.other || []).map((p) => p.id), + } + }, [popup]) + + // contextPopup (우클릭 메뉴 등) + useEffect(() => { + if (!TRACK_ENABLED) return + if (ctx && !prevCtx.current) { + logger.info(`%c[CTX POPUP+] ${describeNode(ctx)}`, BADGE_CTX) + } else if (!ctx && prevCtx.current) { + logger.info(`%c[CTX POPUP-]`, BADGE_CLOSE) + } + prevCtx.current = ctx + }, [ctx]) + + return null +} diff --git a/src/components/common/PageTracker.jsx b/src/components/common/PageTracker.jsx new file mode 100644 index 00000000..19eb99d9 --- /dev/null +++ b/src/components/common/PageTracker.jsx @@ -0,0 +1,28 @@ +'use client' + +// [PAGE-TRACKER 2026-05-14] 라우트 변경마다 콘솔 + document.title 에 현재 경로 표시. +// production 에서는 NEXT_PUBLIC_ENABLE_LOGGING=false 라 logger.info 가 무음. +import { useEffect } from 'react' +import { usePathname } from 'next/navigation' +import { logger } from '@/util/logger' + +const TRACK_ENABLED = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true' + +export default function PageTracker() { + const pathname = usePathname() + + useEffect(() => { + if (!TRACK_ENABLED || !pathname) return + + logger.info( + `%c[PAGE] ${pathname}`, + 'background:#1e88e5;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold', + ) + + if (typeof document !== 'undefined') { + document.title = `${pathname} · HANASYS DESIGN` + } + }, [pathname]) + + return null +} From 7e9ec4e3f2df00d2c23eaac4057eaaa71733093b Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:55:05 +0900 Subject: [PATCH 11/19] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/logger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/logger.js b/src/util/logger.js index e88b23ff..c096298c 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -1,5 +1,7 @@ // utils/logger.js -const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING; +// [LOGGER-GUARD 2026-05-14] string 'false' 가 truthy 라 production 에서도 가드를 통과하던 버그 수정. +// 명시적 === 'true' 비교 → Next.js inline replacement + dead code elimination 으로 production 빌드에서 emit 호출 제거. +const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true'; // [LOGGER-CALLER 2026-05-14] stack 에서 logger.js 이외의 첫 frame 을 추출해 prefix 로 붙임. // 페이지/hook/컴포넌트 어디서 찍힌 로그인지 콘솔에서 즉시 식별 가능. webpack chunk 매칭 위해 파일명만 추출. From 91321745d492accbd3f811cccfae84273d9cb4c1 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 16:29:38 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[1423]=20=E8=A8=AD=E8=A8=88=E4=BE=9D?= =?UTF-8?q?=E9=A0=BC=20import=20=EB=A7=A4=ED=95=91=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EC=9E=AC=EC=A0=95=EC=9D=98=20=E2=80=94=20=EB=AF=B8=EB=A7=A4?= =?UTF-8?q?=EC=B9=AD=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=EA=B0=92=20reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/management/StuffDetail.jsx | 118 ++++++++++++++-------- 1 file changed, 75 insertions(+), 43 deletions(-) diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index 0d6f12a8..fc0d1b88 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -23,6 +23,7 @@ import { QcastContext } from '@/app/QcastProvider' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { useSwal } from '@/hooks/useSwal' import { sanitizeIntegerInputEvent } from '@/util/input-utils' +import { logger } from '@/util/logger' import { CalculatorInput } from '@/components/common/input/CalcInput' import Image from 'next/image' @@ -1045,42 +1046,76 @@ export default function StuffDetail() { form.setValue('installHeight', installHeight) form.setValue('remarks', info.remarks) - if (info.saleStoreLevel === '1') { - setSelOptions(info.saleStoreId) - form.setValue('saleStoreId', info.saleStoreId) - form.setValue('saleStoreName', info.saleStoreName) - form.setValue('saleStoreLevel', info.saleStoreLevel) + // [PLANREQ-MATCH 2026-05-12] 매핑 정책 재정의 — 미매칭 시 list append 폐지, 로그인 초기값 reset 으로 전환 + // - 진입 가드 (2차점 로그인 storeLvl='2'): 1차 hidden·본인 firstAgent 고정 → 1차 derive skip, 2차 매핑만 검증, 실패 시 2차만 본인 store 로 reset + // - T01 / 1차점: 1차 게이트 — 1차 매핑 실패 = 전체 실패. info.saleStoreLevel='2' 케이스는 firstAgent derive + 2차 lookup 둘 다 성공해야 set. 한쪽이라도 실패 시 1,2차 모두 로그인 초기값으로 reset + const resetSaleStoresToLoginDefaults = () => { + if (session?.storeId === 'T01') { + setSelOptions('T01') + form.setValue('saleStoreId', 'T01') + form.setValue('saleStoreLevel', session?.storeLvl) + setOtherSelOptions('') + form.setValue('otherSaleStoreId', '') + form.setValue('otherSaleStoreLevel', '') + } else if (session?.storeLvl === '1') { + setSelOptions(session?.storeId) + form.setValue('saleStoreId', session?.storeId) + form.setValue('saleStoreLevel', session?.storeLvl) + setOtherSelOptions('') + form.setValue('otherSaleStoreId', '') + form.setValue('otherSaleStoreLevel', '') + } + // storeLvl='2' 는 별도 처리 — resetSaleStoresToLoginDefaults() 호출되지 않음 + } + + if (session?.storeId !== 'T01' && session?.storeLvl === '2') { + // 2차점 로그인: 1차 derive skip, 2차 매핑만 검증 + const matched2 = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) + if (matched2) { + setOtherSelOptions(info.saleStoreId) + form.setValue('otherSaleStoreId', info.saleStoreId) + form.setValue('otherSaleStoreName', info.saleStoreName) + form.setValue('otherSaleStoreLevel', info.saleStoreLevel) + } else { + // 2차 매핑 실패 → 2차만 본인 store 로 reset + setOtherSelOptions(session?.storeId) + form.setValue('otherSaleStoreId', session?.storeId) + form.setValue('otherSaleStoreLevel', session?.storeLvl) + } + } else if (info.saleStoreLevel === '1') { + // T01 / 1차점 + planreq 가 1차 ID — 1차 매핑만 시도 + const matched1 = saleStoreList.some((s) => s.saleStoreId === info.saleStoreId) + if (matched1) { + setSelOptions(info.saleStoreId) + form.setValue('saleStoreId', info.saleStoreId) + form.setValue('saleStoreName', info.saleStoreName) + form.setValue('saleStoreLevel', info.saleStoreLevel) + setOtherSelOptions('') + form.setValue('otherSaleStoreId', '') + form.setValue('otherSaleStoreLevel', '') + } else { + resetSaleStoresToLoginDefaults() + } } else { - setOtherSelOptions(info.saleStoreId) - form.setValue('otherSaleStoreId', info.saleStoreId) - form.setValue('otherSaleStoreName', info.saleStoreName) - form.setValue('otherSaleStoreLevel', info.saleStoreLevel) + // T01 / 1차점 + planreq 가 2차 ID — firstAgent derive + 2차 lookup 동시 필요 + const matched2 = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => { - if (res?.firstAgentId) { - const firstAgent = { saleStoreId: res.firstAgentId, saleStoreName: res.firstAgentName } - setSaleStoreList((prev) => { - const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) - return exists ? prev : [...prev, firstAgent] - }) - setShowSaleStoreList((prev) => { - const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) - return exists ? prev : [...prev, firstAgent] - }) - setSelOptions(res.firstAgentId) - form.setValue('saleStoreId', res.firstAgentId) + const firstId = res?.firstAgentId + const matchedFirst = firstId && saleStoreList.some((s) => s.saleStoreId === firstId) + if (matched2 && matchedFirst) { + setSelOptions(firstId) + form.setValue('saleStoreId', firstId) form.setValue('saleStoreName', res.firstAgentName) form.setValue('saleStoreLevel', '1') + setOtherSelOptions(info.saleStoreId) + form.setValue('otherSaleStoreId', info.saleStoreId) + form.setValue('otherSaleStoreName', info.saleStoreName) + form.setValue('otherSaleStoreLevel', info.saleStoreLevel) } else { - setSelOptions('') - form.setValue('saleStoreId', '') - form.setValue('saleStoreName', '') - form.setValue('saleStoreLevel', '') + resetSaleStoresToLoginDefaults() } }).catch(() => { - setSelOptions('') - form.setValue('saleStoreId', '') - form.setValue('saleStoreName', '') - form.setValue('saleStoreLevel', '') + resetSaleStoresToLoginDefaults() }) } } @@ -1271,7 +1306,6 @@ export default function StuffDetail() { const onValid = async (actionType) => { const formData = form.getValues(); if(actionType !== 'save') return false - console.log('Action type:', actionType); // 'save' 또는 'tempSave' let errors = {} let fieldNm @@ -1474,7 +1508,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - console.log('error::::::', error) + logger.error('error::::::', error) }) } else { // 수정모드일때는 PUT @@ -1507,7 +1541,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - console.log('error::::::', error) + logger.error('error::::::', error) }) } } @@ -1598,7 +1632,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - console.log('error::::::', error) + logger.error('error::::::', error) }) } else { setIsGlobalLoading(true) @@ -1622,7 +1656,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - console.error('error::::::', error) + logger.error('error::::::', error) }) } } @@ -1692,7 +1726,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - console.log('error::::::', error) + logger.error('error::::::', error) }) }, }) @@ -2051,13 +2085,12 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ - session?.storeLvl === '1' + // [PLANREQ-MATCH 2026-05-11] T01 또는 1차 user(storeLvl='1') 은 2차 enable, 2차 user(storeLvl='2') 만 disable + session?.storeId === 'T01' || session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : otherSaleStoreList.length === 1 - ? true - : false + : true } isClearable={true} value={otherSaleStoreList.filter(function (option) { @@ -2650,15 +2683,14 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ + // [PLANREQ-MATCH 2026-05-11] T01 또는 1차 user(storeLvl='1') 은 2차 enable, 2차 user(storeLvl='2') 만 disable managementState?.tempFlg === '0' ? true - : session?.storeLvl === '1' + : session?.storeId === 'T01' || session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : otherSaleStoreList.length === 1 - ? true - : false + : true } isClearable={managementState?.tempFlg === '0' ? false : true} value={otherSaleStoreList.filter(function (option) { From 042cdc4b83fb6af42a44244b7417724356360188 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 17:04:43 +0900 Subject: [PATCH 13/19] =?UTF-8?q?1=EC=B0=A8=20=EB=A1=9C=EA=B7=B8=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 374 ++++++++++++++++++------------------- 1 file changed, 187 insertions(+), 187 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index ce02181c..5d31367b 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -96,7 +96,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { const _lp = canvas?.skeleton?.lastPoints const _src = _lp ? 'lastPoints' : 'orgRoofPoints' const _p7 = oldPoints?.[7] - logger.log(`[LP-TRACE] movingLineFromSkeleton.in src=${_src} oldPoints[7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) pos=${movePosition} dir=${moveDirection}`) + // logger.log(`[LP-TRACE] movingLineFromSkeleton.in src=${_src} oldPoints[7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) pos=${movePosition} dir=${moveDirection}`) } catch (_e) {} const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); @@ -108,18 +108,18 @@ const movingLineFromSkeleton = (roofId, canvas) => { const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId) if (oppositeLine) { - logger.log('Opposite line found:', oppositeLine); + // logger.log('Opposite line found:', oppositeLine); } else { - logger.log('No opposite line found'); + // logger.log('No opposite line found'); } if(moveFlowLine !== 0) { return oldPoints.map((point, index) => { - logger.log('Point:', point); + // logger.log('Point:', point); const newPoint = { ...point }; const absMove = Big(moveFlowLine).times(2).div(10); - logger.log('skeletonBuilder moveDirection:', moveDirection); + // logger.log('skeletonBuilder moveDirection:', moveDirection); switch (moveDirection) { case 'left': @@ -180,7 +180,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { if (line.position === 'bottom') { - logger.log('oldPoint:', point); + // logger.log('oldPoint:', point); if (isSamePoint(newPoint, line.start)) { newPoint.y = Big(line.start.y).minus(absMove).toNumber(); @@ -199,7 +199,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { // 사용 예시 } - logger.log('newPoint:', newPoint); + // logger.log('newPoint:', newPoint); //baseline 변경 return newPoint; }) @@ -256,10 +256,10 @@ const movingLineFromSkeleton = (roofId, canvas) => { affected.add(k); affected.add((k + 1) % nPts); }); - logger.log('absMove::', moveUpDownLength); + // logger.log('absMove::', moveUpDownLength); // [BR-TRACE local] movingLineFromSkeleton position/direction 분기 const __isLocalMS = process.env.NEXT_PUBLIC_RUN_MODE === 'local' - if (__isLocalMS) logger.log(`[BR-TRACE] movingLineFromSkeleton position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) + // if (__isLocalMS) logger.log(`[BR-TRACE] movingLineFromSkeleton position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) affected.forEach((i) => { const point = newPoints[i]; if (!point) return; @@ -267,21 +267,21 @@ const movingLineFromSkeleton = (roofId, canvas) => { if (position === 'bottom') { if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); - if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) + // if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'top') { if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); - if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) + // if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'left') { if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); - if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) + // if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) } else if (position === 'right') { if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); - if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) + // if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalMS) { - logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + // logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }); @@ -409,7 +409,7 @@ const buildRawMovedPoints = (roofId, canvas) => { // [LP-TRACE] buildRawMovedPoints 진입 — prevLast 유무 및 [7] 현재 값 try { const _pl7 = prevLast?.[7] - logger.log(`[LP-TRACE] buildRaw.in prevLast=${prevLast ? 'Y' : 'N'} prevLast[7]=(${_pl7?.x?.toFixed(1)},${_pl7?.y?.toFixed(1)}) oldPoints[7]=(${oldPoints[7]?.x?.toFixed(1)},${oldPoints[7]?.y?.toFixed(1)})`) + // logger.log(`[LP-TRACE] buildRaw.in prevLast=${prevLast ? 'Y' : 'N'} prevLast[7]=(${_pl7?.x?.toFixed(1)},${_pl7?.y?.toFixed(1)}) oldPoints[7]=(${oldPoints[7]?.x?.toFixed(1)},${oldPoints[7]?.y?.toFixed(1)})`) } catch (_e) {} const selectLine = roof.moveSelectLine @@ -447,7 +447,7 @@ const buildRawMovedPoints = (roofId, canvas) => { }) // [BR-TRACE local] buildRawMovedPoints position/direction 분기 const __isLocalBR = process.env.NEXT_PUBLIC_RUN_MODE === 'local' - if (__isLocalBR) logger.log(`[BR-TRACE] buildRawMovedPoints position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) + // if (__isLocalBR) logger.log(`[BR-TRACE] buildRawMovedPoints position=${position} dir=${moveDirection} affected=${[...affected].join(',')}`) affected.forEach((i) => { const point = newPoints[i] if (!point) return @@ -455,21 +455,21 @@ const buildRawMovedPoints = (roofId, canvas) => { if (position === 'bottom') { if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber() - if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) + // if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} bottom/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'top') { if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber() - if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) + // if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} top/${moveDirection} y ${__by}→${point.y}`) } else if (position === 'left') { if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber() - if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) + // if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} left/${moveDirection} x ${__bx}→${point.x}`) } else if (position === 'right') { if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber() else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber() - if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) + // if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalBR) { - logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + // logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }) @@ -478,12 +478,12 @@ const buildRawMovedPoints = (roofId, canvas) => { const _sel = selectLine const _sp = _sel?.startPoint const _ep = _sel?.endPoint - logger.log(`[LP-TRACE] buildRaw.out matchingLines=${matchingLines.length} selSP=(${_sp?.x?.toFixed(1)},${_sp?.y?.toFixed(1)}) selEP=(${_ep?.x?.toFixed(1)},${_ep?.y?.toFixed(1)}) bp7=(${basePoints[7]?.x?.toFixed(1)},${basePoints[7]?.y?.toFixed(1)}) newPoints[7]=(${newPoints[7]?.x?.toFixed(1)},${newPoints[7]?.y?.toFixed(1)})`) + // logger.log(`[LP-TRACE] buildRaw.out matchingLines=${matchingLines.length} selSP=(${_sp?.x?.toFixed(1)},${_sp?.y?.toFixed(1)}) selEP=(${_ep?.x?.toFixed(1)},${_ep?.y?.toFixed(1)}) bp7=(${basePoints[7]?.x?.toFixed(1)},${basePoints[7]?.y?.toFixed(1)}) newPoints[7]=(${newPoints[7]?.x?.toFixed(1)},${newPoints[7]?.y?.toFixed(1)})`) } catch (_e) {} return newPoints } catch (e) { - logger.warn('[buildRawMovedPoints] 실패 → null fallback:', e) + // logger.warn('[buildRawMovedPoints] 실패 → null fallback:', e) return null } } @@ -512,7 +512,7 @@ export const verifyMoveBoundary = (roofId, canvas) => { try { const roof = canvas?.getObjects().find((o) => o.id === roofId) if (!roof || !Array.isArray(roof.points)) { - logger.log('[verifyMoveBoundary] roof/points 없음 → unknown') + // logger.log('[verifyMoveBoundary] roof/points 없음 → unknown') return 'unknown' } @@ -520,20 +520,20 @@ export const verifyMoveBoundary = (roofId, canvas) => { const moveUpDown = roof.moveUpDown ?? 0 const position = roof.movePosition const direction = roof.moveDirect - logger.log( - `[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}` - ) + // logger.log( + // `[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}` + // ) if (moveFlowLine === 0 && moveUpDown === 0) { - logger.log('[verifyMoveBoundary] 이동 없음 → ok') + // logger.log('[verifyMoveBoundary] 이동 없음 → ok') return 'ok' } const proposed = buildRawMovedPoints(roofId, canvas) const orig = roof.points if (!Array.isArray(proposed) || proposed.length !== orig.length) { - logger.log( - `[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown` - ) + // logger.log( + // `[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown` + // ) return 'unknown' } @@ -556,9 +556,9 @@ export const verifyMoveBoundary = (roofId, canvas) => { const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next]) const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next]) - logger.log( - `[verifyMoveBoundary] [${i}] orig=(${Math.round(orig[i].x)},${Math.round(orig[i].y)}) → proposed=(${Math.round(proposed[i].x)},${Math.round(proposed[i].y)}) crossOrig=${crossOrig.toFixed(1)} crossNew=${crossNew.toFixed(1)}` - ) + // logger.log( + // `[verifyMoveBoundary] [${i}] orig=(${Math.round(orig[i].x)},${Math.round(orig[i].y)}) → proposed=(${Math.round(proposed[i].x)},${Math.round(proposed[i].y)}) crossOrig=${crossOrig.toFixed(1)} crossNew=${crossNew.toFixed(1)}` + // ) // 원래 골짜기(>0) 였던 꼭짓점만 경계 판정 대상 if (crossOrig > 0) { @@ -566,27 +566,27 @@ export const verifyMoveBoundary = (roofId, canvas) => { const boundaryTol = Math.max(1.0, Math.abs(crossOrig) * 0.05) if (crossNew < -boundaryTol) { - logger.warn( - `[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}` - ) + // logger.warn( + // `[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}` + // ) return 'crossed' } if (Math.abs(crossNew) <= boundaryTol) { - logger.log( - `[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})` - ) + // logger.log( + // `[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})` + // ) if (worst === 'ok') worst = 'on-boundary' } } } if (movedIdx.length === 0) { - logger.log('[verifyMoveBoundary] 이동된 꼭짓점 0개 (buildRawMovedPoints 매칭 실패 가능성)') + // logger.log('[verifyMoveBoundary] 이동된 꼭짓점 0개 (buildRawMovedPoints 매칭 실패 가능성)') } - logger.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) + // logger.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) return worst } catch (e) { - logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) + // logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) return 'unknown' } } @@ -601,7 +601,7 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { const movedPoints = movingLineFromSkeleton(roofId, canvas) if (!Array.isArray(movedPoints) || movedPoints.length === 0) { - logger.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') + // logger.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') return null } @@ -651,13 +651,13 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { } if (zeroLenEdges > 0 || severeReduction || selfIntersect) { - logger.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { - movedLen: movedPoints.length, - oldLen: oldPoints.length, - zeroLenEdges, - severeReduction, - selfIntersect, - }) + // logger.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { + // movedLen: movedPoints.length, + // oldLen: oldPoints.length, + // zeroLenEdges, + // severeReduction, + // selfIntersect, + // }) } return movedPoints @@ -735,7 +735,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi if (!il || !isFinite(il.x1) || !isFinite(il.y1) || !isFinite(il.x2) || !isFinite(il.y2)) return false return il.lineName === 'hip' || il.name === 'hip' }) - logger.log(`[v1 ext] HIP 추출 ${hipLines.length}개 / 전체 innerLines ${roof.innerLines.length}`) + // logger.log(`[v1 ext] HIP 추출 ${hipLines.length}개 / 전체 innerLines ${roof.innerLines.length}`) // 절삭 대상: roofLine seg + canvas 의 다른 보조라인 (eaveHelpLine 등). // 기존 helper 자체(BASELINE_TO_ROOFLINE_HELPER_TYPE) / wall/roof 본체 / 텍스트 등은 제외. @@ -765,7 +765,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi const s = segs[k] auxList.push(`${s.__name || '?'}=(${Math.round(s.A.x)},${Math.round(s.A.y)})→(${Math.round(s.B.x)},${Math.round(s.B.y)}) len=${Math.round(Math.hypot(s.B.x - s.A.x, s.B.y - s.A.y))}`) } - logger.log(`[BR-SEGS] roofLine=${m} aux=${segs.length - m}`, auxList) + // logger.log(`[BR-SEGS] roofLine=${m} aux=${segs.length - m}`, auxList) } // [v1 ext 정정] ext 는 wallbaseLine 의 꼭지점(corner) 에 outer 끝점이 일치하는 hip 만 대상. @@ -784,16 +784,16 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // outer 끝이 wallbaseLine corner 에 일치하지 않으면 ext 대상 아님. if (outerCornerDist > CORNER_EPS) { - logger.log( - `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + - `corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip` - ) + // logger.log( + // `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + + // `corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip` + // ) continue } // 이미 처마(roofLine) 위에 있으면 ext 불필요. if (isPointOnRoofLine(outerEnd, 1.0)) { - logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 이미 roofLine 위 → skip`) + // logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 이미 roofLine 위 → skip`) continue } @@ -815,7 +815,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // [BR-RAY] local only — finite t 인 모든 후보 hit 찍기 (어떤 seg 가 가장 빠른지 비교용) if (__isLocalRayLog && isFinite(t)) { const __label = (k < m) ? `roofLine#${k}` : (segs[k].__name || `aux#${k - m}`) - logger.log(`[BR-RAY] hip outer=(${Math.round(outerEnd.x)},${Math.round(outerEnd.y)}) ${__label} t=${t.toFixed(2)} seg=(${Math.round(A.x)},${Math.round(A.y)})→(${Math.round(B.x)},${Math.round(B.y)})`) + // logger.log(`[BR-RAY] hip outer=(${Math.round(outerEnd.x)},${Math.round(outerEnd.y)}) ${__label} t=${t.toFixed(2)} seg=(${Math.round(A.x)},${Math.round(A.y)})→(${Math.round(B.x)},${Math.round(B.y)})`) } if (t < bestT) { bestT = t @@ -824,7 +824,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi } } if (!isFinite(bestT) || bestT < 0.5) { - logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) NO_HIT or 너무 가까움`) + // logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) NO_HIT or 너무 가까움`) continue } @@ -833,18 +833,18 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi // eaveHelpLine 이 ray 경로를 가로막으면 이미 eave/aux 영역에 도달한 것이므로 더 그릴 필요 없음. // roofLine 승자만 진짜 "처마 도달" 로 인정 → ext 그림. if (bestSegName === 'eaveHelpLine') { - logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 승자=eaveHelpLine bestT=${bestT.toFixed(2)} → skip`) + // logger.log(`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) 승자=eaveHelpLine bestT=${bestT.toFixed(2)} → skip`) continue } const hitX = outerEnd.x + ux * bestT const hitY = outerEnd.y + uy * bestT - logger.log( - `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + - `dir=(${ux.toFixed(3)},${uy.toFixed(3)}) bestT=${bestT.toFixed(2)} src=${bestSrc} ` + - `→ ext=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)})→(${hitX.toFixed(1)},${hitY.toFixed(1)})` - ) + // logger.log( + // `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + + // `dir=(${ux.toFixed(3)},${uy.toFixed(3)}) bestT=${bestT.toFixed(2)} src=${bestSrc} ` + + // `→ ext=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)})→(${hitX.toFixed(1)},${hitY.toFixed(1)})` + // ) // ───────────────────────────────────────────────────────────────────────── // [hip 좌표 연장 2026-04-30] @@ -884,10 +884,10 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi if (typeof hip.setLength === 'function') hip.setLength() if (typeof hip.addLengthText === 'function') hip.addLengthText() - logger.log( - `[v1 ext] hip 연장 oldLen=${oldLen.toFixed(1)} +extLen=${extLen.toFixed(1)} ratio=${ratio.toFixed(3)} ` + - `(plane ${oldPlane}→${hip.attributes.planeSize}, actual ${oldActual}→${hip.attributes.actualSize})` - ) + // logger.log( + // `[v1 ext] hip 연장 oldLen=${oldLen.toFixed(1)} +extLen=${extLen.toFixed(1)} ratio=${ratio.toFixed(3)} ` + + // `(plane ${oldPlane}→${hip.attributes.planeSize}, actual ${oldActual}→${hip.attributes.actualSize})` + // ) // ───────────────────────────────────────────────────────────────────────── // [확장선 스타일] local: 빨강 점선(디버그 가시화), dev/prd: innerLine 동일색·실선(연결된 것처럼). @@ -975,7 +975,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 1. 지붕 폴리곤 유효성 검사 const coordinates = preprocessPolygonCoordinates(roof.points) if (coordinates.length < 3) { - logger.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') + // logger.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') return } @@ -983,10 +983,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const moveUpDown = roof.moveUpDown || 0 // 디버그: offset 변경 + moveLine 상태 확인 - logger.log('[DEBUG] moveFlowLine:', moveFlowLine, 'moveUpDown:', moveUpDown) - logger.log('[DEBUG] wall.lines:', wall.lines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) - logger.log('[DEBUG] wall.baseLines:', wall.baseLines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) - logger.log('[DEBUG] roof.points:', roof.points.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[DEBUG] moveFlowLine:', moveFlowLine, 'moveUpDown:', moveUpDown) + // logger.log('[DEBUG] wall.lines:', wall.lines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) + // logger.log('[DEBUG] wall.baseLines:', wall.baseLines.map((l, i) => `[${i}] (${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`)) + // logger.log('[DEBUG] roof.points:', roof.points.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) // 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거 const isClosedPolygon = (points) => @@ -1076,10 +1076,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { try { const __moveVerdict = verifyMoveBoundary(roofId, canvas) if (__moveVerdict === 'crossed') { - logger.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') + // logger.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') } } catch (e) { - logger.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) + // logger.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) } const movedPoints = safeMovedPointsWithFallback(roofId, canvas) @@ -1128,8 +1128,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { if (SK_INPUT_USE_WALL_BASELINE_DIRECT) { changRoofLinePoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y })) roofLineContactPoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y })) - logger.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):', - changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) + // logger.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):', + // changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) // [v1 평행오버 corner 흡수 2026-04-29] // 평행오버 시 OVER_GUARD 가 인접 baseLine 길이를 OVER_EPS(0.5) 만큼만 남김. @@ -1162,15 +1162,15 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { kept = next } if (absorbedAll.length > 0 && kept.length >= 3) { - logger.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`) - logger.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`, - kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) + // logger.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`) + // logger.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`, + // kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) changRoofLinePoints = kept roofLineContactPoints = kept.map((p) => ({ x: p.x, y: p.y })) } } - logger.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) // 좌표 유효성 검증 const invalidPoints = changRoofLinePoints.filter((p, i) => @@ -1187,7 +1187,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const dist = Math.hypot(next.x - curr.x, next.y - curr.y) if (dist < 0.5) { - logger.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) + // logger.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) } } @@ -1198,7 +1198,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x) if (Math.abs(cross) < 1.0) { - logger.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) + // logger.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) } } @@ -1206,14 +1206,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { let isOverDetected = false const origPoints = roof.points || [] const n = changRoofLinePoints.length - logger.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) - logger.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) - logger.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => { - const o = origPoints[i] - if (!o) return null - const dist = Math.hypot(p.x - o.x, p.y - o.y) - return dist > 1 ? `[${i}] dx=${Math.round(p.x - o.x)} dy=${Math.round(p.y - o.y)}` : null - }).filter(Boolean)) + // logger.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => { + // const o = origPoints[i] + // if (!o) return null + // const dist = Math.hypot(p.x - o.x, p.y - o.y) + // return dist > 1 ? `[${i}] dx=${Math.round(p.x - o.x)} dy=${Math.round(p.y - o.y)}` : null + // }).filter(Boolean)) if (origPoints.length === n && n >= 3) { for (let i = 0; i < n; i++) { const prevOrig = origPoints[(i - 1 + n) % n] @@ -1257,7 +1257,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { ) if (corrected && corrected.length >= 3) { skeletonInputPoints = corrected - logger.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) } } catch (e) { logger.error('[SK_OVER] 보정 실패 → fallback:', e) @@ -1270,8 +1270,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { try { // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 geoJSONPolygon.pop() - logger.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) - logger.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length) + // logger.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) + // logger.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) // 스켈레톤 데이터를 기반으로 내부선 생성 @@ -1324,9 +1324,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // [LP-TRACE] 실제 저장되는 lastPoints[7] — 이 값이 다음 이동의 prevLast[7] try { const _p7 = rawMovedFull[7] - logger.log(`[LP-TRACE] lastPoints.save [7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) len=${rawMovedFull.length}`) + // logger.log(`[LP-TRACE] lastPoints.save [7]=(${_p7?.x?.toFixed(1)},${_p7?.y?.toFixed(1)}) len=${rawMovedFull.length}`) } catch (_e) {} - logger.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') + // logger.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') } else { canvas.skeleton.lastPoints = roofLineContactPoints } @@ -1363,12 +1363,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => { const roof = canvas?.getObjects().find((o) => o.id === roofId) if (!roof) { - logger.warn('[SK_OVER_FN] roof 없음 → 중단') + // logger.warn('[SK_OVER_FN] roof 없음 → 중단') return } const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes?.roofId === roofId) if (!wall || !wall.baseLines?.length) { - logger.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') + // logger.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') return } @@ -1378,22 +1378,22 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => const bl = wall.baseLines[i] const planeSize = bl?.attributes?.planeSize ?? Infinity if (planeSize < 1) { - logger.log(`[SK_OVER_FN] baseLines[${i}] SHOULDER_ABSORBED skip (planeSize=${planeSize})`) + // logger.log(`[SK_OVER_FN] baseLines[${i}] SHOULDER_ABSORBED skip (planeSize=${planeSize})`) continue } if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) { - logger.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) + // logger.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) continue } rawPoints.push({ x: bl.x1, y: bl.y1 }) } if (rawPoints.length < 3) { - logger.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) + // logger.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) return } - logger.log('[SK_OVER_FN] wall.baseLines polygon:', rawPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) + // logger.log('[SK_OVER_FN] wall.baseLines polygon:', rawPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`)) // skeletonPoints 마킹 (오버 좌표 그대로) roof.skeletonPoints = rawPoints.map((p) => ({ x: p.x, y: p.y })) @@ -1403,8 +1403,8 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => try { geoJSONPolygon.pop() - logger.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) - logger.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length) + // logger.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) + // logger.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) roof.innerLines = roof.innerLines || [] @@ -1577,7 +1577,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver if (rotIdx > 0) { wallLines = [...wallLines.slice(rotIdx), ...wallLines.slice(0, rotIdx)] if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - logger.log('[WL-ROT-FIX] wall.lines rotated by', -rotIdx, 'to align with wall.baseLines[0].startPoint') + // logger.log('[WL-ROT-FIX] wall.lines rotated by', -rotIdx, 'to align with wall.baseLines[0].startPoint') } } } @@ -1723,24 +1723,24 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // [PAIR-DIAG] local only — top in 시 페어 인덱스 어긋남 진단 (2026-04-30) // 1) 길이 미스매치 / 2) _keyOfEdge 충돌 / 3) raw 단계 1:1 무너짐 가려내기 위함 if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - logger.log('[PAIR-DIAG] lengths', { - roofLines: roofLines.length, - wallLines: wallLines.length, - wallBaseLines: wall.baseLines.length, - }) + // logger.log('[PAIR-DIAG] lengths', { + // roofLines: roofLines.length, + // wallLines: wallLines.length, + // wallBaseLines: wall.baseLines.length, + // }) const _seen = new Map() roofLines.forEach((l, i) => { const k = _keyOfEdge(l) - if (_seen.has(k)) logger.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) - else _seen.set(k, i) + // if (_seen.has(k)) logger.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) + // else _seen.set(k, i) }) roofLines.forEach((rl, i) => { const wl = wallLines[i] const wb = wall.baseLines[i] - logger.log(`[PAIR-DIAG] raw#${i}`, - `roof=(${Math.round(rl.x1)},${Math.round(rl.y1)})→(${Math.round(rl.x2)},${Math.round(rl.y2)})`, - wl ? `wall=(${Math.round(wl.x1)},${Math.round(wl.y1)})→(${Math.round(wl.x2)},${Math.round(wl.y2)})` : 'wall=∅', - wb ? `base=(${Math.round(wb.x1)},${Math.round(wb.y1)})→(${Math.round(wb.x2)},${Math.round(wb.y2)})` : 'base=∅') + // logger.log(`[PAIR-DIAG] raw#${i}`, + // `roof=(${Math.round(rl.x1)},${Math.round(rl.y1)})→(${Math.round(rl.x2)},${Math.round(rl.y2)})`, + // wl ? `wall=(${Math.round(wl.x1)},${Math.round(wl.y1)})→(${Math.round(wl.x2)},${Math.round(wl.y2)})` : 'wall=∅', + // wb ? `base=(${Math.round(wb.x1)},${Math.round(wb.y1)})→(${Math.round(wb.x2)},${Math.round(wb.y2)})` : 'base=∅') }) } @@ -1774,7 +1774,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const l = `${lineAxis}${k}` // y1 or x1 const otherL = `${lineAxis}${otherK}` // y2 or x2 - logger.log(`${condition}::::isStartEnd:::::`) + // logger.log(`${condition}::::isStartEnd:::::`) // [MOVE-TRACE] index 0 전용 (필요 시 주석 해제) // if (index === 0) { @@ -1826,7 +1826,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const wbL = wallBaseLine[l] const wL = wallLine[l] if (Math.abs(wbL - wL) < 0.1) { - logger.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) + // logger.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) if (moveAxis === 'x') { getAddLine({ x: pLineM, y: pLineL }, { x: newPointM, y: pLineL }, 'green') getAddLine({ x: newPointM, y: pLineL }, ePoint, 'pink') @@ -1858,12 +1858,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + // logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } - logger.log(`${condition}::::isStartEnd (both):::::`) + // logger.log(`${condition}::::isStartEnd (both):::::`) // 원본 기준: moveDistY = abs(roofLine.y - wallBaseLine.y), moveDistX = abs(roofLine.x - wallBaseLine.x) const moveDistY1 = Math.abs(Big(roofLine.y1).minus(wallBaseLine.y1).toNumber()) @@ -1889,7 +1889,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const createHipLine = (fromPoint, toPoint) => { const createdLine = getAddLine(fromPoint, toPoint, 'orange', 'extensionLine') - logger.log('extensionLine created - length:', createdLine.length, 'coords:', { x1: createdLine.x1, y1: createdLine.y1, x2: createdLine.x2, y2: createdLine.y2 }) + // logger.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: '#FF0000', strokeWidth: 2 }) createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } innerLines.push(createdLine) @@ -1915,7 +1915,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver target = { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 } } if (!__isNearSkVertex(target)) { - logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS start target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) + // logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS start target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) } else { createHipLine(sPoint, target) } @@ -1931,7 +1931,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver target = { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 } } if (!__isNearSkVertex(target)) { - logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS end target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) + // logger.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS end target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`) } else { createHipLine(ePoint, target) } @@ -1988,7 +1988,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // }) // } - logger.log('wallBaseLines', wall.baseLines) + // logger.log('wallBaseLines', wall.baseLines) //wall.baseLine은 움직인라인 let movedLines = [] @@ -1996,7 +1996,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // 조건에 맞는 라인들만 필터링 const validWallLines = [...wallLines].sort((a, b) => a.idx - b.idx).filter((wallLine, index) => wallLine.idx - 1 === index) - logger.log('', sortRoofLines, sortWallLines, sortWallBaseLines); + // logger.log('', sortRoofLines, sortWallLines, sortWallBaseLines); (sortWallLines.length === sortWallBaseLines.length && sortWallBaseLines.length > 3) && sortWallLines.forEach((wallLine, index) => { @@ -2009,7 +2009,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver // 임계 = 10: useMovementSetting OVER_EPS=0.5 (canvas unit) 시 잔여 길이=0.5 → planeSize=5 (calcLinePlaneSize × 10). // 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음. if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 10) { - logger.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`) + // logger.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`) return } @@ -2022,7 +2022,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const wallIdWl = wallLine.attributes?.wallId ?? wallLine.attributes?.idx ?? '?' const wallIdWb = wallBaseLine.attributes?.wallId ?? wallBaseLine.attributes?.idx ?? '?' const dirMatch = wlDir === wbDir - logger.log(`🔗 [pair#${index}] wallId wl=${wallIdWl} wb=${wallIdWb} ${dirMatch ? '' : '⚠️DIR_MISMATCH '}wlDir=${wlDir} wbDir=${wbDir} wl=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wb=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + // logger.log(`🔗 [pair#${index}] wallId wl=${wallIdWl} wb=${wallIdWb} ${dirMatch ? '' : '⚠️DIR_MISMATCH '}wlDir=${wlDir} wbDir=${wbDir} wl=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wb=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) } //roofline 외곽선 설정 @@ -2066,7 +2066,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } for (const [p1, p2] of __cgPairs) { if (Math.hypot(p2.x - p1.x, p2.y - p1.y) < 0.5) continue - logger.log(`🎯 [corner-gap eaveHelpLine] index=${index} p1=(${Math.round(p1.x)},${Math.round(p1.y)})→p2=(${Math.round(p2.x)},${Math.round(p2.y)}) wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + // logger.log(`🎯 [corner-gap eaveHelpLine] index=${index} p1=(${Math.round(p1.x)},${Math.round(p1.y)})→p2=(${Math.round(p2.x)},${Math.round(p2.y)}) wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) const __cgLine = new QLine([p1.x, p1.y, p2.x, p2.y], { parentId: roof.id, fontSize: roof.fontSize, @@ -2112,15 +2112,15 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver movedLines.push({ index, p1, p2 }) // [진단] eaveHelpLine 생성 추적: 어느 index/어느 wallBaseLine-wallLine 쌍에서 만들어지는지 - logger.log('🎯 [getAddLine]', { - index, - lineType, - p1: { x: Math.round(p1.x), y: Math.round(p1.y) }, - p2: { x: Math.round(p2.x), y: Math.round(p2.y) }, - wallLine: `(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)})`, - wallBaseLine: `(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`, - wallBaseLen: Math.round(Math.hypot(wallBaseLine.x2 - wallBaseLine.x1, wallBaseLine.y2 - wallBaseLine.y1)), - }) + // logger.log('🎯 [getAddLine]', { + // index, + // lineType, + // p1: { x: Math.round(p1.x), y: Math.round(p1.y) }, + // p2: { x: Math.round(p2.x), y: Math.round(p2.y) }, + // wallLine: `(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)})`, + // wallBaseLine: `(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`, + // wallBaseLen: Math.round(Math.hypot(wallBaseLine.x2 - wallBaseLine.x1, wallBaseLine.y2 - wallBaseLine.y1)), + // }) const dx = Math.abs(p2.x - p1.x); const dy = Math.abs(p2.y - p1.y); @@ -2190,7 +2190,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const midY = (wallBaseLine.y1 + wallBaseLine.y2) / 2 const isH = Math.abs(wallBaseLine.y1 - wallBaseLine.y2) < 0.5 const isV = Math.abs(wallBaseLine.x1 - wallBaseLine.x2) < 0.5 - logger.log(`🧭 [getSelectLinePosition 결과] idx=${index} position=${mLine.position} orient=${mLine.orientation} mid=(${Math.round(midX)},${Math.round(midY)}) isH=${isH} isV=${isV} wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) + // logger.log(`🧭 [getSelectLinePosition 결과] idx=${index} position=${mLine.position} orient=${mLine.orientation} mid=(${Math.round(midX)},${Math.round(midY)}) isH=${isH} isV=${isV} wallLine=(${Math.round(wallLine.x1)},${Math.round(wallLine.y1)})→(${Math.round(wallLine.x2)},${Math.round(wallLine.y2)}) wallBaseLine=(${Math.round(wallBaseLine.x1)},${Math.round(wallBaseLine.y1)})→(${Math.round(wallBaseLine.x2)},${Math.round(wallBaseLine.y2)})`) } if (getOrientation(roofLine) === 'vertical') { @@ -2247,7 +2247,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + // logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } @@ -2306,7 +2306,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } if (isStartEnd.end) { - logger.log('left_out::::isStartEnd:::::', isStartEnd) + // logger.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() @@ -2371,13 +2371,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + // logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - logger.log('right_out::::isStartEnd:::::', isStartEnd) + // logger.log('right_out::::isStartEnd:::::', isStartEnd) const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber() const aStartY = Big(roofLine.y1).plus(moveDist).toNumber() const bStartY = Big(wallLine.y1).plus(moveDist).toNumber() @@ -2532,13 +2532,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + // logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - logger.log('top_out isStartEnd:::::::', isStartEnd) + // logger.log('top_out isStartEnd:::::::', isStartEnd) const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() const bStartX = Big(wallLine.x1).plus(moveDist).toNumber() @@ -2591,7 +2591,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver getAddLine(newPStart, newPEnd, 'red') } if (isStartEnd.end) { - logger.log('isStartEnd:::::', isStartEnd) + // logger.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() @@ -2643,7 +2643,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver getAddLine(newPStart, newPEnd, 'red') } } else if (condition === 'bottom_out') { - logger.log('bottom_out isStartEnd:::::::', isStartEnd) + // logger.log('bottom_out isStartEnd:::::::', isStartEnd) // [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨. // roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현. @@ -2655,13 +2655,13 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 if (prevAbsorbed || nextAbsorbed) { - logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) + // logger.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`) return } } if (isStartEnd.start) { - logger.log('isStartEnd:::::::', isStartEnd) + // logger.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() @@ -2714,7 +2714,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver } if (isStartEnd.end) { - logger.log('isStartEnd:::::', isStartEnd) + // logger.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() @@ -2851,12 +2851,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver if (toRemove.length > 0) { if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { - logger.log( - `[B안 cull] dead-end ridge 제거 ${toRemove.length}개`, - toRemove.map( - (l) => `(${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})` - ) - ) + // logger.log( + // `[B안 cull] dead-end ridge 제거 ${toRemove.length}개`, + // toRemove.map( + // (l) => `(${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})` + // ) + // ) } toRemove.forEach((line) => { canvas.remove(line) @@ -2915,7 +2915,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) { if(!outerLine) { outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); - logger.log('Has matching line:', outerLine); + // logger.log('Has matching line:', outerLine); //if(outerLine === null) return } // [hip pitch fallback 2026-04-30] @@ -3084,12 +3084,12 @@ function logDeadEndLines(skeletonLines, roof) { const p2Dead = deadEndVertices.has(k2); if (p1Dead || p2Dead) { - logger.log('⚠️ [logDeadEndLines] 삭제 후보:', { - idx, - p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y), deadEnd: p1Dead, isRoof: isRoofVertex(line.p1) }, - p2: { x: Math.round(line.p2.x), y: Math.round(line.p2.y), deadEnd: p2Dead, isRoof: isRoofVertex(line.p2) }, - dx: Math.round(dx), dy: Math.round(dy), - }); + // logger.log('⚠️ [logDeadEndLines] 삭제 후보:', { + // idx, + // p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y), deadEnd: p1Dead, isRoof: isRoofVertex(line.p1) }, + // p2: { x: Math.round(line.p2.x), y: Math.round(line.p2.y), deadEnd: p2Dead, isRoof: isRoofVertex(line.p2) }, + // dx: Math.round(dx), dy: Math.round(dy), + // }); } }); @@ -3248,8 +3248,8 @@ const calcOverCorrectedPointsSafe = (points, origPoints, lastPoints) => { changedNow[i] ? op : (lastPoints[i] ?? op) ) - logger.log('[calcOverCorrectedPointsSafe] changedNow:', - changedNow.map((v, i) => v ? i : null).filter(v => v !== null)) + // logger.log('[calcOverCorrectedPointsSafe] changedNow:', + // changedNow.map((v, i) => v ? i : null).filter(v => v !== null)) return calcOverCorrectedPoints(points, virtualOrig) } @@ -3271,7 +3271,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x if (origDir * newDir < 0) { - logger.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)} → ${Math.round(skPoints[fixedIdx].x)}`) + // logger.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)} → ${Math.round(skPoints[fixedIdx].x)}`) skPoints[movedIdx].x = skPoints[fixedIdx].x didClamp = true } @@ -3281,7 +3281,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y if (origDir * newDir < 0) { - logger.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)} → ${Math.round(skPoints[fixedIdx].y)}`) + // logger.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)} → ${Math.round(skPoints[fixedIdx].y)}`) skPoints[movedIdx].y = skPoints[fixedIdx].y didClamp = true } @@ -3342,7 +3342,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { // 보정 실패 시(꼭짓점 부족) 원본 반환 → 기존 동작 보장 if (cleaned.length >= 3) return cleaned if (deduped.length >= 3) return deduped - logger.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') + // logger.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') return points } @@ -4534,14 +4534,14 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { const { testDistance = 10, epsilon = 0.5, debug = false } = options; if (!wall || !selectLine) { - if (debug) logger.log('ERROR: wall 또는 selectLine이 없음'); + // if (debug) logger.log('ERROR: wall 또는 selectLine이 없음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' }; } // selectLine의 좌표 추출 const lineCoords = extractLineCoords(selectLine); if (!lineCoords.valid) { - if (debug) logger.log('ERROR: selectLine 좌표가 유효하지 않음'); + // if (debug) logger.log('ERROR: selectLine 좌표가 유효하지 않음'); return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' }; } @@ -4633,7 +4633,7 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => { } else { // 대각선 - if (debug) logger.log('대각선은 지원하지 않음'); + // if (debug) logger.log('대각선은 지원하지 않음'); return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' }; } @@ -4664,7 +4664,7 @@ const checkPointInPolygon = (point, wall) => { // 2. wall.baseLines를 이용한 Ray Casting Algorithm if (!wall.baseLines || !Array.isArray(wall.baseLines)) { - logger.warn('wall.baseLines가 없습니다'); + // logger.warn('wall.baseLines가 없습니다'); return false; } @@ -4848,9 +4848,9 @@ export const processEaveHelpLines = (lines) => { const mergedHorizontal = mergeLines(horizontalLines, 'horizontal'); // 결과 확인용 로그 - logger.log('Original lines:', lines.length); - logger.log('Merged vertical:', mergedVertical.length); - logger.log('Merged horizontal:', mergedHorizontal.length); + // logger.log('Original lines:', lines.length); + // logger.log('Merged vertical:', mergedVertical.length); + // logger.log('Merged horizontal:', mergedHorizontal.length); return [...mergedVertical, ...mergedHorizontal]; }; @@ -4893,7 +4893,7 @@ const mergeLines = (lines, direction) => { merged.push(current); // 병합 결과 로그 - logger.log(`Merged ${direction} lines:`, merged); + // logger.log(`Merged ${direction} lines:`, merged); return merged; }; @@ -4959,11 +4959,11 @@ function updateAndAddLine(innerLines, targetPoint) { if (!foundLine) { foundLine = findLineContainingPoint(innerLines, targetPoint); if (foundLine) { - logger.log(`[MOVE-TRACE] HIP fallback matched: position=${targetPoint.position} target=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)}) → (${foundLine.x1.toFixed(1)},${foundLine.y1.toFixed(1)})→(${foundLine.x2.toFixed(1)},${foundLine.y2.toFixed(1)}) type=${foundLine.attributes?.type||'?'}`) + // logger.log(`[MOVE-TRACE] HIP fallback matched: position=${targetPoint.position} target=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)}) → (${foundLine.x1.toFixed(1)},${foundLine.y1.toFixed(1)})→(${foundLine.x2.toFixed(1)},${foundLine.y2.toFixed(1)}) type=${foundLine.attributes?.type||'?'}`) } } if (!foundLine) { - logger.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); + // logger.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); return [...innerLines]; } @@ -5116,7 +5116,7 @@ function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) { const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; if (isOnSegment) { - logger.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); + // logger.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); } return isOnSegment; @@ -5223,13 +5223,13 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) { const fmt = (p) => `(${Math.round(p.x)},${Math.round(p.y)})`; const cLen = Math.hypot(clx2 - clx1, cly2 - cly1); const nLen = Math.hypot(nlx2 - nlx1, nly2 - nly1); - logger.log( - `[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` + - `conn=${fmt({x:clx1,y:cly1})}→${fmt({x:clx2,y:cly2})}[len=${cLen.toFixed(2)}] ` + - `neigh=${fmt({x:nlx1,y:nly1})}→${fmt({x:nlx2,y:nly2})}[len=${nLen.toFixed(2)}] ` + - `p1=${fmt(p1)} p2=${fmt(p2)} p3=${fmt(p3)} cross=${crossProduct.toFixed(2)} ` + - `valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}` - ); + // logger.log( + // `[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` + + // `conn=${fmt({x:clx1,y:cly1})}→${fmt({x:clx2,y:cly2})}[len=${cLen.toFixed(2)}] ` + + // `neigh=${fmt({x:nlx1,y:nly1})}→${fmt({x:nlx2,y:nly2})}[len=${nLen.toFixed(2)}] ` + + // `p1=${fmt(p1)} p2=${fmt(p2)} p3=${fmt(p3)} cross=${crossProduct.toFixed(2)} ` + + // `valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}` + // ); if (collinearSkip) return false; return crossProduct > 0; } From 347536e87aee8600c755f9ef01bbef60e7274dde Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 17:21:46 +0900 Subject: [PATCH 14/19] =?UTF-8?q?2=EC=B0=A8=20=EB=A1=9C=EA=B7=B8=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index 5d31367b..e46053c4 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -281,7 +281,7 @@ const movingLineFromSkeleton = (roofId, canvas) => { else if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); // if (__isLocalMS) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalMS) { - // logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }); @@ -469,7 +469,7 @@ const buildRawMovedPoints = (roofId, canvas) => { else if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber() // if (__isLocalBR) logger.log(`[BR-TRACE] i=${i} right/${moveDirection} x ${__bx}→${point.x}`) } else if (__isLocalBR) { - // logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) + logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) } }) @@ -483,7 +483,7 @@ const buildRawMovedPoints = (roofId, canvas) => { return newPoints } catch (e) { - // logger.warn('[buildRawMovedPoints] 실패 → null fallback:', e) + logger.warn('[buildRawMovedPoints] 실패 → null fallback:', e) return null } } @@ -566,9 +566,9 @@ export const verifyMoveBoundary = (roofId, canvas) => { const boundaryTol = Math.max(1.0, Math.abs(crossOrig) * 0.05) if (crossNew < -boundaryTol) { - // logger.warn( - // `[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}` - // ) + logger.warn( + `[verifyMoveBoundary] 꼭짓점[${i}] 골짜기 → 볼록 뒤집힘 (CROSSED): ${crossOrig.toFixed(1)} → ${crossNew.toFixed(1)}` + ) return 'crossed' } if (Math.abs(crossNew) <= boundaryTol) { @@ -586,7 +586,7 @@ export const verifyMoveBoundary = (roofId, canvas) => { // logger.log(`[verifyMoveBoundary] 최종 판정: ${worst} (moved indices=[${movedIdx.join(',')}])`) return worst } catch (e) { - // logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) + logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) return 'unknown' } } @@ -601,7 +601,7 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { const movedPoints = movingLineFromSkeleton(roofId, canvas) if (!Array.isArray(movedPoints) || movedPoints.length === 0) { - // logger.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') + logger.warn('[safeMovedPointsWithFallback] movedPoints 비정상 → bail out') return null } @@ -651,13 +651,13 @@ const safeMovedPointsWithFallback = (roofId, canvas) => { } if (zeroLenEdges > 0 || severeReduction || selfIntersect) { - // logger.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { - // movedLen: movedPoints.length, - // oldLen: oldPoints.length, - // zeroLenEdges, - // severeReduction, - // selfIntersect, - // }) + logger.warn('[safeMovedPointsWithFallback] 붕괴/교차 감지 (로그만)', { + movedLen: movedPoints.length, + oldLen: oldPoints.length, + zeroLenEdges, + severeReduction, + selfIntersect, + }) } return movedPoints @@ -975,7 +975,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // 1. 지붕 폴리곤 유효성 검사 const coordinates = preprocessPolygonCoordinates(roof.points) if (coordinates.length < 3) { - // logger.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') + logger.warn('Polygon has less than 3 unique points. Cannot generate skeleton.') return } @@ -1076,10 +1076,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { try { const __moveVerdict = verifyMoveBoundary(roofId, canvas) if (__moveVerdict === 'crossed') { - // logger.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') + logger.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행') } } catch (e) { - // logger.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) + logger.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e) } const movedPoints = safeMovedPointsWithFallback(roofId, canvas) @@ -1187,7 +1187,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const dist = Math.hypot(next.x - curr.x, next.y - curr.y) if (dist < 0.5) { - // logger.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) + logger.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`) } } @@ -1198,7 +1198,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length] const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x) if (Math.abs(cross) < 1.0) { - // logger.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) + logger.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`) } } @@ -1363,12 +1363,12 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => { const roof = canvas?.getObjects().find((o) => o.id === roofId) if (!roof) { - // logger.warn('[SK_OVER_FN] roof 없음 → 중단') + logger.warn('[SK_OVER_FN] roof 없음 → 중단') return } const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes?.roofId === roofId) if (!wall || !wall.baseLines?.length) { - // logger.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') + logger.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단') return } @@ -1382,14 +1382,14 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => continue } if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) { - // logger.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) + logger.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`) continue } rawPoints.push({ x: bl.x1, y: bl.y1 }) } if (rawPoints.length < 3) { - // logger.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) + logger.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`) return } @@ -1731,8 +1731,8 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const _seen = new Map() roofLines.forEach((l, i) => { const k = _keyOfEdge(l) - // if (_seen.has(k)) logger.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) - // else _seen.set(k, i) + if (_seen.has(k)) logger.warn(`[PAIR-DIAG] key collision: idx ${_seen.get(k)} vs ${i} key=${k}`) + else _seen.set(k, i) }) roofLines.forEach((rl, i) => { const wl = wallLines[i] @@ -1826,7 +1826,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver const wbL = wallBaseLine[l] const wL = wallLine[l] if (Math.abs(wbL - wL) < 0.1) { - // logger.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) + logger.warn(`⚠️ [${condition.toUpperCase()}_${isStart ? 'START' : 'END'}] Line crossing detected! newPoint:`, newPointM, 'pLine:', pLineM) if (moveAxis === 'x') { getAddLine({ x: pLineM, y: pLineL }, { x: newPointM, y: pLineL }, 'green') getAddLine({ x: newPointM, y: pLineL }, ePoint, 'pink') @@ -3342,7 +3342,7 @@ const calcOverCorrectedPoints = (points, origPoints) => { // 보정 실패 시(꼭짓점 부족) 원본 반환 → 기존 동작 보장 if (cleaned.length >= 3) return cleaned if (deduped.length >= 3) return deduped - // logger.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') + logger.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환') return points } @@ -4664,7 +4664,7 @@ const checkPointInPolygon = (point, wall) => { // 2. wall.baseLines를 이용한 Ray Casting Algorithm if (!wall.baseLines || !Array.isArray(wall.baseLines)) { - // logger.warn('wall.baseLines가 없습니다'); + logger.warn('wall.baseLines가 없습니다'); return false; } @@ -4963,7 +4963,7 @@ function updateAndAddLine(innerLines, targetPoint) { } } if (!foundLine) { - // logger.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); + logger.warn(`[updateAndAddLine] NOT FOUND position=${targetPoint.position} point=(${targetPoint.x.toFixed(2)},${targetPoint.y.toFixed(2)})`); return [...innerLines]; } From f6652acbd74b6c0b3216e624557e0cc8265687cd Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 17:26:11 +0900 Subject: [PATCH 15/19] =?UTF-8?q?2=EC=B0=A8=20=EB=A1=9C=EA=B7=B8=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/image/cad/route.js | 4 ++-- src/app/api/image/map/route.js | 2 +- src/app/api/image/upload/route.js | 2 +- src/config/config.export.js | 2 +- src/lib/cadAction.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/api/image/cad/route.js b/src/app/api/image/cad/route.js index a0e60a4c..e124408c 100644 --- a/src/app/api/image/cad/route.js +++ b/src/app/api/image/cad/route.js @@ -11,7 +11,7 @@ const s3 = new S3Client({ }) const uploadImage = async (file) => { - console.log('🚀 ~ uploadImage ~ file:', file) + // console.log('🚀 ~ uploadImage ~ file:', file) const Body = Buffer.from(await file.arrayBuffer()) const Key = `cads/${file.name}` const ContentType = 'image/png' @@ -49,7 +49,7 @@ export async function DELETE(req) { try { const searchParams = req.nextUrl.searchParams const Key = `cads/${searchParams.get('fileName')}` - console.log('🚀 ~ DELETE ~ Key:', Key) + // console.log('🚀 ~ DELETE ~ Key:', Key) if (!Key) { return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) diff --git a/src/app/api/image/map/route.js b/src/app/api/image/map/route.js index 0cc76c02..96df259b 100644 --- a/src/app/api/image/map/route.js +++ b/src/app/api/image/map/route.js @@ -57,7 +57,7 @@ export async function DELETE(req) { try { const searchParams = req.nextUrl.searchParams const Key = `maps/${searchParams.get('fileName')}` - console.log('🚀 ~ DELETE ~ Key:', Key) + // console.log('🚀 ~ DELETE ~ Key:', Key) if (!Key) { return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) diff --git a/src/app/api/image/upload/route.js b/src/app/api/image/upload/route.js index 4d875257..4d802e75 100644 --- a/src/app/api/image/upload/route.js +++ b/src/app/api/image/upload/route.js @@ -55,7 +55,7 @@ export async function DELETE(req) { } const Key = `upload/${fileName}` - console.log('🚀 ~ DELETE ~ Key:', Key) + // console.log('🚀 ~ DELETE ~ Key:', Key) await s3.send( new DeleteObjectCommand({ diff --git a/src/config/config.export.js b/src/config/config.export.js index 620bd65c..142d4026 100644 --- a/src/config/config.export.js +++ b/src/config/config.export.js @@ -5,7 +5,7 @@ import configProduction from './config.production' // 클라이언트에서는 이 함수를 사용하여 config 값을 참조합니다. const Config = () => { - console.log('🚀 ~ Config ~ process.env.NEXT_PUBLIC_RUN_MODE:', process.env.NEXT_PUBLIC_RUN_MODE) + // console.log('🚀 ~ Config ~ process.env.NEXT_PUBLIC_RUN_MODE:', process.env.NEXT_PUBLIC_RUN_MODE) switch (process.env.NEXT_PUBLIC_RUN_MODE) { case 'local': return configLocal diff --git a/src/lib/cadAction.js b/src/lib/cadAction.js index b2feafff..aa15d9dd 100644 --- a/src/lib/cadAction.js +++ b/src/lib/cadAction.js @@ -10,7 +10,7 @@ import fs from 'fs/promises' const imageSavePath = 'public/cadImages' const convertDwgToPng = async (fileName, data) => { - console.log('fileName', fileName) + // console.log('fileName', fileName) try { await fs.readdir(imageSavePath) } catch { From e915a386cc268d16c23435855f62a17a23c24e13 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 15 May 2026 10:03:23 +0900 Subject: [PATCH 16/19] =?UTF-8?q?[1423]=20=E8=A8=AD=E8=A8=88=E4=BE=9D?= =?UTF-8?q?=E9=A0=BC=20import=20=EB=A7=A4=ED=95=91=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EB=A1=A4=EB=B0=B1=20=E2=80=94=20StuffDetail.jsx=207e9ec4e3=20?= =?UTF-8?q?=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영 배포 후 문제 보고로 PLANREQ-MATCH 2026-05-12 재정의 직전 안정 버전으로 되돌림. --- src/components/management/StuffDetail.jsx | 118 ++++++++-------------- 1 file changed, 43 insertions(+), 75 deletions(-) diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index fc0d1b88..0d6f12a8 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -23,7 +23,6 @@ import { QcastContext } from '@/app/QcastProvider' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { useSwal } from '@/hooks/useSwal' import { sanitizeIntegerInputEvent } from '@/util/input-utils' -import { logger } from '@/util/logger' import { CalculatorInput } from '@/components/common/input/CalcInput' import Image from 'next/image' @@ -1046,76 +1045,42 @@ export default function StuffDetail() { form.setValue('installHeight', installHeight) form.setValue('remarks', info.remarks) - // [PLANREQ-MATCH 2026-05-12] 매핑 정책 재정의 — 미매칭 시 list append 폐지, 로그인 초기값 reset 으로 전환 - // - 진입 가드 (2차점 로그인 storeLvl='2'): 1차 hidden·본인 firstAgent 고정 → 1차 derive skip, 2차 매핑만 검증, 실패 시 2차만 본인 store 로 reset - // - T01 / 1차점: 1차 게이트 — 1차 매핑 실패 = 전체 실패. info.saleStoreLevel='2' 케이스는 firstAgent derive + 2차 lookup 둘 다 성공해야 set. 한쪽이라도 실패 시 1,2차 모두 로그인 초기값으로 reset - const resetSaleStoresToLoginDefaults = () => { - if (session?.storeId === 'T01') { - setSelOptions('T01') - form.setValue('saleStoreId', 'T01') - form.setValue('saleStoreLevel', session?.storeLvl) - setOtherSelOptions('') - form.setValue('otherSaleStoreId', '') - form.setValue('otherSaleStoreLevel', '') - } else if (session?.storeLvl === '1') { - setSelOptions(session?.storeId) - form.setValue('saleStoreId', session?.storeId) - form.setValue('saleStoreLevel', session?.storeLvl) - setOtherSelOptions('') - form.setValue('otherSaleStoreId', '') - form.setValue('otherSaleStoreLevel', '') - } - // storeLvl='2' 는 별도 처리 — resetSaleStoresToLoginDefaults() 호출되지 않음 - } - - if (session?.storeId !== 'T01' && session?.storeLvl === '2') { - // 2차점 로그인: 1차 derive skip, 2차 매핑만 검증 - const matched2 = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) - if (matched2) { - setOtherSelOptions(info.saleStoreId) - form.setValue('otherSaleStoreId', info.saleStoreId) - form.setValue('otherSaleStoreName', info.saleStoreName) - form.setValue('otherSaleStoreLevel', info.saleStoreLevel) - } else { - // 2차 매핑 실패 → 2차만 본인 store 로 reset - setOtherSelOptions(session?.storeId) - form.setValue('otherSaleStoreId', session?.storeId) - form.setValue('otherSaleStoreLevel', session?.storeLvl) - } - } else if (info.saleStoreLevel === '1') { - // T01 / 1차점 + planreq 가 1차 ID — 1차 매핑만 시도 - const matched1 = saleStoreList.some((s) => s.saleStoreId === info.saleStoreId) - if (matched1) { - setSelOptions(info.saleStoreId) - form.setValue('saleStoreId', info.saleStoreId) - form.setValue('saleStoreName', info.saleStoreName) - form.setValue('saleStoreLevel', info.saleStoreLevel) - setOtherSelOptions('') - form.setValue('otherSaleStoreId', '') - form.setValue('otherSaleStoreLevel', '') - } else { - resetSaleStoresToLoginDefaults() - } + if (info.saleStoreLevel === '1') { + setSelOptions(info.saleStoreId) + form.setValue('saleStoreId', info.saleStoreId) + form.setValue('saleStoreName', info.saleStoreName) + form.setValue('saleStoreLevel', info.saleStoreLevel) } else { - // T01 / 1차점 + planreq 가 2차 ID — firstAgent derive + 2차 lookup 동시 필요 - const matched2 = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) + setOtherSelOptions(info.saleStoreId) + form.setValue('otherSaleStoreId', info.saleStoreId) + form.setValue('otherSaleStoreName', info.saleStoreName) + form.setValue('otherSaleStoreLevel', info.saleStoreLevel) get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => { - const firstId = res?.firstAgentId - const matchedFirst = firstId && saleStoreList.some((s) => s.saleStoreId === firstId) - if (matched2 && matchedFirst) { - setSelOptions(firstId) - form.setValue('saleStoreId', firstId) + if (res?.firstAgentId) { + const firstAgent = { saleStoreId: res.firstAgentId, saleStoreName: res.firstAgentName } + setSaleStoreList((prev) => { + const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) + return exists ? prev : [...prev, firstAgent] + }) + setShowSaleStoreList((prev) => { + const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) + return exists ? prev : [...prev, firstAgent] + }) + setSelOptions(res.firstAgentId) + form.setValue('saleStoreId', res.firstAgentId) form.setValue('saleStoreName', res.firstAgentName) form.setValue('saleStoreLevel', '1') - setOtherSelOptions(info.saleStoreId) - form.setValue('otherSaleStoreId', info.saleStoreId) - form.setValue('otherSaleStoreName', info.saleStoreName) - form.setValue('otherSaleStoreLevel', info.saleStoreLevel) } else { - resetSaleStoresToLoginDefaults() + setSelOptions('') + form.setValue('saleStoreId', '') + form.setValue('saleStoreName', '') + form.setValue('saleStoreLevel', '') } }).catch(() => { - resetSaleStoresToLoginDefaults() + setSelOptions('') + form.setValue('saleStoreId', '') + form.setValue('saleStoreName', '') + form.setValue('saleStoreLevel', '') }) } } @@ -1306,6 +1271,7 @@ export default function StuffDetail() { const onValid = async (actionType) => { const formData = form.getValues(); if(actionType !== 'save') return false + console.log('Action type:', actionType); // 'save' 또는 'tempSave' let errors = {} let fieldNm @@ -1508,7 +1474,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - logger.error('error::::::', error) + console.log('error::::::', error) }) } else { // 수정모드일때는 PUT @@ -1541,7 +1507,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - logger.error('error::::::', error) + console.log('error::::::', error) }) } } @@ -1632,7 +1598,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - logger.error('error::::::', error) + console.log('error::::::', error) }) } else { setIsGlobalLoading(true) @@ -1656,7 +1622,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - logger.error('error::::::', error) + console.error('error::::::', error) }) } } @@ -1726,7 +1692,7 @@ export default function StuffDetail() { type: 'alert', icon: 'error', }) - logger.error('error::::::', error) + console.log('error::::::', error) }) }, }) @@ -2085,12 +2051,13 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ - // [PLANREQ-MATCH 2026-05-11] T01 또는 1차 user(storeLvl='1') 은 2차 enable, 2차 user(storeLvl='2') 만 disable - session?.storeId === 'T01' || session?.storeLvl === '1' + session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : true + : otherSaleStoreList.length === 1 + ? true + : false } isClearable={true} value={otherSaleStoreList.filter(function (option) { @@ -2683,14 +2650,15 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ - // [PLANREQ-MATCH 2026-05-11] T01 또는 1차 user(storeLvl='1') 은 2차 enable, 2차 user(storeLvl='2') 만 disable managementState?.tempFlg === '0' ? true - : session?.storeId === 'T01' || session?.storeLvl === '1' + : session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : true + : otherSaleStoreList.length === 1 + ? true + : false } isClearable={managementState?.tempFlg === '0' ? false : true} value={otherSaleStoreList.filter(function (option) { From bfd9d446a393e1271a621fecde0d6fbc05d20174 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 15 May 2026 11:09:28 +0900 Subject: [PATCH 17/19] =?UTF-8?q?[1423]=202=EC=B0=A8=20user=20=ED=8C=90?= =?UTF-8?q?=EB=A7=A4=EC=A0=90=20select=20=ED=95=AD=EC=83=81=20enable=20+?= =?UTF-8?q?=20=E8=A8=AD=E8=A8=88=E4=BE=9D=E9=A0=BC=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EB=B3=B8=EC=9D=B8=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/management/StuffDetail.jsx | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index 0d6f12a8..b1328188 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -1051,6 +1051,18 @@ export default function StuffDetail() { form.setValue('saleStoreName', info.saleStoreName) form.setValue('saleStoreLevel', info.saleStoreLevel) } else { + // [PLANREQ-MATCH 2026-05-15] 2차 user: 매핑 실패 시 planReqNo 클리어 + 본인 store 로 초기화 → 2차 select disable 자동 해제 + if (session?.storeLvl === '2') { + const matched = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) + if (!matched) { + form.setValue('planReqNo', '') + setOtherSelOptions(session?.storeId) + form.setValue('otherSaleStoreId', session?.storeId) + form.setValue('otherSaleStoreName', '') + form.setValue('otherSaleStoreLevel', session?.storeLvl) + return + } + } setOtherSelOptions(info.saleStoreId) form.setValue('otherSaleStoreId', info.saleStoreId) form.setValue('otherSaleStoreName', info.saleStoreName) @@ -2051,13 +2063,12 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ + // [PLANREQ-MATCH 2026-05-15] 2차 user 는 하위 store 선택이 가능해야 하므로 항상 enable session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : otherSaleStoreList.length === 1 - ? true - : false + : false } isClearable={true} value={otherSaleStoreList.filter(function (option) { @@ -2650,15 +2661,14 @@ export default function StuffDetail() { getOptionLabel={(x) => x.saleStoreName} getOptionValue={(x) => x.saleStoreId} isDisabled={ + // [PLANREQ-MATCH 2026-05-15] 2차 user 는 하위 store 선택이 가능해야 하므로 항상 enable (tempFlg='0' 잠금만 유지) managementState?.tempFlg === '0' ? true : session?.storeLvl === '1' ? otherSaleStoreList.length > 0 ? false : true - : otherSaleStoreList.length === 1 - ? true - : false + : false } isClearable={managementState?.tempFlg === '0' ? false : true} value={otherSaleStoreList.filter(function (option) { From 905b51b865c06008fe02306072bfc6a5015df6d0 Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 15 May 2026 12:19:15 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[1423]=202=EC=B0=A8=20user=20=ED=8C=90?= =?UTF-8?q?=EB=A7=A4=EC=A0=90=20select=20=ED=95=AD=EC=83=81=20enable=20+?= =?UTF-8?q?=20=E8=A8=AD=E8=A8=88=E4=BE=9D=E9=A0=BC=20=EB=A7=A4=ED=95=91=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EB=B3=B8=EC=9D=B8=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/management/StuffDetail.jsx | 167 ++++++++++++---------- src/locales/ja.json | 1 + src/locales/ko.json | 1 + 3 files changed, 97 insertions(+), 72 deletions(-) diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx index b1328188..68129345 100644 --- a/src/components/management/StuffDetail.jsx +++ b/src/components/management/StuffDetail.jsx @@ -23,6 +23,7 @@ import { QcastContext } from '@/app/QcastProvider' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { useSwal } from '@/hooks/useSwal' import { sanitizeIntegerInputEvent } from '@/util/input-utils' +import { logger } from '@/util/logger' import { CalculatorInput } from '@/components/common/input/CalcInput' import Image from 'next/image' @@ -1008,93 +1009,115 @@ export default function StuffDetail() { //팝업에서 넘어온 설계의뢰 정보로 바꾸기 const setPlanReqInfo = (info) => { - form.setValue('planReqNo', info.planReqNo) - - form.setValue('objectStatusId', info.building) - setSelectObjectStatusId(info.building) - - form.setValue('objectName', info.title) - form.setValue('zipNo', info.zipNo) - form.setValue('address', info.address2) - - prefCodeList.map((row) => { - if (row.prefName == info.address1) { - setPrefValue(row.prefId) - form.setValue('prefId', row.prefId) - form.setValue('prefName', info.address1) - } + // [PLANREQ-DEBUG 2026-05-15] import 흐름 진단 + logger.debug('[PLANREQ-DEBUG] setPlanReqInfo entry', { + session: { storeId: session?.storeId, storeLvl: session?.storeLvl }, + info: { planReqNo: info?.planReqNo, saleStoreId: info?.saleStoreId, saleStoreLevel: info?.saleStoreLevel, saleStoreName: info?.saleStoreName }, + saleStoreList: saleStoreList.map((s) => s.saleStoreId), + otherSaleStoreList: otherSaleStoreList.map((o) => o.saleStoreId), }) - //설계의뢰 팝업에선 WL_안붙어서 옴 - if (info.windSpeed !== '') { - form.setValue('standardWindSpeedId', `WL_${info.windSpeed}`) - } else { - form.setValue('standardWindSpeedId', `WL_${info.windSpeed}`) + // [PLANREQ-MATCH 2026-05-15] all-or-nothing: saleStore 매핑 검증 후에만 모든 필드 적용 + const applyFields = () => { + form.setValue('planReqNo', info.planReqNo) + form.setValue('objectStatusId', info.building) + setSelectObjectStatusId(info.building) + form.setValue('objectName', info.title) + form.setValue('zipNo', info.zipNo) + form.setValue('address', info.address2) + prefCodeList.map((row) => { + if (row.prefName == info.address1) { + setPrefValue(row.prefId) + form.setValue('prefId', row.prefId) + form.setValue('prefName', info.address1) + } + }) + //설계의뢰 팝업에선 WL_안붙어서 옴 + if (info.windSpeed !== '') { + form.setValue('standardWindSpeedId', `WL_${info.windSpeed}`) + } else { + form.setValue('standardWindSpeedId', `WL_${info.windSpeed}`) + } + form.setValue('verticalSnowCover', info.verticalSnowCover) + form.setValue('surfaceType', info.surfaceType) + if (info.surfaceType === 'Ⅱ') { + form.setValue('saltAreaFlg', true) + } else { + form.setValue('saltAreaFlg', false) + } + const installHeight = info.installHeight ? info.installHeight.split('.')[0] : '' + form.setValue('installHeight', installHeight) + form.setValue('remarks', info.remarks) } - form.setValue('verticalSnowCover', info.verticalSnowCover) - form.setValue('surfaceType', info.surfaceType) - - if (info.surfaceType === 'Ⅱ') { - form.setValue('saltAreaFlg', true) - } else { - form.setValue('saltAreaFlg', false) - } - - let installHeight = info.installHeight ? info.installHeight.split('.')[0] : '' - - form.setValue('installHeight', installHeight) - form.setValue('remarks', info.remarks) if (info.saleStoreLevel === '1') { + // 1차 ID: 매핑 실패 사실상 없음 (조회 자체가 권한 필터) → 즉시 적용 + applyFields() setSelOptions(info.saleStoreId) form.setValue('saleStoreId', info.saleStoreId) form.setValue('saleStoreName', info.saleStoreName) form.setValue('saleStoreLevel', info.saleStoreLevel) - } else { - // [PLANREQ-MATCH 2026-05-15] 2차 user: 매핑 실패 시 planReqNo 클리어 + 본인 store 로 초기화 → 2차 select disable 자동 해제 - if (session?.storeLvl === '2') { - const matched = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) - if (!matched) { - form.setValue('planReqNo', '') - setOtherSelOptions(session?.storeId) - form.setValue('otherSaleStoreId', session?.storeId) - form.setValue('otherSaleStoreName', '') - form.setValue('otherSaleStoreLevel', session?.storeLvl) - return - } + return + } + + // info.saleStoreLevel !== '1' (2차 ID) + if (session?.storeLvl === '2') { + const matched = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId) + logger.debug('[PLANREQ-DEBUG] 2차 user match check', { matched, target: info.saleStoreId }) + if (!matched) { + // 매핑 실패 — 아무것도 적용 안 함 + 사용자에게 알림 + swalFire({ + title: getMessage('stuff.detail.planReq.message.notMatch'), + type: 'alert', + icon: 'warning', + }) + return } + applyFields() setOtherSelOptions(info.saleStoreId) form.setValue('otherSaleStoreId', info.saleStoreId) form.setValue('otherSaleStoreName', info.saleStoreName) form.setValue('otherSaleStoreLevel', info.saleStoreLevel) - get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => { - if (res?.firstAgentId) { - const firstAgent = { saleStoreId: res.firstAgentId, saleStoreName: res.firstAgentName } - setSaleStoreList((prev) => { - const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) - return exists ? prev : [...prev, firstAgent] - }) - setShowSaleStoreList((prev) => { - const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) - return exists ? prev : [...prev, firstAgent] - }) - setSelOptions(res.firstAgentId) - form.setValue('saleStoreId', res.firstAgentId) - form.setValue('saleStoreName', res.firstAgentName) - form.setValue('saleStoreLevel', '1') - } else { - setSelOptions('') - form.setValue('saleStoreId', '') - form.setValue('saleStoreName', '') - form.setValue('saleStoreLevel', '') - } - }).catch(() => { - setSelOptions('') - form.setValue('saleStoreId', '') - form.setValue('saleStoreName', '') - form.setValue('saleStoreLevel', '') - }) + return } + + // T01 / 1차 user + 2차 ID: firstAgent 검증 후에만 적용 (실패 시 무반영) + get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => { + logger.debug('[PLANREQ-DEBUG] firstAgent result', { firstAgentId: res?.firstAgentId }) + if (!res?.firstAgentId) { + swalFire({ + title: getMessage('stuff.detail.planReq.message.notMatch'), + type: 'alert', + icon: 'warning', + }) + return + } + applyFields() + setOtherSelOptions(info.saleStoreId) + form.setValue('otherSaleStoreId', info.saleStoreId) + form.setValue('otherSaleStoreName', info.saleStoreName) + form.setValue('otherSaleStoreLevel', info.saleStoreLevel) + const firstAgent = { saleStoreId: res.firstAgentId, saleStoreName: res.firstAgentName } + setSaleStoreList((prev) => { + const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) + return exists ? prev : [...prev, firstAgent] + }) + setShowSaleStoreList((prev) => { + const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) + return exists ? prev : [...prev, firstAgent] + }) + setSelOptions(res.firstAgentId) + form.setValue('saleStoreId', res.firstAgentId) + form.setValue('saleStoreName', res.firstAgentName) + form.setValue('saleStoreLevel', '1') + }).catch(() => { + // 매핑 실패 — 아무것도 적용 안 함 + 사용자에게 알림 + swalFire({ + title: getMessage('stuff.detail.planReq.message.notMatch'), + type: 'alert', + icon: 'warning', + }) + }) } //풍속선택 팝업에서 넘어온 바람정보 diff --git a/src/locales/ja.json b/src/locales/ja.json index ec6d7331..2bc8f273 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -782,6 +782,7 @@ "stuff.detail.tempSave.message2": "担当者名は全角20文字(半角40文字)以下で入力してください.", "stuff.detail.tempSave.message3": "二次販売店を選択してください。", "stuff.detail.confirm.message1": "販売店情報を変更すると、設計依頼文書番号が削除されます。変更しますか?", + "stuff.detail.planReq.message.notMatch": "設計依頼の販売店情報が一致しないため、インポートできません。", "stuff.detail.delete.message1": "仕様が確定したものは削除できません。", "stuff.detail.planList.title": "プランリスト", "stuff.detail.planList.cnt": "全体", diff --git a/src/locales/ko.json b/src/locales/ko.json index 84c95a6b..5d9a5474 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -782,6 +782,7 @@ "stuff.detail.tempSave.message2": "담당자이름은 전각20자(반각40자) 이하로 입력해 주십시오.", "stuff.detail.tempSave.message3": "2차 판매점을 선택해주세요.", "stuff.detail.confirm.message1": "판매점 정보를 변경하면 설계의뢰 문서번호가 삭제됩니다. 변경하시겠습니까?", + "stuff.detail.planReq.message.notMatch": "설계의뢰의 판매점 정보가 일치하지 않아 가져올 수 없습니다.", "stuff.detail.delete.message1": "사양이 확정된 물건은 삭제할 수 없습니다.", "stuff.detail.planList.title": "플랜목록", "stuff.detail.planList.cnt": "전체", From 116953ee2f3760382a286062ea6d1dff10e52b1f Mon Sep 17 00:00:00 2001 From: ysCha Date: Fri, 15 May 2026 12:28:32 +0900 Subject: [PATCH 19/19] =?UTF-8?q?=EC=86=8C=EC=8A=A4=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/skeleton-utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index e46053c4..2329cc92 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -1994,11 +1994,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver let movedLines = [] // 조건에 맞는 라인들만 필터링 - const validWallLines = [...wallLines].sort((a, b) => a.idx - b.idx).filter((wallLine, index) => wallLine.idx - 1 === index) + const validWallLines = [...wallLines].sort((a, b) => a.idx - b.idx).filter((wallLine, index) => wallLine.idx - 1 === index); // [ASI-FIX 2026-05-15] 세미콜론 없으면 다음 줄 `(` 가 함수호출로 묶여 .filter(...)(...) → "is not a function" → SK 빌드 실패 + 확장선 누락 // logger.log('', sortRoofLines, sortWallLines, sortWallBaseLines); - (sortWallLines.length === sortWallBaseLines.length && sortWallBaseLines.length > 3) && + ;(sortWallLines.length === sortWallBaseLines.length && sortWallBaseLines.length > 3) && sortWallLines.forEach((wallLine, index) => { const roofLine = sortRoofLines[index]