feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
- 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:
4121
docs/docs.go
Normal file
4121
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
4097
docs/swagger.json
Normal file
4097
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
2686
docs/swagger.yaml
Normal file
2686
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
348
docs/swagger_types.go
Normal file
348
docs/swagger_types.go
Normal 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
56
go.mod
@@ -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
86
go.sum
@@ -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=
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
71
internal/player/level.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
32
main.go
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user