feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
Some checks failed
Server CI/CD / lint-and-build (push) Failing after 12m3s
Server CI/CD / deploy (push) Has been cancelled

- swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개)
- /swagger/ 경로에 Swagger UI 제공
- 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋)
- 플레이어 레벨/경험치 시스템 및 스탯 성장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 17:51:37 +09:00
parent ee2cf332fb
commit befea9dd68
19 changed files with 12692 additions and 62 deletions

4121
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

4097
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2686
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

348
docs/swagger_types.go Normal file
View File

@@ -0,0 +1,348 @@
// Package docs contains Swagger DTO types for API documentation.
// These types are only used by swag to generate OpenAPI specs.
package docs
import "time"
// --- Common ---
// ErrorResponse is a standard error response.
type ErrorResponse struct {
Error string `json:"error" example:"오류 메시지"`
}
// MessageResponse is a standard success message response.
type MessageResponse struct {
Message string `json:"message" example:"성공"`
}
// StatusResponse is a simple status response.
type StatusResponse struct {
Status string `json:"status" example:"ok"`
}
// --- Auth ---
type RegisterRequest struct {
Username string `json:"username" example:"player1"`
Password string `json:"password" example:"mypassword"`
}
type LoginRequest struct {
Username string `json:"username" example:"player1"`
Password string `json:"password" example:"mypassword"`
}
type LoginResponse struct {
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."`
Username string `json:"username" example:"player1"`
Role string `json:"role" example:"user"`
}
type RefreshRequest struct {
RefreshToken string `json:"refreshToken,omitempty"`
}
type RefreshResponse struct {
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."`
}
type UpdateRoleRequest struct {
Role string `json:"role" example:"admin"`
}
type VerifyTokenRequest struct {
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."`
}
type VerifyTokenResponse struct {
Username string `json:"username" example:"player1"`
}
type SSAFYLoginURLResponse struct {
URL string `json:"url" example:"https://edu.ssafy.com/oauth/authorize?..."`
}
type SSAFYCallbackRequest struct {
Code string `json:"code" example:"auth_code_123"`
State string `json:"state" example:"random_state_string"`
}
type LaunchTicketResponse struct {
Ticket string `json:"ticket" example:"ticket_abc123"`
}
type RedeemTicketRequest struct {
Ticket string `json:"ticket" example:"ticket_abc123"`
}
type RedeemTicketResponse struct {
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."`
}
// UserResponse is a user in the admin user list.
type UserResponse struct {
ID uint `json:"id" example:"1"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Username string `json:"username" example:"player1"`
Role string `json:"role" example:"user"`
SsafyID *string `json:"ssafyId,omitempty" example:"ssafy_123"`
}
// --- Announcement ---
type AnnouncementResponse struct {
ID uint `json:"id" example:"1"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Title string `json:"title" example:"서버 점검 안내"`
Content string `json:"content" example:"3월 16일 서버 점검이 예정되어 있습니다."`
}
type CreateAnnouncementRequest struct {
Title string `json:"title" example:"서버 점검 안내"`
Content string `json:"content" example:"3월 16일 서버 점검이 예정되어 있습니다."`
}
type UpdateAnnouncementRequest struct {
Title string `json:"title,omitempty" example:"수정된 제목"`
Content string `json:"content,omitempty" example:"수정된 내용"`
}
// --- Download ---
type DownloadInfoResponse struct {
ID uint `json:"id" example:"1"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
URL string `json:"url" example:"https://a301.api.tolelom.xyz/api/download/file"`
Version string `json:"version" example:"1.0.0"`
FileName string `json:"fileName" example:"A301_v1.0.zip"`
FileSize string `json:"fileSize" example:"1.5 GB"`
FileHash string `json:"fileHash" example:"a1b2c3d4e5f6..."`
LauncherURL string `json:"launcherUrl" example:"https://a301.api.tolelom.xyz/api/download/launcher"`
LauncherSize string `json:"launcherSize" example:"25.3 MB"`
}
// --- Chain ---
type WalletInfoResponse struct {
Address string `json:"address" example:"1a2b3c4d5e6f..."`
PubKeyHex string `json:"pubKeyHex" example:"abcdef012345..."`
}
type TransferRequest struct {
To string `json:"to" example:"1a2b3c4d5e6f..."`
Amount uint64 `json:"amount" example:"100"`
}
type TransferAssetRequest struct {
AssetID string `json:"assetId" example:"asset_001"`
To string `json:"to" example:"1a2b3c4d5e6f..."`
}
type ListOnMarketRequest struct {
AssetID string `json:"assetId" example:"asset_001"`
Price uint64 `json:"price" example:"500"`
}
type BuyFromMarketRequest struct {
ListingID string `json:"listingId" example:"listing_001"`
}
type CancelListingRequest struct {
ListingID string `json:"listingId" example:"listing_001"`
}
type EquipItemRequest struct {
AssetID string `json:"assetId" example:"asset_001"`
Slot string `json:"slot" example:"weapon"`
}
type UnequipItemRequest struct {
AssetID string `json:"assetId" example:"asset_001"`
}
type MintAssetRequest struct {
TemplateID string `json:"templateId" example:"sword_template"`
OwnerPubKey string `json:"ownerPubKey" example:"abcdef012345..."`
Properties map[string]any `json:"properties"`
}
type GrantRewardRequest struct {
RecipientPubKey string `json:"recipientPubKey" example:"abcdef012345..."`
TokenAmount uint64 `json:"tokenAmount" example:"1000"`
Assets []MintAssetPayload `json:"assets"`
}
type RegisterTemplateRequest struct {
ID string `json:"id" example:"sword_template"`
Name string `json:"name" example:"Sword"`
Schema map[string]any `json:"schema"`
Tradeable bool `json:"tradeable" example:"true"`
}
type InternalGrantRewardRequest struct {
Username string `json:"username" example:"player1"`
TokenAmount uint64 `json:"tokenAmount" example:"1000"`
Assets []MintAssetPayload `json:"assets"`
}
type InternalMintAssetRequest struct {
TemplateID string `json:"templateId" example:"sword_template"`
Username string `json:"username" example:"player1"`
Properties map[string]any `json:"properties"`
}
type MintAssetPayload struct {
TemplateID string `json:"template_id" example:"sword_template"`
Owner string `json:"owner" example:"abcdef012345..."`
Properties map[string]any `json:"properties"`
}
// --- Boss Raid ---
type RequestEntryRequest struct {
Usernames []string `json:"usernames" example:"player1,player2"`
BossID int `json:"bossId" example:"1"`
}
type RequestEntryAuthRequest struct {
Usernames []string `json:"usernames,omitempty"`
BossID int `json:"bossId" example:"1"`
}
type RequestEntryResponse struct {
RoomID uint `json:"roomId" example:"1"`
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
BossID int `json:"bossId" example:"1"`
Players []string `json:"players"`
Status string `json:"status" example:"waiting"`
EntryToken string `json:"entryToken,omitempty" example:"token_abc"`
}
type InternalRequestEntryResponse struct {
RoomID uint `json:"roomId" example:"1"`
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
BossID int `json:"bossId" example:"1"`
Players []string `json:"players"`
Status string `json:"status" example:"waiting"`
Tokens map[string]string `json:"tokens"`
}
type SessionNameRequest struct {
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
}
type RoomStatusResponse struct {
RoomID uint `json:"roomId" example:"1"`
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
Status string `json:"status" example:"in_progress"`
}
type PlayerReward struct {
Username string `json:"username" example:"player1"`
TokenAmount uint64 `json:"tokenAmount" example:"100"`
Assets []MintAssetPayload `json:"assets"`
Experience int `json:"experience" example:"500"`
}
type CompleteRaidRequest struct {
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
Rewards []PlayerReward `json:"rewards"`
}
type CompleteRaidResponse struct {
RoomID uint `json:"roomId" example:"1"`
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
Status string `json:"status" example:"completed"`
RewardResults []RewardResult `json:"rewardResults"`
}
type RewardResult struct {
Username string `json:"username" example:"player1"`
Success bool `json:"success" example:"true"`
Error string `json:"error,omitempty"`
}
type ValidateEntryTokenRequest struct {
EntryToken string `json:"entryToken" example:"token_abc"`
}
type ValidateEntryTokenResponse struct {
Valid bool `json:"valid" example:"true"`
Username string `json:"username,omitempty" example:"player1"`
SessionName string `json:"sessionName,omitempty" example:"Dedi1_Room0"`
}
type MyEntryTokenResponse struct {
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
EntryToken string `json:"entryToken" example:"token_abc"`
}
type RegisterServerRequest struct {
ServerName string `json:"serverName" example:"Dedi1"`
InstanceID string `json:"instanceId" example:"container_abc"`
MaxRooms int `json:"maxRooms" example:"10"`
}
type RegisterServerResponse struct {
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
InstanceID string `json:"instanceId" example:"container_abc"`
}
type HeartbeatRequest struct {
InstanceID string `json:"instanceId" example:"container_abc"`
}
type ResetRoomRequest struct {
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
}
type ResetRoomResponse struct {
Status string `json:"status" example:"ok"`
SessionName string `json:"sessionName" example:"Dedi1_Room0"`
}
// --- Player ---
type PlayerProfileResponse struct {
ID uint `json:"id" example:"1"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
UserID uint `json:"userId" example:"1"`
Nickname string `json:"nickname" example:"용사"`
Level int `json:"level" example:"5"`
Experience int `json:"experience" example:"1200"`
NextExp int `json:"nextExp" example:"2000"`
MaxHP float64 `json:"maxHp" example:"150"`
MaxMP float64 `json:"maxMp" example:"75"`
AttackPower float64 `json:"attackPower" example:"25"`
AttackRange float64 `json:"attackRange" example:"3"`
SprintMultiplier float64 `json:"sprintMultiplier" example:"1.8"`
LastPosX float64 `json:"lastPosX" example:"10.5"`
LastPosY float64 `json:"lastPosY" example:"0"`
LastPosZ float64 `json:"lastPosZ" example:"20.3"`
LastRotY float64 `json:"lastRotY" example:"90"`
TotalPlayTime int64 `json:"totalPlayTime" example:"3600"`
}
type UpdateProfileRequest struct {
Nickname string `json:"nickname" example:"용사"`
}
type GameDataRequest struct {
Level *int `json:"level,omitempty" example:"5"`
Experience *int `json:"experience,omitempty" example:"1200"`
MaxHP *float64 `json:"maxHp,omitempty" example:"150"`
MaxMP *float64 `json:"maxMp,omitempty" example:"75"`
AttackPower *float64 `json:"attackPower,omitempty" example:"25"`
AttackRange *float64 `json:"attackRange,omitempty" example:"3"`
SprintMultiplier *float64 `json:"sprintMultiplier,omitempty" example:"1.8"`
LastPosX *float64 `json:"lastPosX,omitempty" example:"10.5"`
LastPosY *float64 `json:"lastPosY,omitempty" example:"0"`
LastPosZ *float64 `json:"lastPosZ,omitempty" example:"20.3"`
LastRotY *float64 `json:"lastRotY,omitempty" example:"90"`
TotalPlayTime *int64 `json:"totalPlayTime,omitempty" example:"3600"`
}

