dev #862
@ -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 })
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 }) {
|
||||
<ErrorBoundary>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<PageTracker />
|
||||
<ModalTracker />
|
||||
{headerPathname === '/login' || headerPathname === '/join' ? (
|
||||
<QcastProvider>{children}</QcastProvider>
|
||||
) : (
|
||||
|
||||
88
src/components/common/ModalTracker.jsx
Normal file
88
src/components/common/ModalTracker.jsx
Normal file
@ -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
|
||||
}
|
||||
28
src/components/common/PageTracker.jsx
Normal file
28
src/components/common/PageTracker.jsx
Normal file
@ -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
|
||||
}
|
||||
@ -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))
|
||||
|
||||
@ -197,7 +197,7 @@ export default function ChangePasswordPop(props) {
|
||||
</div>
|
||||
<div className="change-password-guide">
|
||||
<span>{getMessage('main.popup.login.guide1')}</span>
|
||||
<span>{getMessage('main.popup.login.guide2')}</span>
|
||||
{/*<span>{getMessage('main.popup.login.guide2')}</span>*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer-btn-wrap">
|
||||
|
||||
@ -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,81 +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 {
|
||||
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',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//풍속선택 팝업에서 넘어온 바람정보
|
||||
@ -2051,13 +2086,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 +2684,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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 추가
|
||||
@ -668,31 +670,9 @@ 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
|
||||
})
|
||||
console.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) => {
|
||||
console.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 +683,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 +727,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; // 포개지는 경우에만 삭제
|
||||
}
|
||||
}
|
||||
@ -758,20 +738,6 @@ export function useRoofAllocationSetting(id) {
|
||||
});
|
||||
// Combine remaining lines with newEaveLines
|
||||
roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines];
|
||||
// [ALLOC-DEBUG] 병합 결과 breakdown
|
||||
console.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) => {
|
||||
console.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]
|
||||
}
|
||||
@ -811,22 +777,13 @@ export function useRoofAllocationSetting(id) {
|
||||
// extensionLine + 동일직선 SK 1:1 통합 (대각선 단일길이/각도 면적 산출)
|
||||
integrateExtensionLines(roofBase)
|
||||
|
||||
// [ALLOC-DEBUG] split 직전 최종 입력
|
||||
console.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 {
|
||||
splitPolygonWithLines(roofBase)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
logger.log(e)
|
||||
canvas.discardActiveObject()
|
||||
return
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1730,10 +1739,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5100,6 +5124,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, {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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": "全体",
|
||||
@ -936,7 +937,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": "変更しない",
|
||||
|
||||
@ -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": "전체",
|
||||
@ -936,7 +937,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": "변경안함",
|
||||
|
||||
@ -1,30 +1,90 @@
|
||||
// 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 매칭 위해 파일명만 추출.
|
||||
const FILE_LINE_RE = /([\w\-\.]+\.(?:jsx?|tsx?)):(\d+)/
|
||||
|
||||
// [LOGGER-FORMAT 2026-05-14] 통일 포맷: [HH:mm:ss.SSS] [LEVEL] [caller] <message>
|
||||
// 레벨별 색상 배지로 콘솔에서 한눈에 구분.
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user