dev #850
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user