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:
@@ -1,7 +1,9 @@
|
||||
package bossraid
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -61,3 +63,165 @@ func (r *Repository) CountActiveByUsername(username string) (int64, error) {
|
||||
).Count(&count).Error
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user