Merge pull request 'dev' (#855) from dev into prd-deploy

Reviewed-on: #855
This commit is contained in:
ysCha 2026-05-14 17:46:14 +09:00
commit b58af58b79
7 changed files with 238 additions and 206 deletions

View File

@ -11,7 +11,7 @@ const s3 = new S3Client({
}) })
const uploadImage = async (file) => { const uploadImage = async (file) => {
console.log('🚀 ~ uploadImage ~ file:', file) // console.log('🚀 ~ uploadImage ~ file:', file)
const Body = Buffer.from(await file.arrayBuffer()) const Body = Buffer.from(await file.arrayBuffer())
const Key = `cads/${file.name}` const Key = `cads/${file.name}`
const ContentType = 'image/png' const ContentType = 'image/png'
@ -49,7 +49,7 @@ export async function DELETE(req) {
try { try {
const searchParams = req.nextUrl.searchParams const searchParams = req.nextUrl.searchParams
const Key = `cads/${searchParams.get('fileName')}` const Key = `cads/${searchParams.get('fileName')}`
console.log('🚀 ~ DELETE ~ Key:', Key) // console.log('🚀 ~ DELETE ~ Key:', Key)
if (!Key) { if (!Key) {
return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 })

View File

@ -57,7 +57,7 @@ export async function DELETE(req) {
try { try {
const searchParams = req.nextUrl.searchParams const searchParams = req.nextUrl.searchParams
const Key = `maps/${searchParams.get('fileName')}` const Key = `maps/${searchParams.get('fileName')}`
console.log('🚀 ~ DELETE ~ Key:', Key) // console.log('🚀 ~ DELETE ~ Key:', Key)
if (!Key) { if (!Key) {
return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 }) return NextResponse.json({ error: 'fileName parameter is required' }, { status: 400 })

View File

@ -55,7 +55,7 @@ export async function DELETE(req) {
} }
const Key = `upload/${fileName}` const Key = `upload/${fileName}`
console.log('🚀 ~ DELETE ~ Key:', Key) // console.log('🚀 ~ DELETE ~ Key:', Key)
await s3.send( await s3.send(
new DeleteObjectCommand({ new DeleteObjectCommand({

View File

@ -23,6 +23,7 @@ import { QcastContext } from '@/app/QcastProvider'
import { useCanvasMenu } from '@/hooks/common/useCanvasMenu' import { useCanvasMenu } from '@/hooks/common/useCanvasMenu'
import { useSwal } from '@/hooks/useSwal' import { useSwal } from '@/hooks/useSwal'
import { sanitizeIntegerInputEvent } from '@/util/input-utils' import { sanitizeIntegerInputEvent } from '@/util/input-utils'
import { logger } from '@/util/logger'
import { CalculatorInput } from '@/components/common/input/CalcInput' import { CalculatorInput } from '@/components/common/input/CalcInput'
import Image from 'next/image' import Image from 'next/image'
@ -1045,42 +1046,76 @@ export default function StuffDetail() {
form.setValue('installHeight', installHeight) form.setValue('installHeight', installHeight)
form.setValue('remarks', info.remarks) form.setValue('remarks', info.remarks)
if (info.saleStoreLevel === '1') { // [PLANREQ-MATCH 2026-05-12] list append , reset
setSelOptions(info.saleStoreId) // - (2 storeLvl='2'): 1 hidden· firstAgent 1 derive skip, 2 , 2 store reset
form.setValue('saleStoreId', info.saleStoreId) // - T01 / 1: 1 1 = . info.saleStoreLevel='2' firstAgent derive + 2 lookup set. 1,2 reset
form.setValue('saleStoreName', info.saleStoreName) const resetSaleStoresToLoginDefaults = () => {
form.setValue('saleStoreLevel', info.saleStoreLevel) 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 { } else {
setOtherSelOptions(info.saleStoreId) // T01 / 1 + planreq 2 ID firstAgent derive + 2 lookup
form.setValue('otherSaleStoreId', info.saleStoreId) const matched2 = otherSaleStoreList.some((o) => o.saleStoreId === info.saleStoreId)
form.setValue('otherSaleStoreName', info.saleStoreName)
form.setValue('otherSaleStoreLevel', info.saleStoreLevel)
get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => { get({ url: `/api/object/saleStore/${info.saleStoreId}/firstAgent` }).then((res) => {
if (res?.firstAgentId) { const firstId = res?.firstAgentId
const firstAgent = { saleStoreId: res.firstAgentId, saleStoreName: res.firstAgentName } const matchedFirst = firstId && saleStoreList.some((s) => s.saleStoreId === firstId)
setSaleStoreList((prev) => { if (matched2 && matchedFirst) {
const exists = prev.some((s) => s.saleStoreId === res.firstAgentId) setSelOptions(firstId)
return exists ? prev : [...prev, firstAgent] form.setValue('saleStoreId', firstId)
})
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('saleStoreName', res.firstAgentName)
form.setValue('saleStoreLevel', '1') form.setValue('saleStoreLevel', '1')
setOtherSelOptions(info.saleStoreId)
form.setValue('otherSaleStoreId', info.saleStoreId)
form.setValue('otherSaleStoreName', info.saleStoreName)
form.setValue('otherSaleStoreLevel', info.saleStoreLevel)
} else { } else {
setSelOptions('') resetSaleStoresToLoginDefaults()
form.setValue('saleStoreId', '')
form.setValue('saleStoreName', '')
form.setValue('saleStoreLevel', '')
} }
}).catch(() => { }).catch(() => {
setSelOptions('') resetSaleStoresToLoginDefaults()
form.setValue('saleStoreId', '')
form.setValue('saleStoreName', '')
form.setValue('saleStoreLevel', '')
}) })
} }
} }
@ -1271,7 +1306,6 @@ export default function StuffDetail() {
const onValid = async (actionType) => { const onValid = async (actionType) => {
const formData = form.getValues(); const formData = form.getValues();
if(actionType !== 'save') return false if(actionType !== 'save') return false
console.log('Action type:', actionType); // 'save' 'tempSave'
let errors = {} let errors = {}
let fieldNm let fieldNm
@ -1474,7 +1508,7 @@ export default function StuffDetail() {
type: 'alert', type: 'alert',
icon: 'error', icon: 'error',
}) })
console.log('error::::::', error) logger.error('error::::::', error)
}) })
} else { } else {
// PUT // PUT
@ -1507,7 +1541,7 @@ export default function StuffDetail() {
type: 'alert', type: 'alert',
icon: 'error', icon: 'error',
}) })
console.log('error::::::', error) logger.error('error::::::', error)
}) })
} }
} }
@ -1598,7 +1632,7 @@ export default function StuffDetail() {
type: 'alert', type: 'alert',
icon: 'error', icon: 'error',
}) })
console.log('error::::::', error) logger.error('error::::::', error)
}) })
} else { } else {
setIsGlobalLoading(true) setIsGlobalLoading(true)
@ -1622,7 +1656,7 @@ export default function StuffDetail() {
type: 'alert', type: 'alert',
icon: 'error', icon: 'error',
}) })
console.error('error::::::', error) logger.error('error::::::', error)
}) })
} }
} }
@ -1692,7 +1726,7 @@ export default function StuffDetail() {
type: 'alert', type: 'alert',
icon: 'error', icon: 'error',
}) })
console.log('error::::::', error) logger.error('error::::::', error)
}) })
}, },
}) })
@ -2051,13 +2085,12 @@ export default function StuffDetail() {
getOptionLabel={(x) => x.saleStoreName} getOptionLabel={(x) => x.saleStoreName}
getOptionValue={(x) => x.saleStoreId} getOptionValue={(x) => x.saleStoreId}
isDisabled={ 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 ? otherSaleStoreList.length > 0
? false ? false
: true : true
: otherSaleStoreList.length === 1 : true
? true
: false
} }
isClearable={true} isClearable={true}
value={otherSaleStoreList.filter(function (option) { value={otherSaleStoreList.filter(function (option) {
@ -2650,15 +2683,14 @@ export default function StuffDetail() {
getOptionLabel={(x) => x.saleStoreName} getOptionLabel={(x) => x.saleStoreName}
getOptionValue={(x) => x.saleStoreId} getOptionValue={(x) => x.saleStoreId}
isDisabled={ isDisabled={
// [PLANREQ-MATCH 2026-05-11] T01 1 user(storeLvl='1') 2 enable, 2 user(storeLvl='2') disable
managementState?.tempFlg === '0' managementState?.tempFlg === '0'
? true ? true
: session?.storeLvl === '1' : session?.storeId === 'T01' || session?.storeLvl === '1'
? otherSaleStoreList.length > 0 ? otherSaleStoreList.length > 0
? false ? false
: true : true
: otherSaleStoreList.length === 1 : true
? true
: false
} }
isClearable={managementState?.tempFlg === '0' ? false : true} isClearable={managementState?.tempFlg === '0' ? false : true}
value={otherSaleStoreList.filter(function (option) { value={otherSaleStoreList.filter(function (option) {

View File

@ -5,7 +5,7 @@ import configProduction from './config.production'
// 클라이언트에서는 이 함수를 사용하여 config 값을 참조합니다. // 클라이언트에서는 이 함수를 사용하여 config 값을 참조합니다.
const 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) { switch (process.env.NEXT_PUBLIC_RUN_MODE) {
case 'local': case 'local':
return configLocal return configLocal

View File

@ -10,7 +10,7 @@ import fs from 'fs/promises'
const imageSavePath = 'public/cadImages' const imageSavePath = 'public/cadImages'
const convertDwgToPng = async (fileName, data) => { const convertDwgToPng = async (fileName, data) => {
console.log('fileName', fileName) // console.log('fileName', fileName)
try { try {
await fs.readdir(imageSavePath) await fs.readdir(imageSavePath)
} catch { } catch {

View File

@ -96,7 +96,7 @@ const movingLineFromSkeleton = (roofId, canvas) => {
const _lp = canvas?.skeleton?.lastPoints const _lp = canvas?.skeleton?.lastPoints
const _src = _lp ? 'lastPoints' : 'orgRoofPoints' const _src = _lp ? 'lastPoints' : 'orgRoofPoints'
const _p7 = oldPoints?.[7] 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) {} } catch (_e) {}
const oppositeLine = findOppositeLine(canvas.skeleton.Edges, startPoint, endPoint, oldPoints); 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) const skeletonLines = canvas.getObjects().filter((object) => object.skeletonType === 'line' && object.parentId === roofId)
if (oppositeLine) { if (oppositeLine) {
logger.log('Opposite line found:', oppositeLine); // logger.log('Opposite line found:', oppositeLine);
} else { } else {
logger.log('No opposite line found'); // logger.log('No opposite line found');
} }
if(moveFlowLine !== 0) { if(moveFlowLine !== 0) {
return oldPoints.map((point, index) => { return oldPoints.map((point, index) => {
logger.log('Point:', point); // logger.log('Point:', point);
const newPoint = { ...point }; const newPoint = { ...point };
const absMove = Big(moveFlowLine).times(2).div(10); const absMove = Big(moveFlowLine).times(2).div(10);
logger.log('skeletonBuilder moveDirection:', moveDirection); // logger.log('skeletonBuilder moveDirection:', moveDirection);
switch (moveDirection) { switch (moveDirection) {
case 'left': case 'left':
@ -180,7 +180,7 @@ const movingLineFromSkeleton = (roofId, canvas) => {
if (line.position === 'bottom') { if (line.position === 'bottom') {
logger.log('oldPoint:', point); // logger.log('oldPoint:', point);
if (isSamePoint(newPoint, line.start)) { if (isSamePoint(newPoint, line.start)) {
newPoint.y = Big(line.start.y).minus(absMove).toNumber(); 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 변경 //baseline 변경
return newPoint; return newPoint;
}) })
@ -256,10 +256,10 @@ const movingLineFromSkeleton = (roofId, canvas) => {
affected.add(k); affected.add(k);
affected.add((k + 1) % nPts); affected.add((k + 1) % nPts);
}); });
logger.log('absMove::', moveUpDownLength); // logger.log('absMove::', moveUpDownLength);
// [BR-TRACE local] movingLineFromSkeleton position/direction 분기 // [BR-TRACE local] movingLineFromSkeleton position/direction 분기
const __isLocalMS = process.env.NEXT_PUBLIC_RUN_MODE === 'local' 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) => { affected.forEach((i) => {
const point = newPoints[i]; const point = newPoints[i];
if (!point) return; if (!point) return;
@ -267,19 +267,19 @@ const movingLineFromSkeleton = (roofId, canvas) => {
if (position === 'bottom') { if (position === 'bottom') {
if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber(); if (moveDirection === 'in') point.y = Big(point.y).minus(moveUpDownLength).toNumber();
else if (moveDirection === 'out') point.y = Big(point.y).plus(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') { } else if (position === 'top') {
if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber(); if (moveDirection === 'in') point.y = Big(point.y).plus(moveUpDownLength).toNumber();
else if (moveDirection === 'out') point.y = Big(point.y).minus(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') { } else if (position === 'left') {
if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber(); if (moveDirection === 'in') point.x = Big(point.x).plus(moveUpDownLength).toNumber();
else if (moveDirection === 'out') point.x = Big(point.x).minus(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') { } else if (position === 'right') {
if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber(); if (moveDirection === 'in') point.x = Big(point.x).minus(moveUpDownLength).toNumber();
else if (moveDirection === 'out') point.x = Big(point.x).plus(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) { } 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] 현재 값 // [LP-TRACE] buildRawMovedPoints 진입 — prevLast 유무 및 [7] 현재 값
try { try {
const _pl7 = prevLast?.[7] 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) {} } catch (_e) {}
const selectLine = roof.moveSelectLine const selectLine = roof.moveSelectLine
@ -447,7 +447,7 @@ const buildRawMovedPoints = (roofId, canvas) => {
}) })
// [BR-TRACE local] buildRawMovedPoints position/direction 분기 // [BR-TRACE local] buildRawMovedPoints position/direction 분기
const __isLocalBR = process.env.NEXT_PUBLIC_RUN_MODE === 'local' 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) => { affected.forEach((i) => {
const point = newPoints[i] const point = newPoints[i]
if (!point) return if (!point) return
@ -455,19 +455,19 @@ const buildRawMovedPoints = (roofId, canvas) => {
if (position === 'bottom') { if (position === 'bottom') {
if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber() if (moveDirection === 'out') point.y = Big(point.y).plus(moveUpDownLength).toNumber()
else if (moveDirection === 'in') point.y = Big(point.y).minus(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') { } else if (position === 'top') {
if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber() if (moveDirection === 'out') point.y = Big(point.y).minus(moveUpDownLength).toNumber()
else if (moveDirection === 'in') point.y = Big(point.y).plus(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') { } else if (position === 'left') {
if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber() if (moveDirection === 'out') point.x = Big(point.x).minus(moveUpDownLength).toNumber()
else if (moveDirection === 'in') point.x = Big(point.x).plus(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') { } else if (position === 'right') {
if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber() if (moveDirection === 'out') point.x = Big(point.x).plus(moveUpDownLength).toNumber()
else if (moveDirection === 'in') point.x = Big(point.x).minus(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) { } else if (__isLocalBR) {
logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`) logger.warn(`[BR-TRACE] i=${i} position=${position} 매칭 분기 없음 (이동 안 됨)`)
} }
@ -478,7 +478,7 @@ const buildRawMovedPoints = (roofId, canvas) => {
const _sel = selectLine const _sel = selectLine
const _sp = _sel?.startPoint const _sp = _sel?.startPoint
const _ep = _sel?.endPoint 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) {} } catch (_e) {}
return newPoints return newPoints
@ -512,7 +512,7 @@ export const verifyMoveBoundary = (roofId, canvas) => {
try { try {
const roof = canvas?.getObjects().find((o) => o.id === roofId) const roof = canvas?.getObjects().find((o) => o.id === roofId)
if (!roof || !Array.isArray(roof.points)) { if (!roof || !Array.isArray(roof.points)) {
logger.log('[verifyMoveBoundary] roof/points 없음 → unknown') // logger.log('[verifyMoveBoundary] roof/points 없음 → unknown')
return 'unknown' return 'unknown'
} }
@ -520,20 +520,20 @@ export const verifyMoveBoundary = (roofId, canvas) => {
const moveUpDown = roof.moveUpDown ?? 0 const moveUpDown = roof.moveUpDown ?? 0
const position = roof.movePosition const position = roof.movePosition
const direction = roof.moveDirect const direction = roof.moveDirect
logger.log( // logger.log(
`[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}` // `[verifyMoveBoundary] 진입 position=${position} direction=${direction} moveUpDown=${moveUpDown} moveFlowLine=${moveFlowLine}`
) // )
if (moveFlowLine === 0 && moveUpDown === 0) { if (moveFlowLine === 0 && moveUpDown === 0) {
logger.log('[verifyMoveBoundary] 이동 없음 → ok') // logger.log('[verifyMoveBoundary] 이동 없음 → ok')
return 'ok' return 'ok'
} }
const proposed = buildRawMovedPoints(roofId, canvas) const proposed = buildRawMovedPoints(roofId, canvas)
const orig = roof.points const orig = roof.points
if (!Array.isArray(proposed) || proposed.length !== orig.length) { if (!Array.isArray(proposed) || proposed.length !== orig.length) {
logger.log( // logger.log(
`[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown` // `[verifyMoveBoundary] proposed 비정상 (len=${proposed?.length}, orig.len=${orig.length}) → unknown`
) // )
return 'unknown' return 'unknown'
} }
@ -556,9 +556,9 @@ export const verifyMoveBoundary = (roofId, canvas) => {
const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next]) const crossOrig = getTurnDirection(orig[prev], orig[i], orig[next])
const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next]) const crossNew = getTurnDirection(proposed[prev], proposed[i], proposed[next])
logger.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)}` // `[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) 였던 꼭짓점만 경계 판정 대상 // 원래 골짜기(>0) 였던 꼭짓점만 경계 판정 대상
if (crossOrig > 0) { if (crossOrig > 0) {
@ -572,18 +572,18 @@ export const verifyMoveBoundary = (roofId, canvas) => {
return 'crossed' return 'crossed'
} }
if (Math.abs(crossNew) <= boundaryTol) { if (Math.abs(crossNew) <= boundaryTol) {
logger.log( // logger.log(
`[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})` // `[verifyMoveBoundary] 꼭짓점[${i}] 경계 도달 (on-boundary): cross ≈ 0 (${crossNew.toFixed(1)})`
) // )
if (worst === 'ok') worst = 'on-boundary' if (worst === 'ok') worst = 'on-boundary'
} }
} }
} }
if (movedIdx.length === 0) { 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 return worst
} catch (e) { } catch (e) {
logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e) logger.warn('[verifyMoveBoundary] 판정 실패 → unknown:', e)
@ -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 if (!il || !isFinite(il.x1) || !isFinite(il.y1) || !isFinite(il.x2) || !isFinite(il.y2)) return false
return il.lineName === 'hip' || il.name === 'hip' 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 등). // 절삭 대상: roofLine seg + canvas 의 다른 보조라인 (eaveHelpLine 등).
// 기존 helper 자체(BASELINE_TO_ROOFLINE_HELPER_TYPE) / wall/roof 본체 / 텍스트 등은 제외. // 기존 helper 자체(BASELINE_TO_ROOFLINE_HELPER_TYPE) / wall/roof 본체 / 텍스트 등은 제외.
@ -765,7 +765,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
const s = segs[k] 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))}`) 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 만 대상. // [v1 ext 정정] ext 는 wallbaseLine 의 꼭지점(corner) 에 outer 끝점이 일치하는 hip 만 대상.
@ -784,16 +784,16 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
// outer 끝이 wallbaseLine corner 에 일치하지 않으면 ext 대상 아님. // outer 끝이 wallbaseLine corner 에 일치하지 않으면 ext 대상 아님.
if (outerCornerDist > CORNER_EPS) { if (outerCornerDist > CORNER_EPS) {
logger.log( // logger.log(
`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + // `[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` +
`corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip` // `corner 거리=${outerCornerDist.toFixed(2)} > ${CORNER_EPS} → 내부 hip skip`
) // )
continue continue
} }
// 이미 처마(roofLine) 위에 있으면 ext 불필요. // 이미 처마(roofLine) 위에 있으면 ext 불필요.
if (isPointOnRoofLine(outerEnd, 1.0)) { 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 continue
} }
@ -815,7 +815,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
// [BR-RAY] local only — finite t 인 모든 후보 hit 찍기 (어떤 seg 가 가장 빠른지 비교용) // [BR-RAY] local only — finite t 인 모든 후보 hit 찍기 (어떤 seg 가 가장 빠른지 비교용)
if (__isLocalRayLog && isFinite(t)) { if (__isLocalRayLog && isFinite(t)) {
const __label = (k < m) ? `roofLine#${k}` : (segs[k].__name || `aux#${k - m}`) 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) { if (t < bestT) {
bestT = t bestT = t
@ -824,7 +824,7 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
} }
} }
if (!isFinite(bestT) || bestT < 0.5) { 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 continue
} }
@ -833,18 +833,18 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
// eaveHelpLine 이 ray 경로를 가로막으면 이미 eave/aux 영역에 도달한 것이므로 더 그릴 필요 없음. // eaveHelpLine 이 ray 경로를 가로막으면 이미 eave/aux 영역에 도달한 것이므로 더 그릴 필요 없음.
// roofLine 승자만 진짜 "처마 도달" 로 인정 → ext 그림. // roofLine 승자만 진짜 "처마 도달" 로 인정 → ext 그림.
if (bestSegName === 'eaveHelpLine') { 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 continue
} }
const hitX = outerEnd.x + ux * bestT const hitX = outerEnd.x + ux * bestT
const hitY = outerEnd.y + uy * bestT const hitY = outerEnd.y + uy * bestT
logger.log( // logger.log(
`[v1 ext] hip outer=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)}) ` + // `[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} ` + // `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)})` // `→ ext=(${outerEnd.x.toFixed(1)},${outerEnd.y.toFixed(1)})→(${hitX.toFixed(1)},${hitY.toFixed(1)})`
) // )
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// [hip 좌표 연장 2026-04-30] // [hip 좌표 연장 2026-04-30]
@ -884,10 +884,10 @@ const drawBaselineToRooflineHelpers = (roof, canvas, baseLinePoints, roofLinePoi
if (typeof hip.setLength === 'function') hip.setLength() if (typeof hip.setLength === 'function') hip.setLength()
if (typeof hip.addLengthText === 'function') hip.addLengthText() if (typeof hip.addLengthText === 'function') hip.addLengthText()
logger.log( // logger.log(
`[v1 ext] hip 연장 oldLen=${oldLen.toFixed(1)} +extLen=${extLen.toFixed(1)} ratio=${ratio.toFixed(3)} ` + // `[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})` // `(plane ${oldPlane}→${hip.attributes.planeSize}, actual ${oldActual}→${hip.attributes.actualSize})`
) // )
// ───────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────
// [확장선 스타일] local: 빨강 점선(디버그 가시화), dev/prd: innerLine 동일색·실선(연결된 것처럼). // [확장선 스타일] local: 빨강 점선(디버그 가시화), dev/prd: innerLine 동일색·실선(연결된 것처럼).
@ -983,10 +983,10 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
const moveUpDown = roof.moveUpDown || 0 const moveUpDown = roof.moveUpDown || 0
// 디버그: offset 변경 + moveLine 상태 확인 // 디버그: offset 변경 + moveLine 상태 확인
logger.log('[DEBUG] moveFlowLine:', moveFlowLine, 'moveUpDown:', moveUpDown) // 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.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] 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] roof.points:', roof.points.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
// 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거 // 닫힌 폴리곤(첫점 = 끝점)인 경우 마지막 중복 점 제거
const isClosedPolygon = (points) => const isClosedPolygon = (points) =>
@ -1128,8 +1128,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
if (SK_INPUT_USE_WALL_BASELINE_DIRECT) { if (SK_INPUT_USE_WALL_BASELINE_DIRECT) {
changRoofLinePoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y })) changRoofLinePoints = orderedBaseLinePoints.map((p) => ({ x: p.x, y: p.y }))
roofLineContactPoints = 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 무시):', // logger.log('[v1 SK_INPUT] wall.baseLines 직접 사용 (45° 확장/movedPoints 무시):',
changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) // changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' '))
// [v1 평행오버 corner 흡수 2026-04-29] // [v1 평행오버 corner 흡수 2026-04-29]
// 평행오버 시 OVER_GUARD 가 인접 baseLine 길이를 OVER_EPS(0.5) 만큼만 남김. // 평행오버 시 OVER_GUARD 가 인접 baseLine 길이를 OVER_EPS(0.5) 만큼만 남김.
@ -1162,15 +1162,15 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
kept = next kept = next
} }
if (absorbedAll.length > 0 && kept.length >= 3) { if (absorbedAll.length > 0 && kept.length >= 3) {
logger.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`) // logger.log(`[v1 SK_INPUT 평행흡수] ${absorbedAll.length}개 corner 제거:\n ${absorbedAll.join('\n ')}`)
logger.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`, // logger.log(`[v1 SK_INPUT 평행흡수] 최종 ${kept.length}개 corner:`,
kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' ')) // kept.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`).join(' '))
changRoofLinePoints = kept changRoofLinePoints = kept
roofLineContactPoints = kept.map((p) => ({ x: p.x, y: p.y })) 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) => const invalidPoints = changRoofLinePoints.filter((p, i) =>
@ -1206,14 +1206,14 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
let isOverDetected = false let isOverDetected = false
const origPoints = roof.points || [] const origPoints = roof.points || []
const n = changRoofLinePoints.length 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] 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:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
logger.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => { // logger.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => {
const o = origPoints[i] // const o = origPoints[i]
if (!o) return null // if (!o) return null
const dist = Math.hypot(p.x - o.x, p.y - o.y) // 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 // return dist > 1 ? `[${i}] dx=${Math.round(p.x - o.x)} dy=${Math.round(p.y - o.y)}` : null
}).filter(Boolean)) // }).filter(Boolean))
if (origPoints.length === n && n >= 3) { if (origPoints.length === n && n >= 3) {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const prevOrig = origPoints[(i - 1 + n) % n] const prevOrig = origPoints[(i - 1 + n) % n]
@ -1257,7 +1257,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
) )
if (corrected && corrected.length >= 3) { if (corrected && corrected.length >= 3) {
skeletonInputPoints = corrected 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) { } catch (e) {
logger.error('[SK_OVER] 보정 실패 → fallback:', e) logger.error('[SK_OVER] 보정 실패 → fallback:', e)
@ -1270,8 +1270,8 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
try { try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop() geoJSONPolygon.pop()
logger.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) // logger.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
logger.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length) // logger.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length)
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성 // 스켈레톤 데이터를 기반으로 내부선 생성
@ -1324,9 +1324,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
// [LP-TRACE] 실제 저장되는 lastPoints[7] — 이 값이 다음 이동의 prevLast[7] // [LP-TRACE] 실제 저장되는 lastPoints[7] — 이 값이 다음 이동의 prevLast[7]
try { try {
const _p7 = rawMovedFull[7] 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) {} } catch (_e) {}
logger.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점') // logger.log('[skeleton] lastPoints 저장: raw 풀 길이', rawMovedFull.length, '점')
} else { } else {
canvas.skeleton.lastPoints = roofLineContactPoints canvas.skeleton.lastPoints = roofLineContactPoints
} }
@ -1378,7 +1378,7 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) =>
const bl = wall.baseLines[i] const bl = wall.baseLines[i]
const planeSize = bl?.attributes?.planeSize ?? Infinity const planeSize = bl?.attributes?.planeSize ?? Infinity
if (planeSize < 1) { 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 continue
} }
if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) { if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) {
@ -1393,7 +1393,7 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) =>
return 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 마킹 (오버 좌표 그대로) // skeletonPoints 마킹 (오버 좌표 그대로)
roof.skeletonPoints = rawPoints.map((p) => ({ x: p.x, y: p.y })) roof.skeletonPoints = rawPoints.map((p) => ({ x: p.x, y: p.y }))
@ -1403,8 +1403,8 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) =>
try { try {
geoJSONPolygon.pop() geoJSONPolygon.pop()
logger.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) // logger.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
logger.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length) // logger.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length)
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
roof.innerLines = roof.innerLines || [] roof.innerLines = roof.innerLines || []
@ -1577,7 +1577,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
if (rotIdx > 0) { if (rotIdx > 0) {
wallLines = [...wallLines.slice(rotIdx), ...wallLines.slice(0, rotIdx)] wallLines = [...wallLines.slice(rotIdx), ...wallLines.slice(0, rotIdx)]
if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { 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,11 +1723,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
// [PAIR-DIAG] local only — top in 시 페어 인덱스 어긋남 진단 (2026-04-30) // [PAIR-DIAG] local only — top in 시 페어 인덱스 어긋남 진단 (2026-04-30)
// 1) 길이 미스매치 / 2) _keyOfEdge 충돌 / 3) raw 단계 1:1 무너짐 가려내기 위함 // 1) 길이 미스매치 / 2) _keyOfEdge 충돌 / 3) raw 단계 1:1 무너짐 가려내기 위함
if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') {
logger.log('[PAIR-DIAG] lengths', { // logger.log('[PAIR-DIAG] lengths', {
roofLines: roofLines.length, // roofLines: roofLines.length,
wallLines: wallLines.length, // wallLines: wallLines.length,
wallBaseLines: wall.baseLines.length, // wallBaseLines: wall.baseLines.length,
}) // })
const _seen = new Map() const _seen = new Map()
roofLines.forEach((l, i) => { roofLines.forEach((l, i) => {
const k = _keyOfEdge(l) const k = _keyOfEdge(l)
@ -1737,10 +1737,10 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
roofLines.forEach((rl, i) => { roofLines.forEach((rl, i) => {
const wl = wallLines[i] const wl = wallLines[i]
const wb = wall.baseLines[i] const wb = wall.baseLines[i]
logger.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)})`, // `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=∅', // 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=∅') // 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 l = `${lineAxis}${k}` // y1 or x1
const otherL = `${lineAxis}${otherK}` // y2 or x2 const otherL = `${lineAxis}${otherK}` // y2 or x2
logger.log(`${condition}::::isStartEnd:::::`) // logger.log(`${condition}::::isStartEnd:::::`)
// [MOVE-TRACE] index 0 전용 (필요 시 주석 해제) // [MOVE-TRACE] index 0 전용 (필요 시 주석 해제)
// if (index === 0) { // if (index === 0) {
@ -1858,12 +1858,12 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
if (prevAbsorbed || nextAbsorbed) { 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 return
} }
} }
logger.log(`${condition}::::isStartEnd (both):::::`) // logger.log(`${condition}::::isStartEnd (both):::::`)
// 원본 기준: moveDistY = abs(roofLine.y - wallBaseLine.y), moveDistX = abs(roofLine.x - wallBaseLine.x) // 원본 기준: moveDistY = abs(roofLine.y - wallBaseLine.y), moveDistX = abs(roofLine.x - wallBaseLine.x)
const moveDistY1 = Math.abs(Big(roofLine.y1).minus(wallBaseLine.y1).toNumber()) 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 createHipLine = (fromPoint, toPoint) => {
const createdLine = getAddLine(fromPoint, toPoint, 'orange', 'extensionLine') 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.set({ name: LINE_TYPE.SUBLINE.HIP, lineName: LINE_TYPE.SUBLINE.HIP, stroke: '#FF0000', strokeWidth: 2 })
createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP } createdLine.attributes = { ...createdLine.attributes, type: LINE_TYPE.SUBLINE.HIP }
innerLines.push(createdLine) innerLines.push(createdLine)
@ -1915,7 +1915,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
target = { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 } target = { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 }
} }
if (!__isNearSkVertex(target)) { 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 { } else {
createHipLine(sPoint, target) createHipLine(sPoint, target)
} }
@ -1931,7 +1931,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
target = { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 } target = { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 }
} }
if (!__isNearSkVertex(target)) { 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 { } else {
createHipLine(ePoint, target) 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은 움직인라인 //wall.baseLine은 움직인라인
let movedLines = [] 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) 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.length === sortWallBaseLines.length && sortWallBaseLines.length > 3) &&
sortWallLines.forEach((wallLine, index) => { 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: useMovementSetting OVER_EPS=0.5 (canvas unit) 시 잔여 길이=0.5 → planeSize=5 (calcLinePlaneSize × 10).
// 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음. // 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음.
if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 10) { 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 return
} }
@ -2022,7 +2022,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
const wallIdWl = wallLine.attributes?.wallId ?? wallLine.attributes?.idx ?? '?' const wallIdWl = wallLine.attributes?.wallId ?? wallLine.attributes?.idx ?? '?'
const wallIdWb = wallBaseLine.attributes?.wallId ?? wallBaseLine.attributes?.idx ?? '?' const wallIdWb = wallBaseLine.attributes?.wallId ?? wallBaseLine.attributes?.idx ?? '?'
const dirMatch = wlDir === wbDir 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 외곽선 설정 //roofline 외곽선 설정
@ -2066,7 +2066,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
} }
for (const [p1, p2] of __cgPairs) { for (const [p1, p2] of __cgPairs) {
if (Math.hypot(p2.x - p1.x, p2.y - p1.y) < 0.5) continue 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], { const __cgLine = new QLine([p1.x, p1.y, p2.x, p2.y], {
parentId: roof.id, parentId: roof.id,
fontSize: roof.fontSize, fontSize: roof.fontSize,
@ -2112,15 +2112,15 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
movedLines.push({ index, p1, p2 }) movedLines.push({ index, p1, p2 })
// [진단] eaveHelpLine 생성 추적: 어느 index/어느 wallBaseLine-wallLine 쌍에서 만들어지는지 // [진단] eaveHelpLine 생성 추적: 어느 index/어느 wallBaseLine-wallLine 쌍에서 만들어지는지
logger.log('🎯 [getAddLine]', { // logger.log('🎯 [getAddLine]', {
index, // index,
lineType, // lineType,
p1: { x: Math.round(p1.x), y: Math.round(p1.y) }, // p1: { x: Math.round(p1.x), y: Math.round(p1.y) },
p2: { x: Math.round(p2.x), y: Math.round(p2.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)})`, // 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)})`, // 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)), // wallBaseLen: Math.round(Math.hypot(wallBaseLine.x2 - wallBaseLine.x1, wallBaseLine.y2 - wallBaseLine.y1)),
}) // })
const dx = Math.abs(p2.x - p1.x); const dx = Math.abs(p2.x - p1.x);
const dy = Math.abs(p2.y - p1.y); 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 midY = (wallBaseLine.y1 + wallBaseLine.y2) / 2
const isH = Math.abs(wallBaseLine.y1 - wallBaseLine.y2) < 0.5 const isH = Math.abs(wallBaseLine.y1 - wallBaseLine.y2) < 0.5
const isV = Math.abs(wallBaseLine.x1 - wallBaseLine.x2) < 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') { if (getOrientation(roofLine) === 'vertical') {
@ -2247,7 +2247,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10 const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
if (prevAbsorbed || nextAbsorbed) { 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 return
} }
} }
@ -2306,7 +2306,7 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
} }
if (isStartEnd.end) { 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 moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
const aStartY = Big(roofLine.y2).plus(moveDist).toNumber() const aStartY = Big(roofLine.y2).plus(moveDist).toNumber()
const bStartY = Big(wallLine.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 prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
if (prevAbsorbed || nextAbsorbed) { 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 return
} }
} }
if (isStartEnd.start) { 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 moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
const aStartY = Big(roofLine.y1).plus(moveDist).toNumber() const aStartY = Big(roofLine.y1).plus(moveDist).toNumber()
const bStartY = Big(wallLine.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 prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
if (prevAbsorbed || nextAbsorbed) { 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 return
} }
} }
if (isStartEnd.start) { 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 moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x1).plus(moveDist).toNumber() const aStartX = Big(roofLine.x1).plus(moveDist).toNumber()
const bStartX = Big(wallLine.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') getAddLine(newPStart, newPEnd, 'red')
} }
if (isStartEnd.end) { if (isStartEnd.end) {
logger.log('isStartEnd:::::', isStartEnd) // logger.log('isStartEnd:::::', isStartEnd)
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x2).minus(moveDist).toNumber() const aStartX = Big(roofLine.x2).minus(moveDist).toNumber()
const bStartX = Big(wallLine.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') getAddLine(newPStart, newPEnd, 'red')
} }
} else if (condition === 'bottom_out') { } else if (condition === 'bottom_out') {
logger.log('bottom_out isStartEnd:::::::', isStartEnd) // logger.log('bottom_out isStartEnd:::::::', isStartEnd)
// [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨. // [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨.
// roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현. // 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 prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10 const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
if (prevAbsorbed || nextAbsorbed) { 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 return
} }
} }
if (isStartEnd.start) { if (isStartEnd.start) {
logger.log('isStartEnd:::::::', isStartEnd) // logger.log('isStartEnd:::::::', isStartEnd)
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x1).minus(moveDist).toNumber() const aStartX = Big(roofLine.x1).minus(moveDist).toNumber()
const bStartX = Big(wallLine.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) { if (isStartEnd.end) {
logger.log('isStartEnd:::::', isStartEnd) // logger.log('isStartEnd:::::', isStartEnd)
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber() const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
const aStartX = Big(roofLine.x2).plus(moveDist).toNumber() const aStartX = Big(roofLine.x2).plus(moveDist).toNumber()
const bStartX = Big(wallLine.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 (toRemove.length > 0) {
if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') { if (process.env.NEXT_PUBLIC_RUN_MODE === 'local') {
logger.log( // logger.log(
`[B안 cull] dead-end ridge 제거 ${toRemove.length}`, // `[B안 cull] dead-end ridge 제거 ${toRemove.length}개`,
toRemove.map( // toRemove.map(
(l) => `(${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})` // (l) => `(${Math.round(l.x1)},${Math.round(l.y1)})→(${Math.round(l.x2)},${Math.round(l.y2)})`
) // )
) // )
} }
toRemove.forEach((line) => { toRemove.forEach((line) => {
canvas.remove(line) canvas.remove(line)
@ -2915,7 +2915,7 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
if(!outerLine) { if(!outerLine) {
outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points); outerLine = findMatchingLine(edgeResult.Polygon, roof, roof.points);
logger.log('Has matching line:', outerLine); // logger.log('Has matching line:', outerLine);
//if(outerLine === null) return //if(outerLine === null) return
} }
// [hip pitch fallback 2026-04-30] // [hip pitch fallback 2026-04-30]
@ -3084,12 +3084,12 @@ function logDeadEndLines(skeletonLines, roof) {
const p2Dead = deadEndVertices.has(k2); const p2Dead = deadEndVertices.has(k2);
if (p1Dead || p2Dead) { if (p1Dead || p2Dead) {
logger.log('⚠️ [logDeadEndLines] 삭제 후보:', { // logger.log('⚠️ [logDeadEndLines] 삭제 후보:', {
idx, // idx,
p1: { x: Math.round(line.p1.x), y: Math.round(line.p1.y), deadEnd: p1Dead, isRoof: isRoofVertex(line.p1) }, // 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) }, // 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), // dx: Math.round(dx), dy: Math.round(dy),
}); // });
} }
}); });
@ -3248,8 +3248,8 @@ const calcOverCorrectedPointsSafe = (points, origPoints, lastPoints) => {
changedNow[i] ? op : (lastPoints[i] ?? op) changedNow[i] ? op : (lastPoints[i] ?? op)
) )
logger.log('[calcOverCorrectedPointsSafe] changedNow:', // logger.log('[calcOverCorrectedPointsSafe] changedNow:',
changedNow.map((v, i) => v ? i : null).filter(v => v !== null)) // changedNow.map((v, i) => v ? i : null).filter(v => v !== null))
return calcOverCorrectedPoints(points, virtualOrig) return calcOverCorrectedPoints(points, virtualOrig)
} }
@ -3271,7 +3271,7 @@ const calcOverCorrectedPoints = (points, origPoints) => {
const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x
const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x
if (origDir * newDir < 0) { 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 skPoints[movedIdx].x = skPoints[fixedIdx].x
didClamp = true didClamp = true
} }
@ -3281,7 +3281,7 @@ const calcOverCorrectedPoints = (points, origPoints) => {
const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y
const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y
if (origDir * newDir < 0) { 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 skPoints[movedIdx].y = skPoints[fixedIdx].y
didClamp = true didClamp = true
} }
@ -4534,14 +4534,14 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => {
const { testDistance = 10, epsilon = 0.5, debug = false } = options; const { testDistance = 10, epsilon = 0.5, debug = false } = options;
if (!wall || !selectLine) { 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' }; return { position: 'unknown', orientation: 'unknown', error: 'invalid_input' };
} }
// selectLine의 좌표 추출 // selectLine의 좌표 추출
const lineCoords = extractLineCoords(selectLine); const lineCoords = extractLineCoords(selectLine);
if (!lineCoords.valid) { if (!lineCoords.valid) {
if (debug) logger.log('ERROR: selectLine 좌표가 유효하지 않음'); // if (debug) logger.log('ERROR: selectLine 좌표가 유효하지 않음');
return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' }; return { position: 'unknown', orientation: 'unknown', error: 'invalid_coords' };
} }
@ -4633,7 +4633,7 @@ export const getSelectLinePosition = (wall, selectLine, options = {}) => {
} else { } else {
// 대각선 // 대각선
if (debug) logger.log('대각선은 지원하지 않음'); // if (debug) logger.log('대각선은 지원하지 않음');
return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' }; return { position: 'unknown', orientation: 'diagonal', error: 'not_supported' };
} }
@ -4848,9 +4848,9 @@ export const processEaveHelpLines = (lines) => {
const mergedHorizontal = mergeLines(horizontalLines, 'horizontal'); const mergedHorizontal = mergeLines(horizontalLines, 'horizontal');
// 결과 확인용 로그 // 결과 확인용 로그
logger.log('Original lines:', lines.length); // logger.log('Original lines:', lines.length);
logger.log('Merged vertical:', mergedVertical.length); // logger.log('Merged vertical:', mergedVertical.length);
logger.log('Merged horizontal:', mergedHorizontal.length); // logger.log('Merged horizontal:', mergedHorizontal.length);
return [...mergedVertical, ...mergedHorizontal]; return [...mergedVertical, ...mergedHorizontal];
}; };
@ -4893,7 +4893,7 @@ const mergeLines = (lines, direction) => {
merged.push(current); merged.push(current);
// 병합 결과 로그 // 병합 결과 로그
logger.log(`Merged ${direction} lines:`, merged); // logger.log(`Merged ${direction} lines:`, merged);
return merged; return merged;
}; };
@ -4959,7 +4959,7 @@ function updateAndAddLine(innerLines, targetPoint) {
if (!foundLine) { if (!foundLine) {
foundLine = findLineContainingPoint(innerLines, targetPoint); foundLine = findLineContainingPoint(innerLines, targetPoint);
if (foundLine) { 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) { if (!foundLine) {
@ -5116,7 +5116,7 @@ function isPointOnLineSegment2(point, lineStart, lineEnd, tolerance = 0.1) {
const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance; const isOnSegment = Math.abs((dist1 + dist2) - lineLength) <= tolerance;
if (isOnSegment) { if (isOnSegment) {
logger.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`); // logger.log(`점 (${px}, ${py})은 선분 [(${x1}, ${y1}), (${x2}, ${y2})] 위에 있습니다.`);
} }
return isOnSegment; return isOnSegment;
@ -5223,13 +5223,13 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
const fmt = (p) => `(${Math.round(p.x)},${Math.round(p.y)})`; const fmt = (p) => `(${Math.round(p.x)},${Math.round(p.y)})`;
const cLen = Math.hypot(clx2 - clx1, cly2 - cly1); const cLen = Math.hypot(clx2 - clx1, cly2 - cly1);
const nLen = Math.hypot(nlx2 - nlx1, nly2 - nly1); const nLen = Math.hypot(nlx2 - nlx1, nly2 - nly1);
logger.log( // logger.log(
`[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` + // `[VALLEY] ${isStartVertex ? 'START' : 'END'} target=${fmt(targetPoint)} ` +
`conn=${fmt({x:clx1,y:cly1})}→${fmt({x:clx2,y:cly2})}[len=${cLen.toFixed(2)}] ` + // `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)}] ` + // `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)} ` + // `p1=${fmt(p1)} p2=${fmt(p2)} p3=${fmt(p3)} cross=${crossProduct.toFixed(2)} ` +
`valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}` // `valley=${collinearSkip ? false : (crossProduct > 0)}${collinearSkip ? ' (collinear-skip)' : ''}`
); // );
if (collinearSkip) return false; if (collinearSkip) return false;
return crossProduct > 0; return crossProduct > 0;
} }