package config import ( "os" "strconv" "strings" "github.com/joho/godotenv" ) // Config holds all runtime settings, loaded from environment (.env optional). type Config struct { // Slack SlackBotToken string DefaultSlackChannel string // 모든 이벤트 브로드캐스트 채널 MonitorChannel string // 에러(slog.Error) 발생 시 알림을 보낼 채널 (예: monitor-dev) // Gitea GiteaWebhookSecret string GiteaBroadcast bool // true면 모든 Gitea 이벤트를 DefaultSlackChannel로 브로드캐스트(기본 off, DM만) // Notion NotionVerificationToken string NotionAPIToken string NotionAssigneeProperties []string // 알림 대상이 되는 People 속성들 (담당자/처리자/참조인원 등) NotionSnapshotFile string // 페이지 스냅샷 저장 파일 (이전→현재 diff용). 비우면 메모리만 // App MappingFile string Addr string // Logging Env string // development | production LogLevel string // debug | info | warn | error LogFormat string // text | json LogFile string // optional file path; empty = stdout only (날짜별 회전의 기준 경로) LogRetentionDays int // 날짜별 로그 파일 보존 일수 (기본 7) } // Load reads configuration for the given environment and loads its .env files. // // The environment is decided by the run command (the -env flag passed as // envOverride). If empty, it falls back to the APP_ENV variable, then // "development". This keeps env selection command-driven, not ambient. // // Precedence: an earlier-loaded file wins (godotenv.Load never overrides an // already-set variable), and missing files are ignored: // // .env.{env}.local # 개인 비밀값 (git 제외, 선택) // .env.{env} # 환경별 설정 (.env.dev / .env.prod) // .env # 공통 기본값 (fallback) func Load(envOverride string) Config { env := envOverride if env == "" { env = getenv("APP_ENV", "dev") } env = normalizeEnv(env) _ = godotenv.Load(".env." + env + ".local") _ = godotenv.Load(".env." + env) _ = godotenv.Load(".env") return Config{ SlackBotToken: os.Getenv("SLACK_BOT_TOKEN"), DefaultSlackChannel: os.Getenv("DEFAULT_SLACK_CHANNEL"), MonitorChannel: os.Getenv("MONITOR_SLACK_CHANNEL"), GiteaWebhookSecret: os.Getenv("GITEA_WEBHOOK_SECRET"), GiteaBroadcast: getenvBool("GITEA_BROADCAST", false), NotionVerificationToken: os.Getenv("NOTION_VERIFICATION_TOKEN"), NotionAPIToken: os.Getenv("NOTION_API_TOKEN"), NotionAssigneeProperties: getenvList("NOTION_ASSIGNEE_PROPERTY", "담당자,처리자,참조인원"), NotionSnapshotFile: getenv("NOTION_SNAPSHOT_FILE", "data/notion-snapshots.json"), MappingFile: getenv("MAPPING_FILE", "data/mappings.json"), Addr: getenv("ADDR", ":8000"), Env: env, LogLevel: getenv("LOG_LEVEL", defaultLogLevel(env)), LogFormat: getenv("LOG_FORMAT", defaultLogFormat(env)), LogFile: getenv("LOG_FILE", defaultLogFile(env)), LogRetentionDays: getenvInt("LOG_RETENTION_DAYS", 7), } } // normalizeEnv canonicalizes the env name to the short form "dev" or "prod". // Long forms (development/production) and unknown values map sensibly. func normalizeEnv(s string) string { switch strings.ToLower(strings.TrimSpace(s)) { case "prod", "production": return "prod" default: return "dev" } } // IsProduction reports whether the app runs in the production environment. func (c Config) IsProduction() bool { return c.Env == "prod" } func defaultLogLevel(env string) string { if env == "prod" { return "info" } return "debug" } func defaultLogFormat(env string) string { if env == "prod" { return "json" } return "text" } // defaultLogFile decides where logs are written when LOG_FILE is unset: // - dev → "" (콘솔/IDE 출력만) // - prod → 파일로 추출 (logs/app.log) func defaultLogFile(env string) string { if env == "prod" { return "logs/app.log" } return "" } func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // getenvList는 콤마로 구분된 값을 트림해 슬라이스로 돌려준다(빈 항목 제거). // 미설정 시 fallback(역시 콤마 구분)을 파싱한다. func getenvList(key, fallback string) []string { v := getenv(key, fallback) parts := strings.Split(v, ",") out := make([]string, 0, len(parts)) for _, p := range parts { if s := strings.TrimSpace(p); s != "" { out = append(out, s) } } return out } func getenvInt(key string, fallback int) int { if v := strings.TrimSpace(os.Getenv(key)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback } // getenvBool은 on/off, true/false, 1/0, yes/no를 모두 받는다. 미설정/인식불가 시 fallback. func getenvBool(key string, fallback bool) bool { switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) { case "1", "true", "t", "yes", "y", "on": return true case "0", "false", "f", "no", "n", "off": return false default: return fallback } }