package main import ( "log" "os" "os/signal" "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" "a301_server/internal/server" _ "a301_server/docs" // swagger docs "github.com/tolelom/tolchain/core" "a301_server/pkg/config" "a301_server/pkg/database" "a301_server/pkg/middleware" "a301_server/routes" ) // @title One of the Plans API // @version 1.0 // @description 멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API // @host a301.api.tolelom.xyz // @BasePath / // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization // @description JWT Bearer 토큰 (예: Bearer eyJhbGci...) // @securityDefinitions.apikey ApiKeyAuth // @in header // @name X-API-Key // @description 내부 API 키 (게임 서버 ↔ API 서버 통신용) func main() { config.Load() config.WarnInsecureDefaults() db, err := database.ConnectMySQL() if err != nil { log.Fatalf("MySQL 연결 실패: %v", err) } log.Println("MySQL 연결 성공") // AutoMigrate if err := db.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &bossraid.DedicatedServer{}, &bossraid.RoomSlot{}, &bossraid.RewardFailure{}, &player.PlayerProfile{}); err != nil { log.Fatalf("AutoMigrate 실패: %v", err) } rdb, err := database.ConnectRedis() if err != nil { log.Fatalf("Redis 연결 실패: %v", err) } log.Println("Redis 연결 성공") // ── 의존성 주입 ────────────────────────────────────────────────── authRepo := auth.NewRepository(db) authSvc := auth.NewService(authRepo, rdb) authHandler := auth.NewHandler(authSvc) chainClient := chain.NewClient(config.C.ChainNodeURLs...) chainRepo := chain.NewRepository(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) // Migrate v1 wallets to v2 (HKDF per-wallet keys) if err := chainSvc.MigrateWalletKeys(); err != nil { log.Fatalf("wallet key migration failed: %v", err) } userResolver := func(username string) (uint, error) { user, err := authRepo.FindByUsername(username) if err != nil { return 0, err } return user.ID, nil } chainSvc.SetUserResolver(userResolver) authSvc.SetWalletCreator(func(userID uint) error { _, err := chainSvc.CreateWallet(userID) return err }) chainSvc.SetPasswordVerifier(authSvc.VerifyPassword) playerRepo := player.NewRepository(db) playerSvc := player.NewService(playerRepo) playerSvc.SetUserResolver(userResolver) playerHandler := player.NewHandler(playerSvc) authSvc.SetProfileCreator(func(userID uint) error { return playerSvc.CreateProfile(userID) }) 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) } brRepo := bossraid.NewRepository(db) brSvc := bossraid.NewService(brRepo, rdb) brSvc.SetRewardGranter(func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) { result, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets) if result != nil { return result.TxID, err } return "", err }) brSvc.SetExpGranter(func(username string, exp int) error { return playerSvc.GrantExperienceByUsername(username, exp) }) brHandler := bossraid.NewHandler(brSvc) if config.C.InternalAPIKey == "" { log.Println("WARN: INTERNAL_API_KEY not set — /api/internal/* endpoints are disabled") } annRepo := announcement.NewRepository(db) annSvc := announcement.NewService(annRepo) annHandler := announcement.NewHandler(annSvc) dlRepo := download.NewRepository(db) dlSvc := download.NewService(dlRepo, config.C.GameDir) dlHandler := download.NewHandler(dlSvc, config.C.BaseURL) // ── 서버 + 라우트 설정 ─────────────────────────────────────────── app := server.New() authMw := middleware.Auth(rdb, config.C.JWTSecret) serverAuthMw := middleware.ServerAuth(config.C.InternalAPIKey) idempotencyReqMw := middleware.IdempotencyRequired(rdb) routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, server.AuthLimiter(), server.APILimiter(), server.HealthCheck(), server.ReadyCheck(db, rdb), server.ChainUserLimiter(), authMw, serverAuthMw, idempotencyReqMw, server.RefreshLimiter()) // ── 백그라운드 워커 ────────────────────────────────────────────── go func() { ticker := time.NewTicker(15 * time.Second) defer ticker.Stop() for range ticker.C { brSvc.CheckStaleSlots() } }() rewardWorker := bossraid.NewRewardWorker( brRepo, func(username string, tokenAmount uint64, assets []core.MintAssetPayload) (string, error) { result, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets) if result != nil { return result.TxID, err } return "", err }, func(username string, exp int) error { return playerSvc.GrantExperienceByUsername(username, exp) }, func(txID string) (bool, error) { result, err := chainClient.GetTxStatus(txID) if err != nil { return false, err } return result != nil && result.Success, nil }, ) rewardWorker.Start() // ── Graceful shutdown ──────────────────────────────────────────── go func() { if err := app.Listen(":" + config.C.AppPort); err != nil { log.Printf("서버 Listen 종료: %v", err) } }() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) sig := <-sigCh log.Printf("수신된 시그널: %v — 서버 종료 중...", sig) rewardWorker.Stop() if err := app.ShutdownWithTimeout(10 * time.Second); err != nil { log.Printf("서버 종료 실패: %v", err) } if rdb != nil { if err := rdb.Close(); err != nil { log.Printf("Redis 종료 실패: %v", err) } else { log.Println("Redis 연결 종료 완료") } } if sqlDB, err := db.DB(); err == nil { if err := sqlDB.Close(); err != nil { log.Printf("MySQL 종료 실패: %v", err) } else { log.Println("MySQL 연결 종료 완료") } } log.Println("서버 종료 완료") }