diff --git a/fems-api/src/config/logger.js b/fems-api/src/config/logger.js index 6b81613..23ded8c 100644 --- a/fems-api/src/config/logger.js +++ b/fems-api/src/config/logger.js @@ -1,24 +1,59 @@ // src/config/logger.js const winston = require("winston"); +require("winston-daily-rotate-file"); const path = require("path"); -const logger = winston.createLogger({ - level: process.env.NODE_ENV === "production" ? "info" : "debug", +// 로그 디렉토리 설정 +const logDir = path.join(__dirname, "../../logs"); + +// 에러 로그 설정 +const errorTransport = new winston.transports.DailyRotateFile({ + level: "error", + dirname: path.join(logDir, "error"), // error 폴더 안에 저장 + filename: "error-%DATE%.log", + datePattern: "YYYY-MM-DD", + zippedArchive: true, // 이전 로그 파일 압축 저장 + maxSize: "20m", // 파일 최대 크기 + maxFiles: "14d", // 로그 파일 보관 기간 format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), - transports: [ - new winston.transports.File({ - filename: path.join(__dirname, "../../logs/error.log"), - level: "error", - }), - new winston.transports.File({ - filename: path.join(__dirname, "../../logs/combined.log"), - }), - ], }); +// 일반 로그 설정 +const infoTransport = new winston.transports.DailyRotateFile({ + level: "info", + dirname: path.join(logDir, "info"), // info 폴더 안에 저장 + filename: "info-%DATE%.log", + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), +}); + +// 로그 포맷 설정 +const logFormat = winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.printf(({ level, message, timestamp, ...meta }) => { + return `${timestamp} ${level}: ${message} ${ + Object.keys(meta).length ? JSON.stringify(meta) : "" + }`; + }) +); + +// 로거 생성 +const logger = winston.createLogger({ + level: process.env.NODE_ENV === "production" ? "info" : "debug", + format: logFormat, + transports: [errorTransport, infoTransport], +}); + +// 개발 환경에서는 콘솔 출력 추가 if (process.env.NODE_ENV !== "production") { logger.add( new winston.transports.Console({ @@ -30,4 +65,15 @@ if (process.env.NODE_ENV !== "production") { ); } +// 로그 디렉토리가 없으면 생성 +const fs = require("fs"); +const errorLogDir = path.join(logDir, "error"); +const infoLogDir = path.join(logDir, "info"); + +[errorLogDir, infoLogDir].forEach((dir) => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } +}); + module.exports = logger; diff --git a/fems-app/src/hooks/use-toast.ts b/fems-app/src/hooks/use-toast.ts index 02e111d..8f97a33 100644 --- a/fems-app/src/hooks/use-toast.ts +++ b/fems-app/src/hooks/use-toast.ts @@ -1,78 +1,60 @@ -"use client" +"use client"; -// Inspired by react-hot-toast library -import * as React from "react" +import * as React from "react"; +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; type Action = | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast + type: "ADD_TOAST"; + toast: ToasterToast; } | { - type: ActionType["UPDATE_TOAST"] - toast: Partial + type: "UPDATE_TOAST"; + toast: Partial; } | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] - } - | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } + type: "DISMISS_TOAST" | "REMOVE_TOAST"; + toastId?: ToasterToast["id"]; + }; -interface State { - toasts: ToasterToast[] +let count = 0; + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); } -const toastTimeouts = new Map>() +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { - return + return; } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) + toastTimeouts.delete(toastId); dispatch({ type: "REMOVE_TOAST", toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) + }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) -} + toastTimeouts.set(toastId, timeout); +}; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -80,7 +62,7 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + }; case "UPDATE_TOAST": return { @@ -88,19 +70,17 @@ export const reducer = (state: State, action: Action): State => { toasts: state.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t ), - } + }; case "DISMISS_TOAST": { - const { toastId } = action + const { toastId } = action; - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + addToRemoveQueue(toast.id); + }); } return { @@ -113,44 +93,44 @@ export const reducer = (state: State, action: Action): State => { } : t ), - } + }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, toasts: [], - } + }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + }; } -} +}; -const listeners: Array<(state: State) => void> = [] +const listeners: Array<(state: State) => void> = []; -let memoryState: State = { toasts: [] } +let memoryState: State = { toasts: [] }; function dispatch(action: Action) { - memoryState = reducer(memoryState, action) + memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState) - }) + listener(memoryState); + }); } -type Toast = Omit +type Toast = Omit; function toast({ ...props }: Toast) { - const id = genId() + const id = genId(); const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); dispatch({ type: "ADD_TOAST", @@ -159,36 +139,36 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() + if (!open) dismiss(); }, }, - }) + }); return { id: id, dismiss, update, - } + }; } function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.push(setState) + listeners.push(setState); return () => { - const index = listeners.indexOf(setState) + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + }; } -export { useToast, toast } +export { useToast, toast };