Repository: SangJin983/my-google-calendar
Pull request #14: Feat: basic logger
Author: login: SangJin983
Created: 2025-04-02T18:12:32Z | Merged: 2025-04-17T07:23:20Z

## [feat] 기본적인 로깅 시스템 구현
### 변경사항 요약
- 로그 레벨 정의 (`DEBUG`, `INFO`, `WARN`, `ERROR`) 및 상수화 
- 환경 변수 (`VITE_LOG_LEVEL`)와 config 파일을 통해 로그 레벨을 설정 
- 로그 레벨에 따른 출력을 필터링하는 기능 추가
- 로그 메세지를 양식에 맞게 포맷팅하는 기능 추가
- 각 로그 레벨에 맞는 출력 기능(`console.debug`, `console.info`...) 추가
### 변경 이유
- 디버깅과 모니터링 과정에서 더 나은 개발 경험을 제공하고, 로그가 사용자에게 노출되는 경우를 방지하기 위함
- 추후 다른 로깅 라이브러리를 사용하거나 외부에 파일로 저장하는 경우를 대비하기 위해, 약간의 포맷팅을 추가함
- 안정적인 제어를 위해 constant, config 파일을 분리함


Diff:
diff --git a/.gitignore b/.gitignore
index a547bf3..169ada5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,9 @@ lerna-debug.log*
 node_modules
 dist
 dist-ssr
+
+# Environment variables files
+.env*
 *.local
 
 # Editor directories and files
diff --git a/src/app/main.jsx b/src/app/main.jsx
index 237200d..b98fadf 100644
--- a/src/app/main.jsx
+++ b/src/app/main.jsx
@@ -1,4 +1,7 @@
 import GlobalStyle from "@common/styles/globalStyles.js";
+import { consoleLogHandler } from "@common/utils/index.js";
+import { createLogger } from "@common/utils/logger/logger.js";
+import { getLoggerLevel } from "@config/loggerConfig.js";
 import { StrictMode } from "react";
 import { createRoot } from "react-dom/client";
 import { Provider as ReduxProvider } from "react-redux";
@@ -6,6 +9,11 @@ import { BrowserRouter } from "react-router-dom";
 import App from "./App.jsx";
 import store from "./store/store.js";
 