56
go.mod
View File

@@ -1,49 +1,81 @@
module a301_server module a301_server
go 1.25 go 1.25.0
require ( require (
github.com/gofiber/fiber/v2 v2.52.11 github.com/gofiber/fiber/v2 v2.52.12
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.18.0 github.com/redis/go-redis/v9 v9.18.0
github.com/tolelom/tolchain v0.0.0 github.com/tolelom/tolchain v0.0.0
golang.org/x/crypto v0.48.0 golang.org/x/crypto v0.49.0
gorm.io/driver/mysql v1.6.0 gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1 gorm.io/gorm v1.31.1
) )
require ( require (
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.2.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/gofiber/swagger v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mailru/easyjson v0.9.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/tinylib/msgp v1.2.5 // indirect github.com/tinylib/msgp v1.2.5 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/sys v0.41.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.43.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
) )
replace github.com/tolelom/tolchain => ../tolchain replace github.com/tolelom/tolchain => ../tolchain

86
go.sum
View File

@@ -1,7 +1,15 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -10,15 +18,45 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=
github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs=
github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA=
github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -31,8 +69,12 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -41,13 +83,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M=
github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
@@ -66,18 +114,34 @@ github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfS
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -86,22 +150,44 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -16,6 +16,16 @@ func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc} return &Handler{svc: svc}
} }
// GetAll godoc
// @Summary 공지사항 목록 조회
// @Description 공지사항 목록을 조회합니다
// @Tags Announcements
// @Produce json
// @Param offset query int false "시작 위치" default(0)
// @Param limit query int false "조회 수" default(20)
// @Success 200 {array} docs.AnnouncementResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/announcements/ [get]
func (h *Handler) GetAll(c *fiber.Ctx) error { func (h *Handler) GetAll(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
limit := c.QueryInt("limit", 20) limit := c.QueryInt("limit", 20)
@@ -32,6 +42,20 @@ func (h *Handler) GetAll(c *fiber.Ctx) error {
return c.JSON(list) return c.JSON(list)
} }
// Create godoc
// @Summary 공지사항 생성 (관리자)
// @Description 새 공지사항을 생성합니다
// @Tags Announcements
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body docs.CreateAnnouncementRequest true "공지사항 내용"
// @Success 201 {object} docs.AnnouncementResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/announcements/ [post]
func (h *Handler) Create(c *fiber.Ctx) error { func (h *Handler) Create(c *fiber.Ctx) error {
var body struct { var body struct {
Title string `json:"title"` Title string `json:"title"`
@@ -53,6 +77,22 @@ func (h *Handler) Create(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(a) return c.Status(fiber.StatusCreated).JSON(a)
} }
// Update godoc
// @Summary 공지사항 수정 (관리자)
// @Description 공지사항을 수정합니다
// @Tags Announcements
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "공지사항 ID"
// @Param body body docs.UpdateAnnouncementRequest true "수정할 내용"
// @Success 200 {object} docs.AnnouncementResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/announcements/{id} [put]
func (h *Handler) Update(c *fiber.Ctx) error { func (h *Handler) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64) id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil { if err != nil {
@@ -85,6 +125,19 @@ func (h *Handler) Update(c *fiber.Ctx) error {
return c.JSON(a) return c.JSON(a)
} }
// Delete godoc
// @Summary 공지사항 삭제 (관리자)
// @Description 공지사항을 삭제합니다
// @Tags Announcements
// @Security BearerAuth
// @Param id path int true "공지사항 ID"
// @Success 204
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/announcements/{id} [delete]
func (h *Handler) Delete(c *fiber.Ctx) error { func (h *Handler) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64) id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil { if err != nil {

View File

@@ -20,6 +20,18 @@ func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc} return &Handler{svc: svc}
} }
// Register godoc
// @Summary 회원가입
// @Description 새로운 사용자 계정을 생성합니다
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body docs.RegisterRequest true "회원가입 정보"
// @Success 201 {object} docs.MessageResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 409 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/auth/register [post]
func (h *Handler) Register(c *fiber.Ctx) error { func (h *Handler) Register(c *fiber.Ctx) error {
var req struct { var req struct {
Username string `json:"username"` Username string `json:"username"`
@@ -50,6 +62,17 @@ func (h *Handler) Register(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "회원가입이 완료되었습니다"}) return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "회원가입이 완료되었습니다"})
} }
// Login godoc
// @Summary 로그인
// @Description 사용자 인증 후 JWT 토큰을 발급합니다
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body docs.LoginRequest true "로그인 정보"
// @Success 200 {object} docs.LoginResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/auth/login [post]
func (h *Handler) Login(c *fiber.Ctx) error { func (h *Handler) Login(c *fiber.Ctx) error {
var req struct { var req struct {
Username string `json:"username"` Username string `json:"username"`
@@ -91,6 +114,17 @@ func (h *Handler) Login(c *fiber.Ctx) error {
}) })
} }
// Refresh godoc
// @Summary 토큰 갱신
// @Description Refresh 토큰으로 새 Access 토큰을 발급합니다 (쿠키 또는 body)
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body docs.RefreshRequest false "Refresh 토큰 (쿠키 우선)"
// @Success 200 {object} docs.RefreshResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/auth/refresh [post]
func (h *Handler) Refresh(c *fiber.Ctx) error { func (h *Handler) Refresh(c *fiber.Ctx) error {
refreshTokenStr := c.Cookies("refresh_token") refreshTokenStr := c.Cookies("refresh_token")
if refreshTokenStr == "" { if refreshTokenStr == "" {
@@ -126,6 +160,16 @@ func (h *Handler) Refresh(c *fiber.Ctx) error {
}) })
} }
// Logout godoc
// @Summary 로그아웃
// @Description 현재 세션을 무효화합니다
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.MessageResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/auth/logout [post]
func (h *Handler) Logout(c *fiber.Ctx) error { func (h *Handler) Logout(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
@@ -146,6 +190,19 @@ func (h *Handler) Logout(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "로그아웃 되었습니다"}) return c.JSON(fiber.Map{"message": "로그아웃 되었습니다"})
} }
// GetAllUsers godoc
// @Summary 전체 유저 목록 (관리자)
// @Description 모든 유저 목록을 조회합니다
// @Tags Users
// @Produce json
// @Security BearerAuth
// @Param offset query int false "시작 위치" default(0)
// @Param limit query int false "조회 수" default(50)
// @Success 200 {array} docs.UserResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/users/ [get]
func (h *Handler) GetAllUsers(c *fiber.Ctx) error { func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
offset := c.QueryInt("offset", 0) offset := c.QueryInt("offset", 0)
limit := c.QueryInt("limit", 50) limit := c.QueryInt("limit", 50)
@@ -162,6 +219,21 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
return c.JSON(users) return c.JSON(users)
} }
// UpdateRole godoc
// @Summary 유저 권한 변경 (관리자)
// @Description 유저의 역할을 admin 또는 user로 변경합니다
// @Tags Users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "유저 ID"
// @Param body body docs.UpdateRoleRequest true "변경할 역할"
// @Success 200 {object} docs.MessageResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/users/{id}/role [patch]
func (h *Handler) UpdateRole(c *fiber.Ctx) error { func (h *Handler) UpdateRole(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64) id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil { if err != nil {
@@ -182,6 +254,18 @@ func (h *Handler) UpdateRole(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "권한이 변경되었습니다"}) return c.JSON(fiber.Map{"message": "권한이 변경되었습니다"})
} }
// VerifyToken godoc
// @Summary 토큰 검증 (내부 API)
// @Description JWT 토큰을 검증하고 username을 반환합니다
// @Tags Internal - Auth
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.VerifyTokenRequest true "검증할 토큰"
// @Success 200 {object} docs.VerifyTokenResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/internal/auth/verify [post]
func (h *Handler) VerifyToken(c *fiber.Ctx) error { func (h *Handler) VerifyToken(c *fiber.Ctx) error {
var req struct { var req struct {
Token string `json:"token"` Token string `json:"token"`
@@ -200,6 +284,14 @@ func (h *Handler) VerifyToken(c *fiber.Ctx) error {
}) })
} }
// SSAFYLoginURL godoc
// @Summary SSAFY 로그인 URL
// @Description SSAFY OAuth 로그인 URL을 생성합니다
// @Tags Auth
// @Produce json
// @Success 200 {object} docs.SSAFYLoginURLResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/auth/ssafy/login [get]
func (h *Handler) SSAFYLoginURL(c *fiber.Ctx) error { func (h *Handler) SSAFYLoginURL(c *fiber.Ctx) error {
loginURL, err := h.svc.GetSSAFYLoginURL() loginURL, err := h.svc.GetSSAFYLoginURL()
if err != nil { if err != nil {
@@ -208,6 +300,17 @@ func (h *Handler) SSAFYLoginURL(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"url": loginURL}) return c.JSON(fiber.Map{"url": loginURL})
} }
// SSAFYCallback godoc
// @Summary SSAFY OAuth 콜백
// @Description SSAFY 인가 코드를 교환하여 로그인합니다
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body docs.SSAFYCallbackRequest true "인가 코드"
// @Success 200 {object} docs.LoginResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/auth/ssafy/callback [post]
func (h *Handler) SSAFYCallback(c *fiber.Ctx) error { func (h *Handler) SSAFYCallback(c *fiber.Ctx) error {
var req struct { var req struct {
Code string `json:"code"` Code string `json:"code"`
@@ -242,8 +345,16 @@ func (h *Handler) SSAFYCallback(c *fiber.Ctx) error {
}) })
} }
// CreateLaunchTicket issues a one-time ticket for the game launcher. // CreateLaunchTicket godoc
// The launcher uses this ticket instead of receiving the JWT directly in the URL. // @Summary 런처 티켓 발급
// @Description 게임 런처용 일회성 티켓을 발급합니다
// @Tags Auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.LaunchTicketResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/auth/launch-ticket [post]
func (h *Handler) CreateLaunchTicket(c *fiber.Ctx) error { func (h *Handler) CreateLaunchTicket(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
@@ -256,8 +367,17 @@ func (h *Handler) CreateLaunchTicket(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ticket": ticket}) return c.JSON(fiber.Map{"ticket": ticket})
} }
// RedeemLaunchTicket exchanges a one-time ticket for an access token. // RedeemLaunchTicket godoc
// Called by the game launcher, not the web browser. // @Summary 런처 티켓 교환
// @Description 일회성 티켓을 Access 토큰으로 교환합니다
// @Tags Auth
// @Accept json
// @Produce json
// @Param body body docs.RedeemTicketRequest true "티켓"
// @Success 200 {object} docs.RedeemTicketResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/auth/redeem-ticket [post]
func (h *Handler) RedeemLaunchTicket(c *fiber.Ctx) error { func (h *Handler) RedeemLaunchTicket(c *fiber.Ctx) error {
var req struct { var req struct {
Ticket string `json:"ticket"` Ticket string `json:"ticket"`
@@ -273,6 +393,18 @@ func (h *Handler) RedeemLaunchTicket(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"token": token}) return c.JSON(fiber.Map{"token": token})
} }
// DeleteUser godoc
// @Summary 유저 삭제 (관리자)
// @Description 유저를 삭제합니다
// @Tags Users
// @Security BearerAuth
// @Param id path int true "유저 ID"
// @Success 204
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/users/{id} [delete]
func (h *Handler) DeleteUser(c *fiber.Ctx) error { func (h *Handler) DeleteUser(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 64) id, err := strconv.ParseUint(c.Params("id"), 10, 64)
if err != nil { if err != nil {

View File

@@ -19,8 +19,18 @@ func bossError(c *fiber.Ctx, status int, userMsg string, err error) error {
return c.Status(status).JSON(fiber.Map{"error": userMsg}) return c.Status(status).JSON(fiber.Map{"error": userMsg})
} }
// RequestEntry handles POST /api/internal/bossraid/entry // RequestEntry godoc
// Called by MMO server when a party requests boss raid entry. // @Summary 보스 레이드 입장 요청 (내부 API)
// @Description MMO 서버에서 파티의 보스 레이드 입장을 요청합니다. 모든 플레이어의 entry token을 반환합니다.
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.RequestEntryRequest true "입장 정보"
// @Success 201 {object} docs.InternalRequestEntryResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 409 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/entry [post]
func (h *Handler) RequestEntry(c *fiber.Ctx) error { func (h *Handler) RequestEntry(c *fiber.Ctx) error {
var req struct { var req struct {
Usernames []string `json:"usernames"` Usernames []string `json:"usernames"`
@@ -38,7 +48,7 @@ func (h *Handler) RequestEntry(c *fiber.Ctx) error {
} }
} }
room, err := h.svc.RequestEntry(req.Usernames, req.BossID) room, tokens, err := h.svc.RequestEntryWithTokens(req.Usernames, req.BossID)
if err != nil { if err != nil {
return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err) return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err)
} }
@@ -49,11 +59,21 @@ func (h *Handler) RequestEntry(c *fiber.Ctx) error {
"bossId": room.BossID, "bossId": room.BossID,
"players": req.Usernames, "players": req.Usernames,
"status": room.Status, "status": room.Status,
"tokens": tokens,
}) })
} }
// StartRaid handles POST /api/internal/bossraid/start // StartRaid godoc
// Called by dedicated server when the Fusion session begins. // @Summary 레이드 시작 (내부 API)
// @Description Fusion 세션이 시작될 때 데디케이티드 서버에서 호출합니다
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.SessionNameRequest true "세션 정보"
// @Success 200 {object} docs.RoomStatusResponse
// @Failure 400 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/start [post]
func (h *Handler) StartRaid(c *fiber.Ctx) error { func (h *Handler) StartRaid(c *fiber.Ctx) error {
var req struct { var req struct {
SessionName string `json:"sessionName"` SessionName string `json:"sessionName"`
@@ -77,8 +97,18 @@ func (h *Handler) StartRaid(c *fiber.Ctx) error {
}) })
} }
// CompleteRaid handles POST /api/internal/bossraid/complete // CompleteRaid godoc
// Called by dedicated server when the boss is killed. Distributes rewards. // @Summary 레이드 완료 (내부 API)
// @Description 보스 처치 시 데디케이티드 서버에서 호출합니다. 보상을 분배합니다.
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.CompleteRaidRequest true "완료 정보 및 보상"
// @Success 200 {object} docs.CompleteRaidResponse
// @Failure 400 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/complete [post]
func (h *Handler) CompleteRaid(c *fiber.Ctx) error { func (h *Handler) CompleteRaid(c *fiber.Ctx) error {
var req struct { var req struct {
SessionName string `json:"sessionName"` SessionName string `json:"sessionName"`
@@ -104,8 +134,17 @@ func (h *Handler) CompleteRaid(c *fiber.Ctx) error {
}) })
} }
// FailRaid handles POST /api/internal/bossraid/fail // FailRaid godoc
// Called by dedicated server on timeout or party wipe. // @Summary 레이드 실패 (내부 API)
// @Description 타임아웃 또는 전멸 시 데디케이티드 서버에서 호출합니다
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.SessionNameRequest true "세션 정보"
// @Success 200 {object} docs.RoomStatusResponse
// @Failure 400 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/fail [post]
func (h *Handler) FailRaid(c *fiber.Ctx) error { func (h *Handler) FailRaid(c *fiber.Ctx) error {
var req struct { var req struct {
SessionName string `json:"sessionName"` SessionName string `json:"sessionName"`
@@ -129,9 +168,20 @@ func (h *Handler) FailRaid(c *fiber.Ctx) error {
}) })
} }
// RequestEntryAuth handles POST /api/bossraid/entry (JWT authenticated). // RequestEntryAuth godoc
// Called by the game client to request boss raid entry. // @Summary 보스 레이드 입장 요청
// The authenticated user must be included in the usernames list. // @Description 게임 클라이언트에서 보스 레이드 입장을 요청합니다. 인증된 유저가 입장 목록에 포함되어야 합니다.
// @Tags Boss Raid
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body docs.RequestEntryAuthRequest true "입장 정보"
// @Success 201 {object} docs.RequestEntryResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 409 {object} docs.ErrorResponse
// @Router /api/bossraid/entry [post]
func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error { func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error {
var req struct { var req struct {
Usernames []string `json:"usernames"` Usernames []string `json:"usernames"`
@@ -188,9 +238,16 @@ func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error {
}) })
} }
// GetMyEntryToken handles GET /api/bossraid/my-entry-token (JWT authenticated). // GetMyEntryToken godoc
// Returns the pending entry token for the authenticated user. // @Summary 내 입장 토큰 조회
// Called by party members after the leader requests entry. // @Description 현재 유저의 대기 중인 입장 토큰을 조회합니다
// @Tags Boss Raid
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.MyEntryTokenResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/bossraid/my-entry-token [get]
func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error { func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error {
username, _ := c.Locals("username").(string) username, _ := c.Locals("username").(string)
if username == "" { if username == "" {
@@ -208,9 +265,18 @@ func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error {
}) })
} }
// ValidateEntryToken handles POST /api/internal/bossraid/validate-entry (ServerAuth). // ValidateEntryToken godoc
// Called by the dedicated server to validate a player's entry token. // @Summary 입장 토큰 검증 (내부 API)
// Consumes the token (one-time use). // @Description 데디케이티드 서버에서 플레이어의 입장 토큰을 검증합니다. 일회성 소모.
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.ValidateEntryTokenRequest true "토큰"
// @Success 200 {object} docs.ValidateEntryTokenResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/validate-entry [post]
func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error { func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error {
var req struct { var req struct {
EntryToken string `json:"entryToken"` EntryToken string `json:"entryToken"`
@@ -237,8 +303,17 @@ func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error {
}) })
} }
// GetRoom handles GET /api/internal/bossraid/room // GetRoom godoc
// Query param: sessionName // @Summary 방 정보 조회 (내부 API)
// @Description sessionName으로 보스 레이드 방 정보를 조회합니다
// @Tags Internal - Boss Raid
// @Produce json
// @Security ApiKeyAuth
// @Param sessionName query string true "세션 이름"
// @Success 200 {object} bossraid.BossRoom
// @Failure 400 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/room [get]
func (h *Handler) GetRoom(c *fiber.Ctx) error { func (h *Handler) GetRoom(c *fiber.Ctx) error {
sessionName := c.Query("sessionName") sessionName := c.Query("sessionName")
if sessionName == "" { if sessionName == "" {
@@ -252,3 +327,127 @@ func (h *Handler) GetRoom(c *fiber.Ctx) error {
return c.JSON(room) return c.JSON(room)
} }
// RegisterServer godoc
// @Summary 데디케이티드 서버 등록 (내부 API)
// @Description 데디케이티드 서버 컨테이너가 시작 시 호출합니다. 룸 슬롯을 자동 할당합니다.
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.RegisterServerRequest true "서버 정보"
// @Success 201 {object} docs.RegisterServerResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 409 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/register [post]
func (h *Handler) RegisterServer(c *fiber.Ctx) error {
var req struct {
ServerName string `json:"serverName"`
InstanceID string `json:"instanceId"`
MaxRooms int `json:"maxRooms"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.ServerName == "" || req.InstanceID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "serverName과 instanceId는 필수입니다"})
}
sessionName, err := h.svc.RegisterServer(req.ServerName, req.InstanceID, req.MaxRooms)
if err != nil {
return bossError(c, fiber.StatusConflict, "서버 등록에 실패했습니다", err)
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"sessionName": sessionName,
"instanceId": req.InstanceID,
})
}
// Heartbeat godoc
// @Summary 하트비트 (내부 API)
// @Description 데디케이티드 서버 컨테이너가 주기적으로 호출합니다
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.HeartbeatRequest true "인스턴스 정보"
// @Success 200 {object} docs.StatusResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/heartbeat [post]
func (h *Handler) Heartbeat(c *fiber.Ctx) error {
var req struct {
InstanceID string `json:"instanceId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.InstanceID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "instanceId는 필수입니다"})
}
if err := h.svc.Heartbeat(req.InstanceID); err != nil {
return bossError(c, fiber.StatusNotFound, "인스턴스를 찾을 수 없습니다", err)
}
return c.JSON(fiber.Map{"status": "ok"})
}
// ResetRoom godoc
// @Summary 룸 슬롯 리셋 (내부 API)
// @Description 레이드 종료 후 슬롯을 idle 상태로 되돌립니다
// @Tags Internal - Boss Raid
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param body body docs.ResetRoomRequest true "세션 정보"
// @Success 200 {object} docs.ResetRoomResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/reset-room [post]
func (h *Handler) ResetRoom(c *fiber.Ctx) error {
var req struct {
SessionName string `json:"sessionName"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if req.SessionName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"})
}
if err := h.svc.ResetRoom(req.SessionName); err != nil {
return bossError(c, fiber.StatusInternalServerError, "슬롯 리셋에 실패했습니다", err)
}
return c.JSON(fiber.Map{"status": "ok", "sessionName": req.SessionName})
}
// GetServerStatus godoc
// @Summary 서버 상태 조회 (내부 API)
// @Description 데디케이티드 서버의 정보와 룸 슬롯 목록을 조회합니다
// @Tags Internal - Boss Raid
// @Produce json
// @Security ApiKeyAuth
// @Param serverName query string true "서버 이름"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/internal/bossraid/server-status [get]
func (h *Handler) GetServerStatus(c *fiber.Ctx) error {
serverName := c.Query("serverName")
if serverName == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "serverName은 필수입니다"})
}
server, slots, err := h.svc.GetServerStatus(serverName)
if err != nil {
return bossError(c, fiber.StatusNotFound, "서버를 찾을 수 없습니다", err)
}
return c.JSON(fiber.Map{
"server": server,
"slots": slots,
})
}

View File

@@ -32,3 +32,41 @@ type BossRoom struct {
StartedAt *time.Time `json:"startedAt,omitempty"` StartedAt *time.Time `json:"startedAt,omitempty"`
CompletedAt *time.Time `json:"completedAt,omitempty"` CompletedAt *time.Time `json:"completedAt,omitempty"`
} }
// SlotStatus represents the status of a dedicated server room slot.
type SlotStatus string
const (
SlotIdle SlotStatus = "idle"
SlotWaiting SlotStatus = "waiting"
SlotInProgress SlotStatus = "in_progress"
)
// DedicatedServer represents a server group (e.g., "Dedi1").
// Multiple containers (replicas) share the same server group name.
type DedicatedServer struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
ServerName string `json:"serverName" gorm:"type:varchar(100);uniqueIndex;not null"`
MaxRooms int `json:"maxRooms" gorm:"default:10;not null"`
}
// RoomSlot represents a room slot on a dedicated server.
// Each slot has a stable session name that the Fusion NetworkRunner uses.
// InstanceID tracks which container process currently owns this slot.
type RoomSlot struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
DedicatedServerID uint `json:"dedicatedServerId" gorm:"index;not null"`
SlotIndex int `json:"slotIndex" gorm:"not null"`
SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;not null"`
Status SlotStatus `json:"status" gorm:"type:varchar(20);index;default:idle;not null"`
BossRoomID *uint `json:"bossRoomId" gorm:"index"`
InstanceID string `json:"instanceId" gorm:"type:varchar(100);index"`
LastHeartbeat *time.Time `json:"lastHeartbeat"`
}

View File

@@ -1,7 +1,9 @@
package bossraid package bossraid
import ( import (
"fmt"
"strings" "strings"
"time"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "gorm.io/gorm/clause"
@@ -61,3 +63,165 @@ func (r *Repository) CountActiveByUsername(username string) (int64, error) {
).Count(&count).Error ).Count(&count).Error
return count, err return count, err
} }
// --- DedicatedServer & RoomSlot ---
// UpsertDedicatedServer creates or updates a server group by name.
func (r *Repository) UpsertDedicatedServer(server *DedicatedServer) error {
var existing DedicatedServer
err := r.db.Where("server_name = ?", server.ServerName).First(&existing).Error
if err == gorm.ErrRecordNotFound {
return r.db.Create(server).Error
}
if err != nil {
return err
}
existing.MaxRooms = server.MaxRooms
return r.db.Save(&existing).Error
}
// FindDedicatedServerByName finds a server group by name.
func (r *Repository) FindDedicatedServerByName(serverName string) (*DedicatedServer, error) {
var server DedicatedServer
if err := r.db.Where("server_name = ?", serverName).First(&server).Error; err != nil {
return nil, err
}
return &server, nil
}
// EnsureRoomSlots ensures the correct number of room slots exist for a server.
func (r *Repository) EnsureRoomSlots(serverID uint, serverName string, maxRooms int) error {
for i := 0; i < maxRooms; i++ {
sessionName := fmt.Sprintf("%s_Room%d", serverName, i)
var existing RoomSlot
err := r.db.Where("session_name = ?", sessionName).First(&existing).Error
if err == gorm.ErrRecordNotFound {
slot := RoomSlot{
DedicatedServerID: serverID,
SlotIndex: i,
SessionName: sessionName,
Status: SlotIdle,
}
if err := r.db.Create(&slot).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
// AssignSlotToInstance finds an unassigned (or stale) slot and assigns it to the given instanceID.
// Returns the assigned slot with its sessionName.
func (r *Repository) AssignSlotToInstance(serverID uint, instanceID string, staleThreshold time.Time) (*RoomSlot, error) {
// First check if this instance already has a slot assigned
var existing RoomSlot
err := r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("dedicated_server_id = ? AND instance_id = ?", serverID, instanceID).
First(&existing).Error
if err == nil {
// Already assigned — refresh heartbeat
now := time.Now()
existing.LastHeartbeat = &now
r.db.Save(&existing)
return &existing, nil
}
// Find an unassigned slot (instance_id is empty or heartbeat is stale)
var slot RoomSlot
err = r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("dedicated_server_id = ? AND (instance_id = '' OR instance_id IS NULL OR last_heartbeat < ?)",
serverID, staleThreshold).
Order("slot_index ASC").
First(&slot).Error
if err != nil {
return nil, fmt.Errorf("사용 가능한 슬롯이 없습니다")
}
// Assign this instance to the slot
now := time.Now()
slot.InstanceID = instanceID
slot.LastHeartbeat = &now
slot.Status = SlotIdle
slot.BossRoomID = nil
if err := r.db.Save(&slot).Error; err != nil {
return nil, err
}
return &slot, nil
}
// UpdateHeartbeat updates the heartbeat for a specific instance.
func (r *Repository) UpdateHeartbeat(instanceID string) error {
now := time.Now()
result := r.db.Model(&RoomSlot{}).
Where("instance_id = ?", instanceID).
Update("last_heartbeat", now)
if result.RowsAffected == 0 {
return fmt.Errorf("인스턴스를 찾을 수 없습니다: %s", instanceID)
}
return result.Error
}
// FindIdleRoomSlot finds an idle room slot with a live instance (with row-level lock).
func (r *Repository) FindIdleRoomSlot(staleThreshold time.Time) (*RoomSlot, error) {
var slot RoomSlot
err := r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("status = ? AND instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat >= ?",
SlotIdle, staleThreshold).
Order("id ASC").
First(&slot).Error
if err != nil {
return nil, err
}
return &slot, nil
}
// UpdateRoomSlot updates a room slot.
func (r *Repository) UpdateRoomSlot(slot *RoomSlot) error {
return r.db.Save(slot).Error
}
// FindRoomSlotBySession finds a room slot by its session name.
func (r *Repository) FindRoomSlotBySession(sessionName string) (*RoomSlot, error) {
var slot RoomSlot
if err := r.db.Where("session_name = ?", sessionName).First(&slot).Error; err != nil {
return nil, err
}
return &slot, nil
}
// ResetRoomSlot sets a room slot back to idle and clears its BossRoomID.
// Does NOT clear InstanceID — the container still owns the slot.
func (r *Repository) ResetRoomSlot(sessionName string) error {
result := r.db.Model(&RoomSlot{}).
Where("session_name = ?", sessionName).
Updates(map[string]interface{}{
"status": SlotIdle,
"boss_room_id": nil,
})
return result.Error
}
// ResetStaleSlots clears instanceID for slots with stale heartbeats
// and resets any active raids on those slots.
func (r *Repository) ResetStaleSlots(threshold time.Time) (int64, error) {
result := r.db.Model(&RoomSlot{}).
Where("instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat < ?", threshold).
Updates(map[string]interface{}{
"instance_id": "",
"status": SlotIdle,
"boss_room_id": nil,
})
return result.RowsAffected, result.Error
}
// GetRoomSlotsByServer returns all room slots for a given server.
func (r *Repository) GetRoomSlotsByServer(serverID uint) ([]RoomSlot, error) {
var slots []RoomSlot
err := r.db.Where("dedicated_server_id = ?", serverID).Order("slot_index ASC").Find(&slots).Error
return slots, err
}

View File

@@ -34,6 +34,7 @@ type Service struct {
repo *Repository repo *Repository
rdb *redis.Client rdb *redis.Client
rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error
expGrant func(username string, exp int) error
} }
func NewService(repo *Repository, rdb *redis.Client) *Service { func NewService(repo *Repository, rdb *redis.Client) *Service {
@@ -45,7 +46,13 @@ func (s *Service) SetRewardGranter(fn func(username string, tokenAmount uint64,
s.rewardGrant = fn s.rewardGrant = fn
} }
// SetExpGranter sets the callback for granting experience to players.
func (s *Service) SetExpGranter(fn func(username string, exp int) error) {
s.expGrant = fn
}
// RequestEntry creates a new boss room for a party. // RequestEntry creates a new boss room for a party.
// Allocates an idle room slot from a registered dedicated server.
// Returns the room with assigned session name. // Returns the room with assigned session name.
func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error) { func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error) {
if len(usernames) == 0 { if len(usernames) == 0 {
@@ -69,18 +76,17 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error
return nil, fmt.Errorf("플레이어 목록 직렬화 실패: %w", err) return nil, fmt.Errorf("플레이어 목록 직렬화 실패: %w", err)
} }
sessionName := fmt.Sprintf("BossRaid_%d_%d", bossID, time.Now().UnixNano()) var room *BossRoom
room := &BossRoom{ // Wrap slot allocation + active-room check + creation in a transaction.
SessionName: sessionName, err = s.repo.Transaction(func(txRepo *Repository) error {
BossID: bossID, // Find an idle room slot from a live dedicated server instance
Status: StatusWaiting, staleThreshold := time.Now().Add(-30 * time.Second)
MaxPlayers: defaultMaxPlayers, slot, err := txRepo.FindIdleRoomSlot(staleThreshold)
Players: string(playersJSON), if err != nil {
return fmt.Errorf("현재 이용 가능한 보스 레이드 방이 없습니다")
} }
// Wrap active-room check + creation in a transaction to prevent TOCTOU race.
err = s.repo.Transaction(func(txRepo *Repository) error {
for _, username := range usernames { for _, username := range usernames {
count, err := txRepo.CountActiveByUsername(username) count, err := txRepo.CountActiveByUsername(username)
if err != nil { if err != nil {
@@ -90,9 +96,24 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error
return fmt.Errorf("플레이어 %s가 이미 보스 레이드 중입니다", username) return fmt.Errorf("플레이어 %s가 이미 보스 레이드 중입니다", username)
} }
} }
room = &BossRoom{
SessionName: slot.SessionName,
BossID: bossID,
Status: StatusWaiting,
MaxPlayers: defaultMaxPlayers,
Players: string(playersJSON),
}
if err := txRepo.Create(room); err != nil { if err := txRepo.Create(room); err != nil {
return fmt.Errorf("방 생성 실패: %w", err) return fmt.Errorf("방 생성 실패: %w", err)
} }
// Mark slot as waiting and link to the boss room
slot.Status = SlotWaiting
slot.BossRoomID = &room.ID
if err := txRepo.UpdateRoomSlot(slot); err != nil {
return fmt.Errorf("슬롯 상태 업데이트 실패: %w", err)
}
return nil return nil
}) })
if err != nil { if err != nil {
@@ -102,7 +123,7 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error
return room, nil return room, nil
} }
// StartRaid marks a room as in_progress. // StartRaid marks a room as in_progress and updates the slot status.
// Uses row-level locking to prevent concurrent state transitions. // Uses row-level locking to prevent concurrent state transitions.
func (s *Service) StartRaid(sessionName string) (*BossRoom, error) { func (s *Service) StartRaid(sessionName string) (*BossRoom, error) {
var resultRoom *BossRoom var resultRoom *BossRoom
@@ -122,6 +143,14 @@ func (s *Service) StartRaid(sessionName string) (*BossRoom, error) {
if err := txRepo.Update(room); err != nil { if err := txRepo.Update(room); err != nil {
return fmt.Errorf("상태 업데이트 실패: %w", err) return fmt.Errorf("상태 업데이트 실패: %w", err)
} }
// Update slot status to in_progress
slot, err := txRepo.FindRoomSlotBySession(sessionName)
if err == nil {
slot.Status = SlotInProgress
txRepo.UpdateRoomSlot(slot)
}
resultRoom = room resultRoom = room
return nil return nil
}) })
@@ -136,6 +165,7 @@ type PlayerReward struct {
Username string `json:"username"` Username string `json:"username"`
TokenAmount uint64 `json:"tokenAmount"` TokenAmount uint64 `json:"tokenAmount"`
Assets []core.MintAssetPayload `json:"assets"` Assets []core.MintAssetPayload `json:"assets"`
Experience int `json:"experience"` // 경험치 보상
} }
// RewardResult holds the result of granting a reward to one player. // RewardResult holds the result of granting a reward to one player.
@@ -204,10 +234,26 @@ func (s *Service) CompleteRaid(sessionName string, rewards []PlayerReward) (*Bos
} }
} }
// Grant experience to players
if s.expGrant != nil {
for _, r := range rewards {
if r.Experience > 0 {
if expErr := s.expGrant(r.Username, r.Experience); expErr != nil {
log.Printf("경험치 지급 실패: %s: %v", r.Username, expErr)
}
}
}
}
// Reset slot to idle so it can accept new raids
if err := s.repo.ResetRoomSlot(sessionName); err != nil {
log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err)
}
return resultRoom, resultRewards, nil return resultRoom, resultRewards, nil
} }
// FailRaid marks a room as failed. // FailRaid marks a room as failed and resets the slot.
// Uses row-level locking to prevent concurrent state transitions. // Uses row-level locking to prevent concurrent state transitions.
func (s *Service) FailRaid(sessionName string) (*BossRoom, error) { func (s *Service) FailRaid(sessionName string) (*BossRoom, error) {
var resultRoom *BossRoom var resultRoom *BossRoom
@@ -233,6 +279,12 @@ func (s *Service) FailRaid(sessionName string) (*BossRoom, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Reset slot to idle so it can accept new raids
if err := s.repo.ResetRoomSlot(sessionName); err != nil {
log.Printf("슬롯 리셋 실패 (fail): %s: %v", sessionName, err)
}
return resultRoom, nil return resultRoom, nil
} }
@@ -345,3 +397,85 @@ func (s *Service) RequestEntryWithTokens(usernames []string, bossID int) (*BossR
return room, tokens, nil return room, tokens, nil
} }
// --- Dedicated Server Management ---
const staleTimeout = 30 * time.Second
// RegisterServer registers a dedicated server instance (container).
// Creates the server group + slots if needed, then assigns a slot to this instance.
// Returns the assigned sessionName.
func (s *Service) RegisterServer(serverName, instanceID string, maxRooms int) (string, error) {
if serverName == "" || instanceID == "" {
return "", fmt.Errorf("serverName과 instanceId는 필수입니다")
}
if maxRooms <= 0 {
maxRooms = 10
}
// Ensure server group exists
server := &DedicatedServer{
ServerName: serverName,
MaxRooms: maxRooms,
}
if err := s.repo.UpsertDedicatedServer(server); err != nil {
return "", fmt.Errorf("서버 그룹 등록 실패: %w", err)
}
// Re-fetch to get the ID
server, err := s.repo.FindDedicatedServerByName(serverName)
if err != nil {
return "", fmt.Errorf("서버 조회 실패: %w", err)
}
// Ensure all room slots exist
if err := s.repo.EnsureRoomSlots(server.ID, serverName, maxRooms); err != nil {
return "", fmt.Errorf("슬롯 생성 실패: %w", err)
}
// Assign a slot to this instance
staleThreshold := time.Now().Add(-staleTimeout)
slot, err := s.repo.AssignSlotToInstance(server.ID, instanceID, staleThreshold)
if err != nil {
return "", fmt.Errorf("슬롯 배정 실패: %w", err)
}
return slot.SessionName, nil
}
// Heartbeat updates the heartbeat for a container instance.
func (s *Service) Heartbeat(instanceID string) error {
return s.repo.UpdateHeartbeat(instanceID)
}
// CheckStaleSlots resets slots whose instances have gone silent.
func (s *Service) CheckStaleSlots() {
threshold := time.Now().Add(-staleTimeout)
count, err := s.repo.ResetStaleSlots(threshold)
if err != nil {
log.Printf("스태일 슬롯 체크 실패: %v", err)
return
}
if count > 0 {
log.Printf("스태일 슬롯 %d개 리셋", count)
}
}
// ResetRoom resets a room slot back to idle.
// Called by the dedicated server after a raid ends and the runner is recycled.
func (s *Service) ResetRoom(sessionName string) error {
return s.repo.ResetRoomSlot(sessionName)
}
// GetServerStatus returns a server group and its room slots.
func (s *Service) GetServerStatus(serverName string) (*DedicatedServer, []RoomSlot, error) {
server, err := s.repo.FindDedicatedServerByName(serverName)
if err != nil {
return nil, nil, fmt.Errorf("서버를 찾을 수 없습니다: %w", err)
}
slots, err := s.repo.GetRoomSlotsByServer(server.ID)
if err != nil {
return nil, nil, fmt.Errorf("슬롯 조회 실패: %w", err)
}
return server, slots, nil
}

