using System.IO; using System.Text.RegularExpressions; using LM = M.Framework.LogManager.LogManager; namespace SheetMe.Designer.Services; /// /// 애플리케이션 로그 — M.Framework.LogManager 배선. /// /// 원칙 셋: /// /// 모든 호출을 try/catch 로 감싼다 — 로깅 실패가 업무를 막으면 안 된다. /// 기록 직전 를 1회 통과시킨다 — 접속 문자열을 값으로 들고 다니므로 /// 예외 메시지에 자격증명이 섞일 수 있다. /// 로그 경로 확보에 실패하면 %LocalAppData% 로 폴백한다(ThemeManager 가 이미 쓰는 경로). /// /// public static class AppLog { #region Member Fields private static readonly Lazy logPath = new(ResolveLogPath); /// 접속 문자열 자격증명 — 로그·화면 어디에도 남기지 않는다 private static readonly Regex SecretPattern = new( @"(?Password|Pwd|User\s*Id|UserId)\s*=\s*[^;""\r\n]*", RegexOptions.IgnoreCase | RegexOptions.Compiled); #endregion #region Properties /// 로그 폴더 — 실행 폴더\logs\Designer, 쓰기 불가 시 %LocalAppData%\SheetMe\logs\Designer public static string LogPath => logPath.Value; #endregion #region Methods /// 정보 기록 public static void Info(string message) => Write(LM.LogType.INFO, message); /// 경고 기록 public static void Warn(string message) => Write(LM.LogType.WARNING, message); /// 오류 기록 — 오류 코드를 돌려주고, 같은 코드가 로그 줄머리에 찍힌다 /// 사용자에게 안내할 짧은 오류 코드(전화 한 통으로 로그를 특정할 수 있게) public static string Error(string context, Exception exception) { var code = NewErrorCode(); Write(LM.LogType.ERROR, $"[{code}] {context}{Environment.NewLine}{exception}"); return code; } /// 보안·감사성 이벤트(로그인, 권한 거부, DB 쓰기) public static void Audit(string message) => Write(LM.LogType.FIXED, message); /// /// 기동 시 1회 — 기록할 최소 로그 레벨을 지정한다. /// LogLevel 은 누적형이다(DEBUG ⊃ INFO ⊃ WARNING ⊃ ERROR). FIXED 는 어느 레벨에서도 남는다. /// /// 함정: LogLevel 은 열거형이 아니라 문자열 속성이라 오타를 컴파일러가 못 잡는다. /// 유효하지 않은 값이 들어가면 FIXED 만 기록되고 나머지가 전부 조용히 사라진다. /// 그래서 LogType 열거값의 이름으로만 지정한다. /// public static void Initialize(bool verbose = false) { try { LM.LogLevel = (verbose ? LM.LogType.DEBUG : LM.LogType.INFO).ToString(); } catch { // 레벨 설정 실패는 기본 동작으로 진행 } } /// 자격증명 마스킹 — 예외 메시지·설정 덤프가 로그로 나가기 전 마지막 방어선 public static string Redact(string text) => string.IsNullOrEmpty(text) ? text : SecretPattern.Replace(text, "${key}=***"); private static void Write(LM.LogType level, string message) { try { LM.LogWrite(level, LogPath, Redact(message)); } catch { // 로깅 실패는 업무를 막지 않는다 } } /// 오류 코드 — 사용자가 읽어줄 수 있을 만큼 짧게(시각 기반이라 로그에서 바로 찾힌다) private static string NewErrorCode() => DateTime.Now.ToString("MMddHHmmss"); private static string ResolveLogPath() { var candidates = new[] { Path.Combine(AppContext.BaseDirectory, "logs", "Designer"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SheetMe", "logs", "Designer"), }; foreach (var candidate in candidates) { try { Directory.CreateDirectory(candidate); // 실제로 쓸 수 있는지까지 확인한다 — Program Files 하위는 만들어져도 쓰기가 막힐 수 있다 var probe = Path.Combine(candidate, ".writeprobe"); File.WriteAllText(probe, string.Empty); File.Delete(probe); return candidate; } catch { // 다음 후보로 } } return Path.GetTempPath(); } #endregion }