package userprogram import ( "fmt" "os" "path/filepath" "regexp" "strconv" "time" ) const ( DefaultUpdateDir = "/update_data" DefaultLogDir = "/log" DefaultSchema = "public" DefaultInitialCSV = "/initial_data/user_program_info_init_20251208.csv" DefaultTable = "user_program_info" DefaultDatabase = "user_program_info" defaultTargetRange = "20060102" ) type MySQLConfig struct { Host string Port int User string Password string Database string Table string } type Paths struct { UpdateDir string LogDir string InitialCSV string Schema string } func NewMySQLConfigFromEnv() (MySQLConfig, error) { port, err := strconv.Atoi(env("USER_PROGRAM_INFO_PORT", "3306")) if err != nil { return MySQLConfig{}, fmt.Errorf("invalid USER_PROGRAM_INFO_PORT: %w", err) } host, err := envRequiredValue("USER_PROGRAM_INFO_HOST") if err != nil { return MySQLConfig{}, err } user, err := envRequiredValue("USER_PROGRAM_INFO_USERNAME") if err != nil { return MySQLConfig{}, err } password, err := envRequiredValue("USER_PROGRAM_INFO_PASSWORD") if err != nil { return MySQLConfig{}, err } cfg := MySQLConfig{ Host: host, Port: port, User: user, Password: password, Database: env("USER_PROGRAM_INFO_DB", DefaultDatabase), Table: env("USER_PROGRAM_INFO_TABLE", DefaultTable), } if cfg.Host == "" || cfg.User == "" || cfg.Password == "" { return MySQLConfig{}, fmt.Errorf("mysql connection envs are required") } return cfg, nil } func NewPathsFromEnv() (Paths, error) { paths := Paths{ UpdateDir: env("USER_PROGRAM_UPDATE_DIR", DefaultUpdateDir), LogDir: env("USER_PROGRAM_IMPORT_LOG_DIR", DefaultLogDir), InitialCSV: env("USER_PROGRAM_INFO_CSV", DefaultInitialCSV), Schema: env("USER_PROGRAM_INFO_SCHEMA", DefaultSchema), } for _, dir := range []string{paths.UpdateDir, paths.LogDir} { if dir == "" { continue } if err := os.MkdirAll(dir, 0o755); err != nil { return Paths{}, fmt.Errorf("create dir %s: %w", dir, err) } } return paths, nil } func ParseTargetDate(raw string) (time.Time, error) { if raw == "" { return yesterdayKST(), nil } t, err := time.ParseInLocation("2006-01-02", raw, kst()) if err != nil { return time.Time{}, fmt.Errorf("invalid date %q (expected YYYY-MM-DD)", raw) } return t, nil } func DateFromFilename(path string) (time.Time, error) { base := filepath.Base(path) re := regexp.MustCompile(`(\d{8})`) match := re.FindStringSubmatch(base) if len(match) < 2 { return time.Time{}, fmt.Errorf("no date in filename: %s", base) } return time.ParseInLocation(defaultTargetRange, match[1], kst()) } func yesterdayKST() time.Time { now := time.Now().In(kst()) yesterday := now.AddDate(0, 0, -1) return time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, kst()) } func kst() *time.Location { loc, err := time.LoadLocation("Asia/Seoul") if err != nil { return time.FixedZone("KST", 9*60*60) } return loc } func env(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func envRequiredValue(key string) (string, error) { v := os.Getenv(key) if v == "" { return "", fmt.Errorf("%s is required", key) } return v, nil }