package main import ( "log" "os" "os/signal" "strconv" "syscall" "time" "a301_server/internal/announcement" "a301_server/internal/auth" "a301_server/internal/bossraid" "a301_server/internal/chain" "a301_server/internal/download" "a301_server/internal/player" "github.com/tolelom/tolchain/core" "a301_server/pkg/config" "a301_server/pkg/database" "a301_server/pkg/middleware" "a301_server/routes" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/limiter" "github.com/gofiber/fiber/v2/middleware/logger" ) func main() { config.Load() config.WarnInsecureDefaults() if err := database.ConnectMySQL(); err != nil { log.Fatalf("MySQL 연결 실패: %v", err) } log.Println("MySQL 연결 성공") // AutoMigrate if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &player.PlayerProfile{}); err != nil { log.Fatalf("AutoMigrate 실패: %v", err) } if err := database.ConnectRedis(); err != nil { log.Fatalf("Redis 연결 실패: %v", err) } log.Println("Redis 연결 성공") // 의존성 주입 authRepo := auth.NewRepository(database.DB) authSvc := auth.NewService(authRepo, database.RDB) authHandler := auth.NewHandler(authSvc) // Chain (blockchain integration) chainClient := chain.NewClient(config.C.ChainNodeURL) chainRepo := chain.NewRepository(database.DB) chainSvc, err := chain.NewService(chainRepo, chainClient, config.C.ChainID, config.C.OperatorKeyHex, config.C.WalletEncryptionKey) if err != nil { log.Fatalf("chain service init failed: %v", err) } chainHandler := chain.NewHandler(chainSvc) // username → userID 변환 (게임 서버 내부 API용) chainSvc.SetUserResolver(func(username string) (uint, error) { user, err := authRepo.FindByUsername(username) if err != nil { return 0, err } return user.ID, nil }) // 회원가입 시 블록체인 월렛 자동 생성 authSvc.SetWalletCreator(func(userID uint) error { _, err := chainSvc.CreateWallet(userID) return err }) // Player Profile playerRepo := player.NewRepository(database.DB) playerSvc := player.NewService(playerRepo) playerSvc.SetUserResolver(func(username string) (uint, error) { user, err := authRepo.FindByUsername(username) if err != nil { return 0, err } return user.ID, nil }) playerHandler := player.NewHandler(playerSvc) // 회원가입 시 플레이어 프로필 자동 생성 authSvc.SetProfileCreator(func(userID uint) error { return playerSvc.CreateProfile(userID) }) // 초기 admin 계정 생성 (콜백 등록 후 실행) if err := authSvc.EnsureAdmin(config.C.AdminUsername, config.C.AdminPassword); err != nil { log.Printf("admin 계정 생성 실패: %v", err) } else { log.Printf("admin 계정 확인 완료: %s", config.C.AdminUsername) } // Boss Raid brRepo := bossraid.NewRepository(database.DB) brSvc := bossraid.NewService(brRepo, database.RDB) brSvc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error { _, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets) return err }) brHandler := bossraid.NewHandler(brSvc) if config.C.InternalAPIKey == "" { log.Println("WARN: INTERNAL_API_KEY not set — /api/internal/* endpoints are disabled") } annRepo := announcement.NewRepository(database.DB) annSvc := announcement.NewService(annRepo) annHandler := announcement.NewHandler(annSvc) dlRepo := download.NewRepository(database.DB) dlSvc := download.NewService(dlRepo, config.C.GameDir) dlHandler := download.NewHandler(dlSvc, config.C.BaseURL) app := fiber.New(fiber.Config{ StreamRequestBody: true, BodyLimit: 4 * 1024 * 1024 * 1024, // 4GB }) app.Use(middleware.RequestID) app.Use(logger.New(logger.Config{ Format: `{"time":"${time}","status":${status},"latency":"${latency}","method":"${method}","path":"${path}","ip":"${ip}","reqId":"${locals:requestID}"}` + "\n", TimeFormat: "2006-01-02T15:04:05Z07:00", })) app.Use(middleware.SecurityHeaders) app.Use(cors.New(cors.Config{ AllowOrigins: "https://a301.tolelom.xyz", AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key", AllowMethods: "GET, POST, PUT, PATCH, DELETE", AllowCredentials: true, })) // Rate limiting: 인증 관련 엔드포인트 (로그인/회원가입/리프레시) authLimiter := limiter.New(limiter.Config{ Max: 10, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "요청이 너무 많습니다. 잠시 후 다시 시도해주세요"}) }, }) // Rate limiting: 일반 API apiLimiter := limiter.New(limiter.Config{ Max: 60, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "요청이 너무 많습니다. 잠시 후 다시 시도해주세요"}) }, }) // Health check handlers healthCheck := func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) } readyCheck := func(c *fiber.Ctx) error { sqlDB, err := database.DB.DB() if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db pool"}) } if err := sqlDB.Ping(); err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "db"}) } if err := database.RDB.Ping(c.Context()).Err(); err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"status": "error", "detail": "redis"}) } return c.JSON(fiber.Map{"status": "ok"}) } // Rate limiting: 체인 트랜잭션 (유저별 분당 20회) chainUserLimiter := limiter.New(limiter.Config{ Max: 20, Expiration: 1 * time.Minute, KeyGenerator: func(c *fiber.Ctx) string { if uid, ok := c.Locals("userID").(uint); ok { return "chain_user:" + strconv.FormatUint(uint64(uid), 10) } return "chain_ip:" + c.IP() }, LimitReached: func(c *fiber.Ctx) error { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{"error": "트랜잭션 요청이 너무 많습니다. 잠시 후 다시 시도해주세요"}) }, }) routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, authLimiter, apiLimiter, healthCheck, readyCheck, chainUserLimiter) // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh log.Printf("수신된 시그널: %v — 서버 종료 중...", sig) if err := app.ShutdownWithTimeout(10 * time.Second); err != nil { log.Printf("서버 종료 실패: %v", err) } // Redis 연결 정리 if database.RDB != nil { if err := database.RDB.Close(); err != nil { log.Printf("Redis 종료 실패: %v", err) } else { log.Println("Redis 연결 종료 완료") } } // MySQL 연결 정리 if sqlDB, err := database.DB.DB(); err == nil { if err := sqlDB.Close(); err != nil { log.Printf("MySQL 종료 실패: %v", err) } else { log.Println("MySQL 연결 종료 완료") } } }() log.Fatal(app.Listen(":" + config.C.AppPort)) }