+// --- 로거 설정 및 초기화 ---
+const logLevel = getLoggerLevel();
+createLogger({ level: logLevel, handler: consoleLogHandler });
+
+// --- 애플리케이션 렌더링 ---
 createRoot(document.getElementById("root")).render(
   <StrictMode>
     <ReduxProvider store={store}>
diff --git a/src/common/constants/index.js b/src/common/constants/index.js
index ed1ec7e..ebbc313 100644
--- a/src/common/constants/index.js
+++ b/src/common/constants/index.js
@@ -1 +1,2 @@
+export * from "./logLevels";
 export * from "./status";
diff --git a/src/common/constants/logLevels.js b/src/common/constants/logLevels.js
new file mode 100644
index 0000000..0d07c5b
--- /dev/null
+++ b/src/common/constants/logLevels.js
@@ -0,0 +1,6 @@
+export const LOG_LEVELS = {
+  DEBUG: 0,
+  INFO: 1,
+  WARN: 2,
+  ERROR: 3,
+};
diff --git a/src/common/utils/index.js b/src/common/utils/index.js
new file mode 100644
index 0000000..89db5ea
--- /dev/null
+++ b/src/common/utils/index.js
@@ -0,0 +1,2 @@
+export * from "./logger";
+export * from "./result"
\ No newline at end of file
diff --git a/src/common/utils/logger/consoleLogHandler.js b/src/common/utils/logger/consoleLogHandler.js
new file mode 100644
index 0000000..3ce7586
--- /dev/null
+++ b/src/common/utils/logger/consoleLogHandler.js
@@ -0,0 +1,31 @@
+import { LOG_LEVELS } from "@common/constants";
+
+export const consoleLogHandler = {
+  /**
+   * 로그 메시지를 콘솔에 출력 합니다.
+   * @param {number} level - 로그 레벨 숫자 값 (LOG_LEVELS 참고)
+   * @param {string} levelName - 로그 레벨 이름 ('DEBUG', 'INFO' 등)
+   * @param {string} formattedMessage - 포맷팅된 로그 메시지
+   */
+
+  handleLog: (level, levelName, formattedMessage) => {
+    // levelName 은 현재 직접 사용하지 않지만 인터페이스 유지를 위해 받음
+    switch (level) {
+      case LOG_LEVELS.DEBUG:
+        console.debug(formattedMessage);
+        break;
+      case LOG_LEVELS.INFO:
+        console.info(formattedMessage);
+        break;
+      case LOG_LEVELS.WARN:
+        console.warn(formattedMessage);
+        break;
+      case LOG_LEVELS.ERROR:
+        console.error(formattedMessage);
+        break;
+      default:
+        console.log(`[${levelName}] ${formattedMessage}`); // 기본 console.log 에 levelName 추가
+        break;
+    }
+  },
+};
diff --git a/src/common/utils/logger/index.js b/src/common/utils/logger/index.js
new file mode 100644
index 0000000..2bc0101
--- /dev/null
+++ b/src/common/utils/logger/index.js
@@ -0,0 +1,2 @@
+export { consoleLogHandler } from "./consoleLogHandler";
+export { logger } from "./logger";
diff --git a/src/common/utils/logger/logger.js b/src/common/utils/logger/logger.js
new file mode 100644
index 0000000..a0821fd
--- /dev/null
+++ b/src/common/utils/logger/logger.js
@@ -0,0 +1,113 @@
+import { LOG_LEVELS } from "@common/constants";
+
+let logger;
+
+// --- 유틸 함수 ---
+const getLogLevelName = (levelValue) => {
+  return (
+    Object.keys(LOG_LEVELS).find((key) => LOG_LEVELS[key] === levelValue) ||
+    "UNKNOWN"
+  );
+};
+
+const safeJsonStringify = (value, indent = 2) => {
+  try {
+    return JSON.stringify(value, null, indent);
+  } catch (e) {
+    return "(Cannot stringify value)";
+  }
+};
+
+const formatErrorArg = (errorObject) => {
+  if (!(errorObject instanceof Error)) {
+    return ""; // Error 객체가 아니라면 빈 문자열 반환
+  }
+
+  return `\nError:${errorObject.message}\nStack:${errorObject.stack}`;
+};
+const formatOtherArgs = (args) => {
+  if (!args || args.length === 0) {
+    return "";
+  }
+
+  return `\nArgs:${safeJsonStringify(args)}`;
+};
+
+const formatLogMessage = (level, message, args) => {
+  const timeStamp = new Date().toISOString();
+  const levelName = getLogLevelName(level);
+  const baseMessage = `[${timeStamp}] [${levelName}] ${message}`;
+
+  if (!args || args.length === 0) {
+    return baseMessage;
+  }
+
+  // Error 객체 찾기
+  const errorObject = args.find((arg) => arg instanceof Error);
+
+  // 요청한 로그의 레벨이 ERROR 이상이고 Error 객체가 존재한다면 에러 포멧팅 적용
+  if (level >= LOG_LEVELS.ERROR && errorObject) {
+    const otherArgs = args.filter((arg) => arg !== errorObject); // Error 객체가 아닌 나머지 인자 필터링
+    const formattedErrorMessage = formatErrorArg(errorObject);
+    const formattedOtherMessage = formatOtherArgs(otherArgs);
+
+    // 기본 메시지 + 에러 포멧 + 나머지 인자 포멧
+    return `${baseMessage}${formattedErrorMessage}${formattedOtherMessage}`;
+  }
+
+  const formattedArgsMessage = formatOtherArgs(args);
+  return `${baseMessage}${formattedArgsMessage}`;
+};
+
+// --- 로거 생성(초기화) 함수 ---
+/**
+ * 주입 받은 설정으로 로거 인스턴스를 초기화합니다.
+ * @param {object} config - 로거 설정 객체
+ * @param {number} config.level - 로그 레벨 (숫자)
+ * @param {object} config.handler - 로그 핸들러 객체 (handleLog 메서드 필요)
+ * @return {object} - logger 인스턴스 (debug, info, warn, error 메서드 포함)
+ */
+const createLogger = (config) => {
+  if (typeof config?.level !== "number" || !config?.handler?.handleLog) {
+    console.error(
+      "유효 하지 않은 로거 설정이 주입되었습니다. 기본 콘솔 로거를 사용합니다.",
+      config
+    );
+    return {
+      debug: console.debug,
+      info: console.info,
+      warn: console.warn,
+      error: console.error,
+    };
+  }
+
+  const CURRENT_LOG_LEVEL = config.level;
+  const CURRENT_LOG_HANDLER = config.handler;
+
+  const log = (level, message, ...args) => {
+    if (level < CURRENT_LOG_LEVEL) {
+      return;
+    }
+
+    const formattedMessage = formatLogMessage(level, message, args);
+    const levelName = getLogLevelName(level);
+
+    try {
+      // 설정에서 주입 받은 핸들러 사용
+      CURRENT_LOG_HANDLER.handleLog(level, levelName, formattedMessage);
+    } catch (handlerError) {
+      console.error("Logging handler failed:", handlerError);
+      console.error("Original log message:", formattedMessage);
+    }
+  };
+
+  // 최종 로거 인스턴스 반환
+  logger = {
+    debug: (msg, ...args) => log(LOG_LEVELS.DEBUG, msg, ...args),
+    info: (msg, ...args) => log(LOG_LEVELS.INFO, msg, ...args),
+    warn: (msg, ...args) => log(LOG_LEVELS.WARN, msg, ...args),
+    error: (msg, ...args) => log(LOG_LEVELS.ERROR, msg, ...args),
+  };
+};
+
+export { createLogger, logger };
diff --git a/src/common/utils/result.js b/src/common/utils/result.js
new file mode 100644
index 0000000..1f63123
--- /dev/null
+++ b/src/common/utils/result.js
@@ -0,0 +1,7 @@
+export const Ok = (value) => ({
+  ok: true,
+  value,
+});
+export const Err = (error) => ({ ok: false, error });
+export const isOk = (result) => result.ok === true;
+export const isErr = (result) => result.ok === false;
diff --git a/src/config/index.js b/src/config/index.js
new file mode 100644
index 0000000..7d2ae1c
--- /dev/null
+++ b/src/config/index.js
@@ -0,0 +1,2 @@
+export * from "./loggerConfig";
+export * from "./supabaseConfig";
diff --git a/src/config/loggerConfig.js b/src/config/loggerConfig.js
new file mode 100644
index 0000000..1496ed4
--- /dev/null
+++ b/src/config/loggerConfig.js
@@ -0,0 +1,17 @@
+import { LOG_LEVELS } from "@common/constants";
+
+const getLogLevelFromEnv = () => {
+  const logLevelName =
+    import.meta.env.VITE_LOG_LEVEL || (import.meta.env.PROD ? "INFO" : "DEBUG");
+
+  return LOG_LEVELS[logLevelName.toUpperCase()] ?? LOG_LEVELS.DEBUG;
+};
+
+const getLogLevelNameFromEnv = () => {
+  return (
+    import.meta.env.VITE_LOG_LEVEL || (import.meta.env.PROD ? "INFO" : "DEBUG")
+  );
+};
+
+export const getLoggerLevel = getLogLevelFromEnv;
+export const getLoggerLevelName = getLogLevelNameFromEnv;
diff --git a/src/features/events/utils/timeUtils.js b/src/features/events/utils/timeUtils.js
index 553cc42..dfa02ce 100644
--- a/src/features/events/utils/timeUtils.js
+++ b/src/features/events/utils/timeUtils.js
@@ -1,126 +1,131 @@
-const formatDateToLocalDateTimeString = (date) => {
-  if (!date || Number.isNaN(date.getTime())) {
-    return "";
+import { Err, logger, Ok } from "@common/utils";
+import {
+  addHours,
+  format,
+  isAfter,
+  isBefore,
+  isValid,
+  parse,
+  setHours,
+  startOfDay,
+  startOfHour,
+  subHours,
+} from "date-fns";
+
+// -- 커스텀 에러 정의 --
+class InvalidDateFormatError extends Error {
+  constructor(message = "Invalid date format") {
+    super(message);
+    this.name = "InvaildDateFormatError";
   }
-  try {
-    const year = date.getFullYear();
-    const month = (date.getMonth() + 1).toString().padStart(2, "0");
-    const day = date.getDate().toString().padStart(2, "0");
-    const hour = date.getHours().toString().padStart(2, "0");
-    const minute = "00"; // 정각으로 고정
-
-    return `${year}-${month}-${day}T${hour}:${minute}`;
-  } catch {
-    return "";
+}
+class DateCalculationError extends Error {
+  constructor(message = "Date calculation error") {
+    super(message);
+    this.name = "DateCalculationError";
   }
-};
+}
 
-const adjustTimeToHour = (dateTimeString) => {
+const LOCAL_DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm";
+
+// 현지 시간 문자열 -> Date 객체 변환 및 정시 처리
+const parseAndAdjustTimeToHour = (dateTimeString) => {
   if (!dateTimeString) {
-    return "";
+    return Err(new Error("Input string is empty"));
   }
   try {
-    const [date, time] = dateTimeString.split("T");
-    const hour = time?.split(":")[0];
-
-    if (hour === undefined) {
-      throw new Error("Invalid time format");
+    // 'yyyy-MM-ddTHH:mm' 형식으로 파싱 시도
+    const parsedDate = parse(dateTimeString, LOCAL_DATETIME_FORMAT, new Date());
+    // 유효한 날짜인지 확인
+    if (!isValid(parsedDate)) {
+      return Err(new InvalidDateFormatError());
     }
-
-    const adjustedTime = `${date}T${hour.padStart(2, "0")}:00`;
-
-    return adjustedTime;
+    // Date에 정시처리
+    return Ok(startOfHour(parsedDate));
   } catch (error) {
-    console.error("Error adjusting time:", error, dateTimeString)
-    return dateTimeString;
+    logger.error("Unexpected error parsing date time:", error, dateTimeString);
+    return Err(
+      error instanceof Error ? error : new Error("Unknown parsing error")
+    );
   }
 };
 
-const calculateStartTimeConstraints = (endDateTimeString) => {
-  if (!endDateTimeString) {
-    return { min: "", max: "" };
+// Date 객체 -> 현지 시간 문자열 (YYYY-MM-DDTHH:mm) 변환
+const formatDateToLocalDateTimeString = (date) => {
+  if (!date || !isValid(date)) {
+    return Err(new Error("Invalid Date object provided"));
   }
   try {
-    const datePart = endDateTimeString.split("T")[0];
-    const minStartTimeLocalString = `${datePart}T00:00`;
-
-    const maxStartDate = new Date(endDateTimeString); // 정확한 계산을 위해 Date 객체로 변환
-    maxStartDate.setHours(maxStartDate.getHours() - 1);
-    const maxStartTimeLocalString =
-      formatDateToLocalDateTimeString(maxStartDate);
-
-    // min과 max값 비교(종료시간이 00시와 같이, 날짜가 넘어간 케이스 대비)
-    if (maxStartDate < new Date(minStartTimeLocalString)) {
-      return { min: minStartTimeLocalString, max: minStartTimeLocalString };
-    }
-
-    return { min: minStartTimeLocalString, max: maxStartTimeLocalString };
-  } catch {
-    return { min: "", max: "" };
+    return Ok(format(date, LOCAL_DATETIME_FORMAT));
+  } catch (error) {
+    logger.error("Error formating date to local string:", error, date);
+    return Err(
+      error instanceof Error ? error : new Error("Unknown formatting error")
+    );
   }
 };
 
-const calculateEndTimeConstraints = (startDateTimeString) => {
-  if (!startDateTimeString) {
-    return { min: "", max: "" };
+const dateToUTCISOString = (date) => {
+  if (!date || !isValid(date)) {
+    return Err(new Error("Invalid Date object provided"));
   }
   try {
-    const datePart = startDateTimeString.split("T")[0];
-    const maxEndTimeLocalString = `${datePart}T23:00`;
-
-    const minEndDate = new Date(startDateTimeString);
-    minEndDate.setHours(minEndDate.getHours() + 1);
-    const minEndTimeLocalString = formatDateToLocalDateTimeString(minEndDate);
-
-    // 시작시간 23시와 같이 종료시간 날짜가 넘어가는 케이스 대비
-    if (minEndDate > new Date(maxEndTimeLocalString)) {
-      return { min: maxEndTimeLocalString, max: maxEndTimeLocalString };
-    }
-
-    return { min: minEndTimeLocalString, max: maxEndTimeLocalString };
-  } catch {
-    return { min: "", max: "" };
+    return Ok(date.toISOString());
+  } catch (error) {
+    logger.error("Error converting date to UTC ISO string:", error, date);
+    return Err(
+      error instanceof Error ? error : new Error("Unknown conversion error")
+    );
   }
 };
 
-const isTimeBefore = (time1LocalString, time2LocalString) => {
-  if (!time1LocalString || !time2LocalString) {
-    return false;
+const calculateStartTimeConstraints = (endDate) => {
+  if (!endDate || !isValid(endDate)) {
+    return Err(new Error("Invalid end date provided"));
   }
   try {
-    return new Date(time1LocalString) < new Date(time2LocalString);
-  } catch {
-    return false;
-  }
-};
+    const minStartDate = startOfDay(endDate);
+    const maxStartDate = subHours(endDate, 1);
 
-const isTimeAfter = (time1LocalString, time2LocalString) => {
-  if (!time1LocalString || !time2LocalString) {
-    return false;
-  }
-  try {
-    return new Date(time1LocalString) > new Date(time2LocalString);
-  } catch {
-    return false;
+    // maxStartDate가 minStartDate 이전인 경우 처리 (ex: 종료 시간이 00시인 경우)
+    if (isBefore(maxStartDate, minStartDate)) {
+      return Ok({ min: minStartDate, max: minStartDate });
+    }
+    return Ok({ min: minStartDate, max: maxStartDate });
+  } catch (error) {
+    logger.error("Error calculating start time constraints:", error, endDate);
+    return Err(
+      new DateCalculationError("Failed to calculate start constraints")
+    );
   }
 };
 
-const areSameLocalDateString = (dateTimeString1, dateTimeString2) => {
-  if (!dateTimeString1 || !dateTimeString2) {
-    return false;
+const calculateEndTimeConstraints = (startDate) => {
+  if (!startDate || !isValid(startDate)) {
+    return Err(new Error("Invalid start date provided"));
   }
   try {
-    return dateTimeString1.split("T")[0] === dateTimeString2.split("T")[0];
-  } catch {
-    return false;
+    const minEndDate = addHours(startDate, 1);
+    const maxEndDate = setHours(startDate, 23);
+
+    // minEndDate가 maxEndDate 이후인 경우 처리 (ex: 시작 시간이 23시인 경우)
+    if (isAfter(minEndDate, maxEndDate)) {
+      return Ok({ min: maxEndDate, max: maxEndDate });
+    }
+
+    return Ok({ min: minEndDate, max: maxEndDate });
+  } catch (error) {
+    logger.error("Error calculating end time constraints:", error, startDate);
+    return Err(new DateCalculationError("Failed to calculate end constraints"));
   }
 };
 
 export {
-  adjustTimeToHour,
-  areSameLocalDateString,
   calculateEndTimeConstraints,
   calculateStartTimeConstraints,
-  isTimeBefore,
-  isTimeAfter,
+  DateCalculationError,
+  dateToUTCISOString,
+  formatDateToLocalDateTimeString,
+  InvalidDateFormatError,
+  parseAndAdjustTimeToHour,
 };


Reviews:
user: login: pjb6510
body: .env파일은 반드시 .gitignore에 추가하여 git에서 제거하세요.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T20:47:16Z
user: login: SangJin983
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T06:25:46Z
user: login: SangJin983
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T08:06:31Z
user: login: SangJin983
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T12:44:48Z
user: login: pjb6510
body: 지금과 같은 방식은 주입이 아니라 호출입니다. 이를 혼동해서는 안됩니다. 아주아주 중요합니다.
주입은 메소드나 함수의 파라미터 혹은 언어에 따라서 annotation, decorator로 주입됩니다.

호출과 주입은 아주아주 큰 차이가 있습니다.
주입은 호출보다 계약조건이 까다롭고 명시적입니다.

계약 조건이란 : 특정 로직 (보통 프로시저나 함수)을 호출함에 있어 지켜야하는 조건.

## 시간적 계약 조건 : 어떤 함수는, 다른 어떤 함수가 실행 된 후 실행되어야 합니다.
- 의존 관계로서 호출이라는 방법을 선택할 경우, 호출에 따른 상하 관계에 따라 주입받는 모듈과 주입되는 모듈의 초기화 시간이 달라질 수 있습니다. 프로그램의 진입점으로부터 가까운 순으로 모듈이 초기화 됩니다. 자바스크립트를 기준으로는 호출의 방법으로는 시간적 계약 조건이 명시되지 않습니다. DB에 로그를 남기기 위해 DB 연결을 먼저 해야하는 상황을 상상해보세요. 과연 DB Insert 함수가 logger를 통해 호출되기 전에 DB 연결이 되어있는지를 보장할 수 있을까요?
- 반면, 파라미터를 통해 인자를 내려받는 방식은, 해당 함수를 비로소 실행 할 때, 모듈을 파라미터를 통해 내려주므로 시간적 조건을 제어할 수 있습니다. 이는 명시적입니다. 

## 제어권의 역전 : SOLID 원칙 중 하나입니다. 직접 검색하고 알아보세요.
- 지금과 같이 logger가 loggerConfig를 통해 handler를 직접 호출하게 되면 logger를 호출하는 입장에서 handler를 통제하기 어려워집니다. handler에 대한 통제권은 loggerConfig에게 있지, logger를 초기화하는 입장에 있지 않기 때문이에요. 상위 레이어에게 handler라는 메소드에 대한 제어권을 주었지만 정작 제어권을 위임한 곳이 logger의 초기화 주체가 아니라 config가 되고 말았어요. 이는 별로 좋은 방법이 아닙니다.

## 그럼 적절한 방법은?
- 최상위의 진입점 레이어에 가까이 위치한 setup 레이어를 두고 여기서 파라미터 주입을 통해 유틸함수인 logger의 초기화를 끝내두도록 해보세요. 
- setup 레이어에서 logger 초기화에 필요한 것
  - 환경변수를 가져온다. (필요하다면 환경변수의 유무를 검사한다.) <- config의 역할
  - handler함수를 마련한다. (DB 연결 같은 초기화 과정이 필요하다면 이 과정 전에 끝내두도록 한다.)
  - 마련해둔 모든 설정 값들(loggerConfig)를 logger에 주입한다.
- 이후 setup 레이어보다 진입점으로부터 먼 곳에서는 부담없이 로그를 마구마구 사용한다.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-13T13:57:30Z

Review comments:
path: .env.development
user: login: pjb6510
body: .env파일은 반드시 .gitignore에서 명시하여 git에 올라가지 않게 하세요. 환경변수가 직접 레포지토리에 올라가는 건 큰 보안이슈가 됩니다.
created_at: 2025-04-08T20:40:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,102 @@
+import { LOG_LEVELS } from "@common/constants";
+import { LOGGER_CONFIG } from "@config/loggerConfig";
+
+const CURRENT_LOG_LEVEL = LOGGER_CONFIG.LEVEL;
+
+const getLogLevelName = (levelValue) => {
+  return (
+    Object.keys(LOG_LEVELS).find((key) => LOG_LEVELS[key] === levelValue) ||
+    "UNKNOWN"
+  );
+};
+
+const safeJsonStringify = (value, indent = 2) => {
+  try {
+    return JSON.stringify(value, null, indent);
+  } catch (e) {
+    return "(Cannot stringify value)";
+  }
+};
+
+const formatErrorArg = (errorObject) => {
+  if (!(errorObject instanceof Error)) {
+    return ""; // Error 객체가 아니라면 빈 문자열 반환
+  }
+
+  return `\nError:${errorObject.message}\nStack:${errorObject.stack}`;
+};
+const formatOtherArgs = (args) => {
+  if (!args || args.length === 0) {
+    return "";
+  }
+
+  return `\nArgs:${safeJsonStringify(args)}`;
+};
+
+const formatLogMessage = (level, message, args) => {
+  const timeStamp = new Date().toISOString();
+  const levelName = getLogLevelName(level);
+  const baseMessage = `[${timeStamp}] [${levelName}] ${message}`;
+
+  if (!args || args.length === 0) {
+    return baseMessage;
+  }
+
+  // Error 객체 찾기
+  const errorObject = args.find((arg) => arg instanceof Error);
+
+  // 요청한 로그의 레벨이 ERROR 이상이고 Error 객체가 존재한다면 에러 포멧팅 적용
+  if (level >= LOG_LEVELS.ERROR && errorObject) {
+    const otherArgs = args.filter((arg) => arg !== errorObject); // Error 객체가 아닌 나머지 인자 필터링
+    const formattedErrorMessage = formatErrorArg(errorObject);
+    const formattedOtherMessage = formatOtherArgs(otherArgs);
+
+    // 기본 메시지 + 에러 포멧 + 나머지 인자 포멧
+    return `${baseMessage}${formattedErrorMessage}${formattedOtherMessage}`;
+  }
+
+  const formattedArgsMessage = formatOtherArgs(args);
+  return `${baseMessage}${formattedArgsMessage}`;
+};
+
+const log = (level, message, ...args) => {
+  if (level < CURRENT_LOG_LEVEL) {
+    return;
+  }
+
+  const formattedMessage = formatLogMessage(level, message, args);
+
+  switch (level) {
+    case LOG_LEVELS.DEBUG:
+      console.debug(formattedMessage);
+      break;
+    case LOG_LEVELS.INFO:
+      console.info(formattedMessage);
+      break;
+    case LOG_LEVELS.WARN:
+      console.warn(formattedMessage);
+      break;
+    case LOG_LEVELS.ERROR:
+      console.error(formattedMessage);
+      break;
+    default:
+      console.log(formattedMessage); // 알 수 없는 경우는 기본 log 사용
+      break;
+  }
+};
path: src/common/utils/logger.js
user: login: pjb6510
body: .debug, .info, .warn 등 과 같은 동일한 인터페이스를 가진 객체를 인자로 받아서 유연하게 사용될 수 있도록 하면 어떨까요?
지금과 같은 형태로는 결국 console 메소드밖에 사용할 수 없습니다.
로거 함수는 환경에 따라서 콘솔 로그 외에도 file log를 남기거나 하는 경우도 왕왕 있습니다.
created_at: 2025-04-08T20:46:42Z
author_association: COLLABORATOR
path: .env.development
user: login: SangJin983
body: 환경변수에 대한 이해에 부족한 점이 있었습니다. (민감한 정보만 .local로 관리한다고 생각함)
즉시 반영하겠습니다.
created_at: 2025-04-09T06:25:46Z
author_association: OWNER
path: .env.development
user: login: SangJin983
body: 반영했습니다👌
created_at: 2025-04-09T08:06:31Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,102 @@
+import { LOG_LEVELS } from "@common/constants";
+import { LOGGER_CONFIG } from "@config/loggerConfig";
+
+const CURRENT_LOG_LEVEL = LOGGER_CONFIG.LEVEL;
+
+const getLogLevelName = (levelValue) => {
+  return (
+    Object.keys(LOG_LEVELS).find((key) => LOG_LEVELS[key] === levelValue) ||
+    "UNKNOWN"
+  );
+};
+
+const safeJsonStringify = (value, indent = 2) => {
+  try {
+    return JSON.stringify(value, null, indent);
+  } catch (e) {
+    return "(Cannot stringify value)";
+  }
+};
+
+const formatErrorArg = (errorObject) => {
+  if (!(errorObject instanceof Error)) {
+    return ""; // Error 객체가 아니라면 빈 문자열 반환
+  }
+
+  return `\nError:${errorObject.message}\nStack:${errorObject.stack}`;
+};
+const formatOtherArgs = (args) => {
+  if (!args || args.length === 0) {
+    return "";
+  }
+
+  return `\nArgs:${safeJsonStringify(args)}`;
+};
+
+const formatLogMessage = (level, message, args) => {
+  const timeStamp = new Date().toISOString();
+  const levelName = getLogLevelName(level);
+  const baseMessage = `[${timeStamp}] [${levelName}] ${message}`;
+
+  if (!args || args.length === 0) {
+    return baseMessage;
+  }
+
+  // Error 객체 찾기
+  const errorObject = args.find((arg) => arg instanceof Error);
+
+  // 요청한 로그의 레벨이 ERROR 이상이고 Error 객체가 존재한다면 에러 포멧팅 적용
+  if (level >= LOG_LEVELS.ERROR && errorObject) {
+    const otherArgs = args.filter((arg) => arg !== errorObject); // Error 객체가 아닌 나머지 인자 필터링
+    const formattedErrorMessage = formatErrorArg(errorObject);
+    const formattedOtherMessage = formatOtherArgs(otherArgs);
+
+    // 기본 메시지 + 에러 포멧 + 나머지 인자 포멧
+    return `${baseMessage}${formattedErrorMessage}${formattedOtherMessage}`;
+  }
+
+  const formattedArgsMessage = formatOtherArgs(args);
+  return `${baseMessage}${formattedArgsMessage}`;
+};
+
+const log = (level, message, ...args) => {
+  if (level < CURRENT_LOG_LEVEL) {
+    return;
+  }
+
+  const formattedMessage = formatLogMessage(level, message, args);
+
+  switch (level) {
+    case LOG_LEVELS.DEBUG:
+      console.debug(formattedMessage);
+      break;
+    case LOG_LEVELS.INFO:
+      console.info(formattedMessage);
+      break;
+    case LOG_LEVELS.WARN:
+      console.warn(formattedMessage);
+      break;
+    case LOG_LEVELS.ERROR:
+      console.error(formattedMessage);
+      break;
+    default:
+      console.log(formattedMessage); // 알 수 없는 경우는 기본 log 사용
+      break;
+  }
+};
path: src/common/utils/logger.js
user: login: SangJin983
body: 적용해봤습니다 리뷰 부탁드립니다!
created_at: 2025-04-09T12:44:48Z
author_association: OWNER

PR comments:
user: login: SangJin983
created_at: 2025-04-09T12:52:19Z
body: ##[Fix/Refactor] 환경변수 관리 문제 수정 및 logger 구조 수정
### 변경사항 요약
- `.gitignore`: `.env*`추가
- `consoleLogHandler.js`: log의 직접 구현을 인터페이스화시켜서 분리함
- `loggerConfig.js`: 환경에 따라서 핸들러를 변경할 수 있도록 HANDLER 속성 추가 (추후 환경에 따라 다르게 핸들러를 주입할 수 있도록 확장 가능)
- `logger.js`: 주입 받은 핸들러로 작동할 수 있도록 구조 개선
- 간단한 index작업 및 파일 위치 변경
- ### 구체적 구현
```src/config/loggerConfig.js
// ...

const currentLogHandler = consoleLogHandler; // <- 환경에 따라 핸들러를 유연하게 주입하기 위함 (추후 로직 개선)

export const LOGGER_CONFIG = {
  LEVEL: currentLogLevel,
  LEVEL_NAME: logLevelName,
  HANDLER: currentLogHandler, // <- 추가
};
// ...
```
```src/common/utils/logger/logger.js
// ...
const CURRENT_LOG_HANDLER = LOGGER_CONFIG.HANDLER;
// ...

// 직접 구현을 제거하고 인자를 주입하는 방식으로 수정
const log = (level, message, ...args) => {
  if (level < CURRENT_LOG_LEVEL) {
    return;
  }

  const formattedMessage = formatLogMessage(level, message, args);
  const levelName = getLogLevelName(level);

  // 주입된 핸들러의 handleLog 메서드 호출
  try {
    // 핸들러가 존재하고 handleLog 메서드가 있는지 확인
    if (
      CURRENT_LOG_HANDLER &&
      typeof CURRENT_LOG_HANDLER.handleLog === "function"
    ) {
      CURRENT_LOG_HANDLER.handleLog(level, levelName, formattedMessage);
    } else {
      // 핸들러가 없거나 잘못된 경우 대비
      console.error("Logger handler is not configured correctly.");
      console.log(`[${levelName}] ${formattedMessage}`);
    }
  } catch (handlerError) {
    // 핸들러 자체의 오류인 경우
    console.error("Logging handler failed:", handlerError);
    console.error("Original log message:", formattedMessage);
  }
};
// ...
```
```src/common/utils/logger/consoleLogHandler.js
export const consoleLogHandler = {
  /**
   * 로그 메시지를 콘솔에 출력 합니다.
   * @param {number} level - 로그 레벨 숫자 값 (LOG_LEVELS 참고)
   * @param {string} levelName - 로그 레벨 이름 ('DEBUG', 'INFO' 등)
   * @param {string} formattedMessage - 포맷팅된 로그 메시지
   */

  handleLog: (level, levelName, formattedMessage) => {
    // levelName 은 현재 직접 사용하지 않지만 인터페이스 유지를 위해 받음
    switch (level) {
      case LOG_LEVELS.DEBUG:
        console.debug(formattedMessage);
        break;
      case LOG_LEVELS.INFO:
        console.info(formattedMessage);
        break;
      case LOG_LEVELS.WARN:
        console.warn(formattedMessage);
        break;
      case LOG_LEVELS.ERROR:
        console.error(formattedMessage);
        break;
      default:
        console.log(`[${levelName}] ${formattedMessage}`); // 기본 console.log 에 levelName 추가
        break;
    }
  },
};
```
### 변경이유
- logger 함수가 추후 다양한 라이브러리나 동작을 반영할 수 있도록 구조를 개선함
author_association: OWNER
user: login: SangJin983
created_at: 2025-04-17T02:03:12Z
body: ## [Refactor] logger 에 의존성 주입 및 초기화 로직 개선
### 변경 사안 요약
- 기존의 'logger 객체가 의존성을 호출(선택)'하던 방식을 '의존성 주입(DI)을 통한 logger 인스터스 생성' 방식으로 수정
- logger.js 에서 바로 exoprt 하지 않고, main.jsx 에서 logger 인스턴스를 초기화(및 생성)하고 export 하는 방식으로 수정
### 변경 이유 (코드 리뷰 반영)
- 초기화 시점을 명확히 제어할 수 있습니다.
- 주입과 호출을 편의성의 측면에서만 생각해서 혼동이 있었습니다. (모듈을 통해 선택하는 것이 같은 효과라고 잘못 인지함) logger를 사용하는 측에서 주입할 의존성을 선택할 수 있다면, (기존에는 loggerConfig를 수정해야 했던것과 달리) loggerConfig나 logger.js의 내부를 수정하지 않고도, 유연하게 대응할 수 있습니다. 
### 세부 구현
- loggerConfig:
```src/config/loggerConfig.js
import { LOG_LEVELS } from "@common/constants";

const getLogLevelFromEnv = () => {
  const logLevelName =
    import.meta.env.VITE_LOG_LEVEL || (import.meta.env.PROD ? "INFO" : "DEBUG");

  return LOG_LEVELS[logLevelName.toUpperCase()] ?? LOG_LEVELS.DEBUG;
};

const getLogLevelNameFromEnv = () => {
  return (
    import.meta.env.VITE_LOG_LEVEL || (import.meta.env.PROD ? "INFO" : "DEBUG")
  );
};

export const getLoggerLevel = getLogLevelFromEnv;
export const getLoggerLevelName = getLogLevelNameFromEnv;

```
```
- 초기화 역할을 맡던 부분(handler를 내부에서 선택하는)을 제거했습니다.
- 초기화 시 정확한 정보(ex.환경 변수의 적용)를 불러올 수 있게, 함수의 형태로 반환했습니다
```
- logger:
```src/common/utils/logger/logger.js
// --- 제거 ---
// const CURRENT_LOG_LEVEL = LOGGER_CONFIG.LEVEL;
// const CURRENT_LOG_HANDLER = LOGGER_CONFIG.HANDLER;

// ... 유틸함수 등은 그대로 사용
// --- 로거 생성 함수 ---
/**
 * 주입 받은 설정으로 로거 인스턴스를 생성합니다.
 * @param {object} config - 로거 설정 객체
 * @param {number} config.level - 로그 레벨 (숫자)
 * @param {object} config.handler - 로그 핸들러 객체 (handleLog 메서드 필요)
 * @return {object} - logger 인스턴스 (debug, info, warn, error 메서드 포함)
 */
const createLogger = (config) => {
  if (typeof config?.level !== "number" || !config?.handler?.handleLog) {
    console.error(
      "유효 하지 않은 로거 설정이 주입되었습니다. 기본 콘솔 로거를 사용합니다.",
      config
    );
    return {
      debug: console.debug,
      info: console.info,
      warn: console.warn,
      error: console.error,
    };
  }

  const CURRENT_LOG_LEVEL = config.level;
  const CURRENT_LOG_HANDLER = config.handler;
// ...

    try {
      // 설정에서 주입 받은 핸들러 사용
      CURRENT_LOG_HANDLER.handleLog(level, levelName, formattedMessage);
    } catch (handlerError) {
// ...
```
```
- logger 내부에서 핸들러를 전역적으로 설정하던 것을 제거하고 매개변수에서 주입받아 처리하게 만들었습니다
```
- main.jsx:
```src/app/main.jsx
// ...
// --- 로거 설정 및 초기화 ---
const logLevel = getLoggerLevel();
const logHandler = consoleLogHandler;
const appLogger = createLogger({ level: logLevel, handler: logHandler });

export const logger = appLogger;

// --- 애플리케이션 렌더링 ---
createRoot(document.getElementById("root")).render( // ...
```
```
- main.jsx에서 초기화 작업을 하는 이유는, 이전 프로젝트에서도 초기화를 main 이나 app 에서 했던 기억이 있어서 이렇게 해봤습니다.
- setup.js 나 이런 걸 만들어서 Init()을 호출 하는 방식도 가능할 것 같은데, 그정도로 복잡하지는 않은 것 같아서 이런 식으로 해봤습니다.
- main 자체가 provider 제공이라든가 하는 app 을 렌더링 한다든가 하는 작업들이 초기화에 맞다고 생각합니다.
```
### 참고자료
- [제어권 역전(IoC)](https://ko.wikipedia.org/wiki/%EC%A0%9C%EC%96%B4_%EB%B0%98%EC%A0%84)
author_association: OWNER
user: login: SangJin983
created_at: 2025-04-17T07:21:20Z
body: ## [Fix] 순환참조 문제 해결
author_association: OWNER