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
}