View File

@@ -52,6 +52,16 @@ func chainError(c *fiber.Ctx, status int, userMsg string, err error) error {
// ---- Query Handlers ---- // ---- Query Handlers ----
// GetWalletInfo godoc
// @Summary 지갑 정보 조회
// @Description 현재 유저의 블록체인 지갑 정보를 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.WalletInfoResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/chain/wallet [get]
func (h *Handler) GetWalletInfo(c *fiber.Ctx) error { func (h *Handler) GetWalletInfo(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -67,6 +77,16 @@ func (h *Handler) GetWalletInfo(c *fiber.Ctx) error {
}) })
} }
// GetBalance godoc
// @Summary 잔액 조회
// @Description 현재 유저의 토큰 잔액을 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/balance [get]
func (h *Handler) GetBalance(c *fiber.Ctx) error { func (h *Handler) GetBalance(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -79,6 +99,18 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// GetAssets godoc
// @Summary 에셋 목록 조회
// @Description 현재 유저의 블록체인 에셋 목록을 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Param offset query int false "시작 위치" default(0)
// @Param limit query int false "조회 수" default(50)
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/assets [get]
func (h *Handler) GetAssets(c *fiber.Ctx) error { func (h *Handler) GetAssets(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -93,6 +125,18 @@ func (h *Handler) GetAssets(c *fiber.Ctx) error {
return c.Send(result) return c.Send(result)
} }
// GetAsset godoc
// @Summary 에셋 상세 조회
// @Description 특정 에셋의 상세 정보를 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Param id path string true "에셋 ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/asset/{id} [get]
func (h *Handler) GetAsset(c *fiber.Ctx) error { func (h *Handler) GetAsset(c *fiber.Ctx) error {
assetID := c.Params("id") assetID := c.Params("id")
if !validID(assetID) { if !validID(assetID) {
@@ -106,6 +150,16 @@ func (h *Handler) GetAsset(c *fiber.Ctx) error {
return c.Send(result) return c.Send(result)
} }
// GetInventory godoc
// @Summary 인벤토리 조회
// @Description 현재 유저의 인벤토리를 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/inventory [get]
func (h *Handler) GetInventory(c *fiber.Ctx) error { func (h *Handler) GetInventory(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -119,6 +173,17 @@ func (h *Handler) GetInventory(c *fiber.Ctx) error {
return c.Send(result) return c.Send(result)
} }
// GetMarketListings godoc
// @Summary 마켓 목록 조회
// @Description 마켓에 등록된 매물 목록을 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Param offset query int false "시작 위치" default(0)
// @Param limit query int false "조회 수" default(50)
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/market [get]
func (h *Handler) GetMarketListings(c *fiber.Ctx) error { func (h *Handler) GetMarketListings(c *fiber.Ctx) error {
offset, limit := parsePagination(c) offset, limit := parsePagination(c)
result, err := h.svc.GetMarketListings(offset, limit) result, err := h.svc.GetMarketListings(offset, limit)
@@ -129,6 +194,17 @@ func (h *Handler) GetMarketListings(c *fiber.Ctx) error {
return c.Send(result) return c.Send(result)
} }
// GetMarketListing godoc
// @Summary 마켓 매물 상세 조회
// @Description 특정 마켓 매물의 상세 정보를 조회합니다
// @Tags Chain
// @Produce json
// @Security BearerAuth
// @Param id path string true "매물 ID"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/market/{id} [get]
func (h *Handler) GetMarketListing(c *fiber.Ctx) error { func (h *Handler) GetMarketListing(c *fiber.Ctx) error {
listingID := c.Params("id") listingID := c.Params("id")
if !validID(listingID) { if !validID(listingID) {
@@ -144,6 +220,20 @@ func (h *Handler) GetMarketListing(c *fiber.Ctx) error {
// ---- User Transaction Handlers ---- // ---- User Transaction Handlers ----
// Transfer godoc
// @Summary 토큰 전송
// @Description 다른 유저에게 토큰을 전송합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.TransferRequest true "전송 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/transfer [post]
func (h *Handler) Transfer(c *fiber.Ctx) error { func (h *Handler) Transfer(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -166,6 +256,20 @@ func (h *Handler) Transfer(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// TransferAsset godoc
// @Summary 에셋 전송
// @Description 다른 유저에게 에셋을 전송합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.TransferAssetRequest true "전송 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/asset/transfer [post]
func (h *Handler) TransferAsset(c *fiber.Ctx) error { func (h *Handler) TransferAsset(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -188,6 +292,20 @@ func (h *Handler) TransferAsset(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// ListOnMarket godoc
// @Summary 마켓 등록
// @Description 에셋을 마켓에 등록합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.ListOnMarketRequest true "등록 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/market/list [post]
func (h *Handler) ListOnMarket(c *fiber.Ctx) error { func (h *Handler) ListOnMarket(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -210,6 +328,20 @@ func (h *Handler) ListOnMarket(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// BuyFromMarket godoc
// @Summary 마켓 구매
// @Description 마켓에서 매물을 구매합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.BuyFromMarketRequest true "구매 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/market/buy [post]
func (h *Handler) BuyFromMarket(c *fiber.Ctx) error { func (h *Handler) BuyFromMarket(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -231,6 +363,20 @@ func (h *Handler) BuyFromMarket(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// CancelListing godoc
// @Summary 마켓 등록 취소
// @Description 마켓에 등록한 매물을 취소합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.CancelListingRequest true "취소 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/market/cancel [post]
func (h *Handler) CancelListing(c *fiber.Ctx) error { func (h *Handler) CancelListing(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -252,6 +398,20 @@ func (h *Handler) CancelListing(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// EquipItem godoc
// @Summary 아이템 장착
// @Description 에셋을 장비 슬롯에 장착합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.EquipItemRequest true "장착 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/inventory/equip [post]
func (h *Handler) EquipItem(c *fiber.Ctx) error { func (h *Handler) EquipItem(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -274,6 +434,20 @@ func (h *Handler) EquipItem(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// UnequipItem godoc
// @Summary 아이템 장착 해제
// @Description 에셋의 장비 슬롯 장착을 해제합니다
// @Tags Chain - Transactions
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.UnequipItemRequest true "해제 정보"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/inventory/unequip [post]
func (h *Handler) UnequipItem(c *fiber.Ctx) error { func (h *Handler) UnequipItem(c *fiber.Ctx) error {
userID, err := getUserID(c) userID, err := getUserID(c)
if err != nil { if err != nil {
@@ -297,6 +471,21 @@ func (h *Handler) UnequipItem(c *fiber.Ctx) error {
// ---- Operator (Admin) Transaction Handlers ---- // ---- Operator (Admin) Transaction Handlers ----
// MintAsset godoc
// @Summary 에셋 발행 (관리자)
// @Description 새 에셋을 발행합니다
// @Tags Chain - Admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.MintAssetRequest true "발행 정보"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/admin/mint [post]
func (h *Handler) MintAsset(c *fiber.Ctx) error { func (h *Handler) MintAsset(c *fiber.Ctx) error {
var req struct { var req struct {
TemplateID string `json:"templateId"` TemplateID string `json:"templateId"`
@@ -316,6 +505,21 @@ func (h *Handler) MintAsset(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(result) return c.Status(fiber.StatusCreated).JSON(result)
} }
// GrantReward godoc
// @Summary 보상 지급 (관리자)
// @Description 유저에게 토큰 및 에셋 보상을 지급합니다
// @Tags Chain - Admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.GrantRewardRequest true "보상 정보"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/admin/reward [post]
func (h *Handler) GrantReward(c *fiber.Ctx) error { func (h *Handler) GrantReward(c *fiber.Ctx) error {
var req struct { var req struct {
RecipientPubKey string `json:"recipientPubKey"` RecipientPubKey string `json:"recipientPubKey"`
@@ -335,6 +539,21 @@ func (h *Handler) GrantReward(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(result) return c.Status(fiber.StatusCreated).JSON(result)
} }
// RegisterTemplate godoc
// @Summary 템플릿 등록 (관리자)
// @Description 새 에셋 템플릿을 등록합니다
// @Tags Chain - Admin
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.RegisterTemplateRequest true "템플릿 정보"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/chain/admin/template [post]
func (h *Handler) RegisterTemplate(c *fiber.Ctx) error { func (h *Handler) RegisterTemplate(c *fiber.Ctx) error {
var req struct { var req struct {
ID string `json:"id"` ID string `json:"id"`
@@ -357,7 +576,19 @@ func (h *Handler) RegisterTemplate(c *fiber.Ctx) error {
// ---- Internal Handlers (game server, username-based) ---- // ---- Internal Handlers (game server, username-based) ----
// InternalGrantReward grants reward by username. For game server use. // InternalGrantReward godoc
// @Summary 보상 지급 (내부 API)
// @Description username으로 유저에게 보상을 지급합니다 (게임 서버용)
// @Tags Internal - Chain
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.InternalGrantRewardRequest true "보상 정보"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/chain/reward [post]
func (h *Handler) InternalGrantReward(c *fiber.Ctx) error { func (h *Handler) InternalGrantReward(c *fiber.Ctx) error {
var req struct { var req struct {
Username string `json:"username"` Username string `json:"username"`
@@ -377,7 +608,19 @@ func (h *Handler) InternalGrantReward(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(result) return c.Status(fiber.StatusCreated).JSON(result)
} }
// InternalMintAsset mints an asset by username. For game server use. // InternalMintAsset godoc
// @Summary 에셋 발행 (내부 API)
// @Description username으로 에셋을 발행합니다 (게임 서버용)
// @Tags Internal - Chain
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param Idempotency-Key header string true "멱등성 키"
// @Param body body docs.InternalMintAssetRequest true "발행 정보"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/chain/mint [post]
func (h *Handler) InternalMintAsset(c *fiber.Ctx) error { func (h *Handler) InternalMintAsset(c *fiber.Ctx) error {
var req struct { var req struct {
TemplateID string `json:"templateId"` TemplateID string `json:"templateId"`
@@ -397,7 +640,17 @@ func (h *Handler) InternalMintAsset(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(result) return c.Status(fiber.StatusCreated).JSON(result)
} }
// InternalGetBalance returns balance by username. For game server use. // InternalGetBalance godoc
// @Summary 잔액 조회 (내부 API)
// @Description username으로 잔액을 조회합니다 (게임 서버용)
// @Tags Internal - Chain
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "유저명"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/chain/balance [get]
func (h *Handler) InternalGetBalance(c *fiber.Ctx) error { func (h *Handler) InternalGetBalance(c *fiber.Ctx) error {
username := c.Query("username") username := c.Query("username")
if !validID(username) { if !validID(username) {
@@ -410,7 +663,19 @@ func (h *Handler) InternalGetBalance(c *fiber.Ctx) error {
return c.JSON(result) return c.JSON(result)
} }
// InternalGetAssets returns assets by username. For game server use. // InternalGetAssets godoc
// @Summary 에셋 목록 조회 (내부 API)
// @Description username으로 에셋 목록을 조회합니다 (게임 서버용)
// @Tags Internal - Chain
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "유저명"
// @Param offset query int false "시작 위치" default(0)
// @Param limit query int false "조회 수" default(50)
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/chain/assets [get]
func (h *Handler) InternalGetAssets(c *fiber.Ctx) error { func (h *Handler) InternalGetAssets(c *fiber.Ctx) error {
username := c.Query("username") username := c.Query("username")
if !validID(username) { if !validID(username) {
@@ -425,7 +690,17 @@ func (h *Handler) InternalGetAssets(c *fiber.Ctx) error {
return c.Send(result) return c.Send(result)
} }
// InternalGetInventory returns inventory by username. For game server use. // InternalGetInventory godoc
// @Summary 인벤토리 조회 (내부 API)
// @Description username으로 인벤토리를 조회합니다 (게임 서버용)
// @Tags Internal - Chain
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "유저명"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/chain/inventory [get]
func (h *Handler) InternalGetInventory(c *fiber.Ctx) error { func (h *Handler) InternalGetInventory(c *fiber.Ctx) error {
username := c.Query("username") username := c.Query("username")
if !validID(username) { if !validID(username) {

View File

@@ -19,6 +19,14 @@ func NewHandler(svc *Service, baseURL string) *Handler {
return &Handler{svc: svc, baseURL: baseURL} return &Handler{svc: svc, baseURL: baseURL}
} }
// GetInfo godoc
// @Summary 다운로드 정보 조회
// @Description 게임 및 런처 다운로드 정보를 조회합니다
// @Tags Download
// @Produce json
// @Success 200 {object} docs.DownloadInfoResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/download/info [get]
func (h *Handler) GetInfo(c *fiber.Ctx) error { func (h *Handler) GetInfo(c *fiber.Ctx) error {
info, err := h.svc.GetInfo() info, err := h.svc.GetInfo()
if err != nil { if err != nil {
@@ -27,8 +35,20 @@ func (h *Handler) GetInfo(c *fiber.Ctx) error {
return c.JSON(info) return c.JSON(info)
} }
// Upload accepts a raw binary body (application/octet-stream). // Upload godoc
// The filename is passed as a query parameter: ?filename=A301_v1.0.zip // @Summary 게임 파일 업로드 (관리자)
// @Description 게임 zip 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.
// @Tags Download
// @Accept application/octet-stream
// @Produce json
// @Security BearerAuth
// @Param filename query string false "파일명" default(game.zip)
// @Success 200 {object} docs.DownloadInfoResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/download/upload/game [post]
func (h *Handler) Upload(c *fiber.Ctx) error { func (h *Handler) Upload(c *fiber.Ctx) error {
filename := strings.TrimSpace(c.Query("filename", "game.zip")) filename := strings.TrimSpace(c.Query("filename", "game.zip"))
// 경로 순회 방지: 디렉토리 구분자 제거, 기본 파일명만 사용 // 경로 순회 방지: 디렉토리 구분자 제거, 기본 파일명만 사용
@@ -49,6 +69,14 @@ func (h *Handler) Upload(c *fiber.Ctx) error {
return c.JSON(info) return c.JSON(info)
} }
// ServeFile godoc
// @Summary 게임 파일 다운로드
// @Description 게임 zip 파일을 다운로드합니다
// @Tags Download
// @Produce application/octet-stream
// @Success 200 {file} binary
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/download/file [get]
func (h *Handler) ServeFile(c *fiber.Ctx) error { func (h *Handler) ServeFile(c *fiber.Ctx) error {
path := h.svc.GameFilePath() path := h.svc.GameFilePath()
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
@@ -63,6 +91,18 @@ func (h *Handler) ServeFile(c *fiber.Ctx) error {
return c.SendFile(path) return c.SendFile(path)
} }
// UploadLauncher godoc
// @Summary 런처 업로드 (관리자)
// @Description 런처 실행 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.
// @Tags Download
// @Accept application/octet-stream
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.DownloadInfoResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 403 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/download/upload/launcher [post]
func (h *Handler) UploadLauncher(c *fiber.Ctx) error { func (h *Handler) UploadLauncher(c *fiber.Ctx) error {
body := c.Request().BodyStream() body := c.Request().BodyStream()
info, err := h.svc.UploadLauncher(body, h.baseURL) info, err := h.svc.UploadLauncher(body, h.baseURL)
@@ -73,6 +113,14 @@ func (h *Handler) UploadLauncher(c *fiber.Ctx) error {
return c.JSON(info) return c.JSON(info)
} }
// ServeLauncher godoc
// @Summary 런처 다운로드
// @Description 런처 실행 파일을 다운로드합니다
// @Tags Download
// @Produce application/octet-stream
// @Success 200 {file} binary
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/download/launcher [get]
func (h *Handler) ServeLauncher(c *fiber.Ctx) error { func (h *Handler) ServeLauncher(c *fiber.Ctx) error {
path := h.svc.LauncherFilePath() path := h.svc.LauncherFilePath()
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {

View File

@@ -16,7 +16,16 @@ func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc} return &Handler{svc: svc}
} }
// GetProfile 자신의 프로필 조회 (JWT 인증) // GetProfile godoc
// @Summary 내 프로필 조회
// @Description 현재 유저의 플레이어 프로필을 조회합니다
// @Tags Player
// @Produce json
// @Security BearerAuth
// @Success 200 {object} docs.PlayerProfileResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/player/profile [get]
func (h *Handler) GetProfile(c *fiber.Ctx) error { func (h *Handler) GetProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
@@ -28,10 +37,22 @@ func (h *Handler) GetProfile(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
} }
return c.JSON(profile) return c.JSON(profileWithNextExp(profile))
} }
// UpdateProfile 자신의 프로필 수정 (JWT 인증) // UpdateProfile godoc
// @Summary 프로필 수정
// @Description 현재 유저의 닉네임을 수정합니다
// @Tags Player
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body docs.UpdateProfileRequest true "수정할 프로필"
// @Success 200 {object} player.PlayerProfile
// @Failure 400 {object} docs.ErrorResponse
// @Failure 401 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/player/profile [put]
func (h *Handler) UpdateProfile(c *fiber.Ctx) error { func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(uint) userID, ok := c.Locals("userID").(uint)
if !ok { if !ok {
@@ -67,7 +88,17 @@ func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
return c.JSON(profile) return c.JSON(profile)
} }
// InternalGetProfile 내부 API: username 쿼리 파라미터로 프로필 조회 // InternalGetProfile godoc
// @Summary 프로필 조회 (내부 API)
// @Description username으로 플레이어 프로필을 조회합니다 (게임 서버용)
// @Tags Internal - Player
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "유저명"
// @Success 200 {object} docs.PlayerProfileResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 404 {object} docs.ErrorResponse
// @Router /api/internal/player/profile [get]
func (h *Handler) InternalGetProfile(c *fiber.Ctx) error { func (h *Handler) InternalGetProfile(c *fiber.Ctx) error {
username := c.Query("username") username := c.Query("username")
if username == "" { if username == "" {
@@ -79,10 +110,50 @@ func (h *Handler) InternalGetProfile(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
} }
return c.JSON(profile) return c.JSON(profileWithNextExp(profile))
} }
// InternalSaveGameData 내부 API: username 쿼리 파라미터로 게임 데이터 저장 // profileWithNextExp wraps a PlayerProfile with nextExp for JSON response.
func profileWithNextExp(p *PlayerProfile) fiber.Map {
nextExp := 0
if p.Level < MaxLevel {
nextExp = RequiredExp(p.Level)
}
return fiber.Map{
"id": p.ID,
"createdAt": p.CreatedAt,
"updatedAt": p.UpdatedAt,
"userId": p.UserID,
"nickname": p.Nickname,
"level": p.Level,
"experience": p.Experience,
"nextExp": nextExp,
"maxHp": p.MaxHP,
"maxMp": p.MaxMP,
"attackPower": p.AttackPower,
"attackRange": p.AttackRange,
"sprintMultiplier": p.SprintMultiplier,
"lastPosX": p.LastPosX,
"lastPosY": p.LastPosY,
"lastPosZ": p.LastPosZ,
"lastRotY": p.LastRotY,
"totalPlayTime": p.TotalPlayTime,
}
}
// InternalSaveGameData godoc
// @Summary 게임 데이터 저장 (내부 API)
// @Description username으로 게임 데이터를 저장합니다 (게임 서버용)
// @Tags Internal - Player
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param username query string true "유저명"
// @Param body body docs.GameDataRequest true "게임 데이터"
// @Success 200 {object} docs.MessageResponse
// @Failure 400 {object} docs.ErrorResponse
// @Failure 500 {object} docs.ErrorResponse
// @Router /api/internal/player/save [post]
func (h *Handler) InternalSaveGameData(c *fiber.Ctx) error { func (h *Handler) InternalSaveGameData(c *fiber.Ctx) error {
username := c.Query("username") username := c.Query("username")
if username == "" { if username == "" {

71
internal/player/level.go Normal file
View File

@@ -0,0 +1,71 @@
package player
// MaxLevel is the maximum player level.
const MaxLevel = 50
// RequiredExp returns the experience needed to reach the next level.
// Formula: level^2 * 100
func RequiredExp(level int) int {
return level * level * 100
}
// CalcStatsForLevel computes combat stats for a given level.
func CalcStatsForLevel(level int) (maxHP, maxMP, attackPower float64) {
maxHP = 100 + float64(level-1)*10
maxMP = 50 + float64(level-1)*5
attackPower = 10 + float64(level-1)*2
return
}
// LevelUpResult holds the result of applying experience gain.
type LevelUpResult struct {
OldLevel int `json:"oldLevel"`
NewLevel int `json:"newLevel"`
Experience int `json:"experience"`
NextExp int `json:"nextExp"`
MaxHP float64 `json:"maxHp"`
MaxMP float64 `json:"maxMp"`
AttackPower float64 `json:"attackPower"`
LeveledUp bool `json:"leveledUp"`
}
// ApplyExperience adds exp to current level/exp and applies level ups.
// Returns the result including new stats if leveled up.
func ApplyExperience(currentLevel, currentExp, gainedExp int) LevelUpResult {
level := currentLevel
exp := currentExp + gainedExp
// Process level ups
for level < MaxLevel {
required := RequiredExp(level)
if exp < required {
break
}
exp -= required
level++
}
// Cap at max level
if level >= MaxLevel {
level = MaxLevel
exp = 0 // No more exp needed at max level
}
maxHP, maxMP, attackPower := CalcStatsForLevel(level)
nextExp := 0
if level < MaxLevel {
nextExp = RequiredExp(level)
}
return LevelUpResult{
OldLevel: currentLevel,
NewLevel: level,
Experience: exp,
NextExp: nextExp,
MaxHP: maxHP,
MaxMP: maxMP,
AttackPower: attackPower,
LeveledUp: level > currentLevel,
}
}

View File

@@ -165,6 +165,43 @@ func (s *Service) SaveGameDataByUsername(username string, data *GameDataRequest)
return s.SaveGameData(userID, data) return s.SaveGameData(userID, data)
} }
// GrantExperience adds experience to a player and handles level ups + stat recalculation.
func (s *Service) GrantExperience(userID uint, exp int) (*LevelUpResult, error) {
profile, err := s.repo.FindByUserID(userID)
if err != nil {
return nil, fmt.Errorf("프로필이 존재하지 않습니다")
}
result := ApplyExperience(profile.Level, profile.Experience, exp)
updates := map[string]interface{}{
"level": result.NewLevel,
"experience": result.Experience,
"max_hp": result.MaxHP,
"max_mp": result.MaxMP,
"attack_power": result.AttackPower,
}
if err := s.repo.UpdateStats(userID, updates); err != nil {
return nil, fmt.Errorf("레벨업 저장 실패: %w", err)
}
return &result, nil
}
// GrantExperienceByUsername grants experience to a player by username.
func (s *Service) GrantExperienceByUsername(username string, exp int) error {
if s.userResolver == nil {
return fmt.Errorf("userResolver가 설정되지 않았습니다")
}
userID, err := s.userResolver(username)
if err != nil {
return fmt.Errorf("존재하지 않는 유저입니다")
}
_, err = s.GrantExperience(userID, exp)
return err
}
// GameDataRequest 게임 데이터 저장 요청 (nil 필드는 변경하지 않음). // GameDataRequest 게임 데이터 저장 요청 (nil 필드는 변경하지 않음).
type GameDataRequest struct { type GameDataRequest struct {
Level *int `json:"level,omitempty"` Level *int `json:"level,omitempty"`

32
main.go
View File

@@ -15,6 +15,8 @@ import (
"a301_server/internal/download" "a301_server/internal/download"
"a301_server/internal/player" "a301_server/internal/player"
_ "a301_server/docs" // swagger docs
"github.com/tolelom/tolchain/core" "github.com/tolelom/tolchain/core"
"a301_server/pkg/config" "a301_server/pkg/config"
"a301_server/pkg/database" "a301_server/pkg/database"
@@ -27,6 +29,22 @@ import (
"github.com/gofiber/fiber/v2/middleware/logger" "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() { func main() {
config.Load() config.Load()
config.WarnInsecureDefaults() config.WarnInsecureDefaults()
@@ -37,7 +55,7 @@ func main() {
log.Println("MySQL 연결 성공") log.Println("MySQL 연결 성공")
// AutoMigrate // AutoMigrate
if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &player.PlayerProfile{}); err != nil { 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) log.Fatalf("AutoMigrate 실패: %v", err)
} }
@@ -106,6 +124,9 @@ func main() {
_, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets) _, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets)
return err return err
}) })
brSvc.SetExpGranter(func(username string, exp int) error {
return playerSvc.GrantExperienceByUsername(username, exp)
})
brHandler := bossraid.NewHandler(brSvc) brHandler := bossraid.NewHandler(brSvc)
if config.C.InternalAPIKey == "" { if config.C.InternalAPIKey == "" {
@@ -196,6 +217,15 @@ func main() {
routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, authLimiter, apiLimiter, healthCheck, readyCheck, chainUserLimiter) 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 // Graceful shutdown
go func() { go func() {
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)

View File

@@ -9,6 +9,7 @@ import (
"a301_server/internal/player" "a301_server/internal/player"
"a301_server/pkg/middleware" "a301_server/pkg/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/swagger"
) )
func Register( func Register(
@@ -25,6 +26,9 @@ func Register(
readyCheck fiber.Handler, readyCheck fiber.Handler,
chainUserLimiter fiber.Handler, chainUserLimiter fiber.Handler,
) { ) {
// Swagger UI
app.Get("/swagger/*", swagger.HandlerDefault)
// Health / Ready (rate limiter 밖) // Health / Ready (rate limiter 밖)
app.Get("/health", healthCheck) app.Get("/health", healthCheck)
app.Get("/ready", readyCheck) app.Get("/ready", readyCheck)
@@ -104,6 +108,10 @@ func Register(
br.Post("/fail", brH.FailRaid) br.Post("/fail", brH.FailRaid)
br.Get("/room", brH.GetRoom) br.Get("/room", brH.GetRoom)
br.Post("/validate-entry", brH.ValidateEntryToken) br.Post("/validate-entry", brH.ValidateEntryToken)
br.Post("/register", brH.RegisterServer)
br.Post("/heartbeat", brH.Heartbeat)
br.Post("/reset-room", brH.ResetRoom)
br.Get("/server-status", brH.GetServerStatus)
// Player Profile (authenticated) // Player Profile (authenticated)
p := api.Group("/player", middleware.Auth) p := api.Group("/player", middleware.Auth)