From 281d9bb583e9b6a1cbfbb87f4dc8182627400f22 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:38:27 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.js | 4 +++ src/util/logger.js | 70 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/app/layout.js b/src/app/layout.js index 40386d49..59b1ddcc 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -10,6 +10,8 @@ import Header from '@/components/header/Header' import QModal from '@/components/common/modal/QModal' import PopupManager from '@/components/common/popupManager/PopupManager' import ErrorBoundary from '@/components/common/ErrorBoundary' +import PageTracker from '@/components/common/PageTracker' // [PAGE-TRACKER 2026-05-14] 라우트 변경 시 콘솔/탭타이틀에 경로 표시 +import ModalTracker from '@/components/common/ModalTracker' // [MODAL-TRACKER 2026-05-14] 모달/팝업 open·close 추적 import './globals.css' import '../styles/style.scss' @@ -73,6 +75,8 @@ export default async function RootLayout({ children }) { + + {headerPathname === '/login' || headerPathname === '/join' ? ( {children} ) : ( diff --git a/src/util/logger.js b/src/util/logger.js index 5830c8db..e88b23ff 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -1,30 +1,88 @@ // utils/logger.js const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING; +// [LOGGER-CALLER 2026-05-14] stack 에서 logger.js 이외의 첫 frame 을 추출해 prefix 로 붙임. +// 페이지/hook/컴포넌트 어디서 찍힌 로그인지 콘솔에서 즉시 식별 가능. webpack chunk 매칭 위해 파일명만 추출. +const FILE_LINE_RE = /([\w\-\.]+\.(?:jsx?|tsx?)):(\d+)/ + +// [LOGGER-FORMAT 2026-05-14] 통일 포맷: [HH:mm:ss.SSS] [LEVEL] [caller] +// 레벨별 색상 배지로 콘솔에서 한눈에 구분. +const STYLE_TS = 'color:#888' +const STYLE_CALLER = 'color:#888;font-style:italic' +const STYLE_LEVEL = { + log: 'background:#616161;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + info: 'background:#1e88e5;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + warn: 'background:#fb8c00;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + error: 'background:#e53935;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', + debug: 'background:#8e24aa;color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold', +} + +function formatTime() { + const d = new Date() + const hh = String(d.getHours()).padStart(2, '0') + const mm = String(d.getMinutes()).padStart(2, '0') + const ss = String(d.getSeconds()).padStart(2, '0') + const ms = String(d.getMilliseconds()).padStart(3, '0') + return `${hh}:${mm}:${ss}.${ms}` +} + +function getCallerTag() { + try { + const stack = new Error().stack + if (!stack) return '' + const lines = stack.split('\n') + for (let i = 1; i < lines.length; i++) { + const m = lines[i].match(FILE_LINE_RE) + if (m && m[1] !== 'logger.js') return `${m[1]}:${m[2]}` + } + } catch (_) {} + return '' +} + +function emit(level, args) { + const ts = formatTime() + const caller = getCallerTag() + const levelStyle = STYLE_LEVEL[level] || STYLE_LEVEL.log + const levelLabel = level.toUpperCase() + + // prefix: '[ts] %cLEVEL%c [caller] ' → styled level + dimmed caller + // user 첫 인자가 string 이면 prefix 와 합쳐 %c 호환 유지. + const prefixText = caller + ? `%c[${ts}] %c${levelLabel}%c [${caller}]` + : `%c[${ts}] %c${levelLabel}%c` + const prefixStyles = [STYLE_TS, levelStyle, STYLE_CALLER] + + if (typeof args[0] === 'string') { + console[level](`${prefixText} ${args[0]}`, ...prefixStyles, ...args.slice(1)) + } else { + console[level](prefixText, ...prefixStyles, ...args) + } +} + export const logger = { log: (...args) => { if (isLoggingEnabled) { - console.log(...args); + emit('log', args); } }, error: (...args) => { // 에러는 항상 로깅하거나, 또는 환경에 따라 다르게 처리 - console.error(...args); + emit('error', args); // 운영 환경에서는 서버로 에러를 보내는 코드를 추가할 수도 있음 }, warn: (...args) => { if (isLoggingEnabled) { - console.warn(...args); + emit('warn', args); } }, info: (...args) => { if (isLoggingEnabled) { - console.info(...args); + emit('info', args); } }, debug: (...args) => { if (isLoggingEnabled) { - console.debug(...args); + emit('debug', args); } } -}; \ No newline at end of file +}; -- 2.47.2 From d60bf27778fcd7a84b8aa0dcf5c5acbdd0832248 Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:49:41 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/ModalTracker.jsx | 88 ++++++++++++++++++++++++++ src/components/common/PageTracker.jsx | 28 ++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/components/common/ModalTracker.jsx create mode 100644 src/components/common/PageTracker.jsx diff --git a/src/components/common/ModalTracker.jsx b/src/components/common/ModalTracker.jsx new file mode 100644 index 00000000..4ea2957c --- /dev/null +++ b/src/components/common/ModalTracker.jsx @@ -0,0 +1,88 @@ +'use client' + +// [MODAL-TRACKER 2026-05-14] QModal / PopupManager / contextPopup 의 atom 을 구독해 +// 모달·팝업이 열리고 닫힐 때 콘솔에 컴포넌트명/id 를 출력. 모달 컴포넌트 개별 수정 없음. +import { useEffect, useRef } from 'react' +import { useRecoilValue } from 'recoil' +import { modalState, modalContent } from '@/store/modalAtom' +import { popupState, contextPopupState } from '@/store/popupAtom' +import { logger } from '@/util/logger' + +const TRACK_ENABLED = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true' + +const BADGE_OPEN = 'background:#7b1fa2;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' +const BADGE_CLOSE = 'background:#9e9e9e;color:#fff;padding:2px 8px;border-radius:3px' +const BADGE_POPUP = 'background:#00897b;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' +const BADGE_CTX = 'background:#ef6c00;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold' + +function describeNode(node) { + if (!node) return 'null' + if (Array.isArray(node)) return `Array(${node.length})` + const t = node?.type + if (!t) return typeof node + return typeof t === 'string' ? t : t.displayName || t.name || 'Anonymous' +} + +export default function ModalTracker() { + const open = useRecoilValue(modalState) + const content = useRecoilValue(modalContent) + const popup = useRecoilValue(popupState) + const ctx = useRecoilValue(contextPopupState) + + const firstModal = useRef(true) + const prevPopupIds = useRef({ config: [], other: [] }) + const prevCtx = useRef(null) + + // QModal open/close + useEffect(() => { + if (!TRACK_ENABLED) return + if (firstModal.current) { + firstModal.current = false + return + } + if (open) { + logger.info(`%c[MODAL OPEN] ${describeNode(content)}`, BADGE_OPEN) + } else { + logger.info(`%c[MODAL CLOSE]`, BADGE_CLOSE) + } + }, [open]) + + // PopupManager push/pop (config + other 두 버킷) + useEffect(() => { + if (!TRACK_ENABLED) return + const prev = prevPopupIds.current + for (const bucket of ['config', 'other']) { + const cur = popup?.[bucket] || [] + const curIds = cur.map((p) => p.id) + const prevIds = prev[bucket] || [] + curIds + .filter((id) => !prevIds.includes(id)) + .forEach((id) => { + const item = cur.find((p) => p.id === id) + logger.info(`%c[POPUP+ ${bucket}] ${describeNode(item?.component)} (id=${id})`, BADGE_POPUP) + }) + prevIds + .filter((id) => !curIds.includes(id)) + .forEach((id) => { + logger.info(`%c[POPUP- ${bucket}] (id=${id})`, BADGE_CLOSE) + }) + } + prevPopupIds.current = { + config: (popup?.config || []).map((p) => p.id), + other: (popup?.other || []).map((p) => p.id), + } + }, [popup]) + + // contextPopup (우클릭 메뉴 등) + useEffect(() => { + if (!TRACK_ENABLED) return + if (ctx && !prevCtx.current) { + logger.info(`%c[CTX POPUP+] ${describeNode(ctx)}`, BADGE_CTX) + } else if (!ctx && prevCtx.current) { + logger.info(`%c[CTX POPUP-]`, BADGE_CLOSE) + } + prevCtx.current = ctx + }, [ctx]) + + return null +} diff --git a/src/components/common/PageTracker.jsx b/src/components/common/PageTracker.jsx new file mode 100644 index 00000000..19eb99d9 --- /dev/null +++ b/src/components/common/PageTracker.jsx @@ -0,0 +1,28 @@ +'use client' + +// [PAGE-TRACKER 2026-05-14] 라우트 변경마다 콘솔 + document.title 에 현재 경로 표시. +// production 에서는 NEXT_PUBLIC_ENABLE_LOGGING=false 라 logger.info 가 무음. +import { useEffect } from 'react' +import { usePathname } from 'next/navigation' +import { logger } from '@/util/logger' + +const TRACK_ENABLED = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true' + +export default function PageTracker() { + const pathname = usePathname() + + useEffect(() => { + if (!TRACK_ENABLED || !pathname) return + + logger.info( + `%c[PAGE] ${pathname}`, + 'background:#1e88e5;color:#fff;padding:2px 8px;border-radius:3px;font-weight:bold', + ) + + if (typeof document !== 'undefined') { + document.title = `${pathname} · HANASYS DESIGN` + } + }, [pathname]) + + return null +} -- 2.47.2 From 7e9ec4e3f2df00d2c23eaac4057eaaa71733093b Mon Sep 17 00:00:00 2001 From: ysCha Date: Thu, 14 May 2026 14:55:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/util/logger.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/logger.js b/src/util/logger.js index e88b23ff..c096298c 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -1,5 +1,7 @@ // utils/logger.js -const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING; +// [LOGGER-GUARD 2026-05-14] string 'false' 가 truthy 라 production 에서도 가드를 통과하던 버그 수정. +// 명시적 === 'true' 비교 → Next.js inline replacement + dead code elimination 으로 production 빌드에서 emit 호출 제거. +const isLoggingEnabled = process.env.NEXT_PUBLIC_ENABLE_LOGGING === 'true'; // [LOGGER-CALLER 2026-05-14] stack 에서 logger.js 이외의 첫 frame 을 추출해 prefix 로 붙임. // 페이지/hook/컴포넌트 어디서 찍힌 로그인지 콘솔에서 즉시 식별 가능. webpack chunk 매칭 위해 파일명만 추출. -- 2.47.2