Files
a301_server/main.go
tolelom befea9dd68
Some checks failed
Server CI/CD / lint-and-build (push) Failing after 12m3s
Server CI/CD / deploy (push) Has been cancelled
feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
- swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개)
- /swagger/ 경로에 Swagger UI 제공
- 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋)
- 플레이어 레벨/경험치 시스템 및 스탯 성장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 17:51:37 +09:00

258 lines
8.2 KiB
Go

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"
_ "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"
"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"
)
// @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()
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{}, &bossraid.DedicatedServer{}, &bossraid.RoomSlot{}, &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
})
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(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)
// Background: stale dedicated server detection
go func() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for range ticker.C {
brSvc.CheckStaleSlots()
}
}()
// 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))
}