ERP-node/frontend/lib/authLogger.ts

226 lines
5.8 KiB
TypeScript
Raw Normal View History

/**
*
* - //
* - localStorage에
* - window.__AUTH_LOG.show()
*/
const STORAGE_KEY = "auth_debug_log";
const MAX_ENTRIES = 200;
export type AuthEventType =
| "TOKEN_SET"
| "TOKEN_REMOVED"
| "TOKEN_EXPIRED_DETECTED"
| "TOKEN_REFRESH_START"
| "TOKEN_REFRESH_SUCCESS"
| "TOKEN_REFRESH_FAIL"
| "REDIRECT_TO_LOGIN"
| "API_401_RECEIVED"
| "API_401_RETRY"
| "AUTH_CHECK_START"
| "AUTH_CHECK_SUCCESS"
| "AUTH_CHECK_FAIL"
| "AUTH_GUARD_BLOCK"
| "AUTH_GUARD_PASS"
| "MENU_LOAD_FAIL"
| "VISIBILITY_CHANGE"
| "MIDDLEWARE_REDIRECT";
interface AuthLogEntry {
timestamp: string;
event: AuthEventType;
detail: string;
tokenStatus: string;
url: string;
stack?: string;
}
function getTokenSummary(): string {
if (typeof window === "undefined") return "SSR";
const token = localStorage.getItem("authToken");
if (!token) return "없음";
try {
const payload = JSON.parse(atob(token.split(".")[1]));
const exp = payload.exp * 1000;
const now = Date.now();
const remainMs = exp - now;
if (remainMs <= 0) {
return `만료됨(${Math.abs(Math.round(remainMs / 60000))}분 전)`;
}
const remainMin = Math.round(remainMs / 60000);
const remainHour = Math.floor(remainMin / 60);
const min = remainMin % 60;
return `유효(${remainHour}h${min}m 남음, user:${payload.userId})`;
} catch {
return "파싱실패";
}
}
function getCallStack(): string {
try {
const stack = new Error().stack || "";
const lines = stack.split("\n").slice(3, 7);
return lines.map((l) => l.trim()).join(" <- ");
} catch {
return "";
}
}
function writeLog(event: AuthEventType, detail: string) {
if (typeof window === "undefined") return;
const entry: AuthLogEntry = {
timestamp: new Date().toISOString(),
event,
detail,
tokenStatus: getTokenSummary(),
url: window.location.pathname + window.location.search,
stack: getCallStack(),
};
// 콘솔 출력 (그룹)
const isError = ["TOKEN_REMOVED", "REDIRECT_TO_LOGIN", "API_401_RECEIVED", "TOKEN_REFRESH_FAIL", "AUTH_GUARD_BLOCK"].includes(event);
const logFn = isError ? console.warn : console.debug;
logFn(`[AuthLog] ${event}: ${detail} | 토큰: ${entry.tokenStatus} | ${entry.url}`);
// localStorage에 저장
try {
const stored = localStorage.getItem(STORAGE_KEY);
const logs: AuthLogEntry[] = stored ? JSON.parse(stored) : [];
logs.push(entry);
// 최대 개수 초과 시 오래된 것 제거
while (logs.length > MAX_ENTRIES) {
logs.shift();
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(logs));
} catch {
// localStorage 공간 부족 등의 경우 무시
}
}
/**
*
*/
function getLogs(): AuthLogEntry[] {
if (typeof window === "undefined") return [];
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
/**
*
*/
function clearLogs() {
if (typeof window === "undefined") return;
localStorage.removeItem(STORAGE_KEY);
}
/**
*
*/
function showLogs(filter?: AuthEventType | "ERROR") {
const logs = getLogs();
if (logs.length === 0) {
console.log("[AuthLog] 저장된 로그가 없습니다.");
return;
}
let filtered = logs;
if (filter === "ERROR") {
filtered = logs.filter((l) =>
["TOKEN_REMOVED", "REDIRECT_TO_LOGIN", "API_401_RECEIVED", "TOKEN_REFRESH_FAIL", "AUTH_GUARD_BLOCK", "AUTH_CHECK_FAIL", "TOKEN_EXPIRED_DETECTED"].includes(l.event)
);
} else if (filter) {
filtered = logs.filter((l) => l.event === filter);
}
console.log(`\n[AuthLog] 총 ${filtered.length}건 (전체 ${logs.length}건)`);
console.log("─".repeat(120));
filtered.forEach((entry, i) => {
const time = entry.timestamp.replace("T", " ").split(".")[0];
console.log(
`${i + 1}. [${time}] ${entry.event}\n 상세: ${entry.detail}\n 토큰: ${entry.tokenStatus}\n 경로: ${entry.url}${entry.stack ? `\n 호출: ${entry.stack}` : ""}\n`
);
});
}
/**
*
*/
function getLastRedirectReason(): AuthLogEntry | null {
const logs = getLogs();
for (let i = logs.length - 1; i >= 0; i--) {
if (logs[i].event === "REDIRECT_TO_LOGIN") {
return logs[i];
}
}
return null;
}
/**
*
*/
function downloadLogs() {
if (typeof window === "undefined") return;
const logs = getLogs();
if (logs.length === 0) {
console.log("[AuthLog] 저장된 로그가 없습니다.");
return;
}
const text = logs
.map((entry, i) => {
const time = entry.timestamp.replace("T", " ").split(".")[0];
return `[${i + 1}] ${time} | ${entry.event}\n 상세: ${entry.detail}\n 토큰: ${entry.tokenStatus}\n 경로: ${entry.url}${entry.stack ? `\n 호출: ${entry.stack}` : ""}`;
})
.join("\n\n");
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `auth-debug-log_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-")}.txt`;
a.click();
URL.revokeObjectURL(url);
console.log("[AuthLog] 로그 파일 다운로드 완료");
}
// 전역 접근 가능하게 등록
if (typeof window !== "undefined") {
(window as any).__AUTH_LOG = {
show: showLogs,
errors: () => showLogs("ERROR"),
clear: clearLogs,
download: downloadLogs,
lastRedirect: getLastRedirectReason,
raw: getLogs,
};
}
export const AuthLogger = {
log: writeLog,
getLogs,
clearLogs,
showLogs,
downloadLogs,
getLastRedirectReason,
};
export default AuthLogger;