Fix: 로딩 중 강제 종료 시 stale waiting room 자동 정리
All checks were successful
Server CI/CD / lint-and-build (push) Successful in 38s
Server CI/CD / deploy (push) Successful in 55s

로딩 화면에서 강제 종료하면 BossRoom이 waiting 상태로 남아
재입장이 영구 차단되는 문제 수정.

- waiting 상태 2분 초과 BossRoom 자동 정리 (15초 주기)
- RequestEntry 시 해당 유저의 stale waiting room 선제 정리
- 연결된 RoomSlot도 idle로 리셋

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 11:00:36 +09:00
parent 4bbab002ea
commit 7ece5f3c44
2 changed files with 85 additions and 2 deletions

View File

@@ -64,6 +64,18 @@ func (r *Repository) CountActiveByUsername(username string) (int64, error) {
return count, err
}
// FindStaleWaitingRoomsByUsername returns waiting rooms containing the given username
// that were created before the threshold.
func (r *Repository) FindStaleWaitingRoomsByUsername(username string, threshold time.Time) ([]BossRoom, error) {
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(username)
search := `"` + escaped + `"`
var rooms []BossRoom
err := r.db.Where("status = ? AND players LIKE ? AND created_at < ?",
StatusWaiting, "%"+search+"%", threshold).
Find(&rooms).Error
return rooms, err
}
// --- DedicatedServer & RoomSlot ---
// UpsertDedicatedServer creates or updates a server group by name.
@@ -213,6 +225,40 @@ func (r *Repository) DeleteRoomBySessionName(sessionName string) error {
return r.db.Unscoped().Where("session_name = ?", sessionName).Delete(&BossRoom{}).Error
}
// CleanupStaleWaitingRooms deletes BossRoom records stuck in "waiting" status
// past the given threshold and resets their associated RoomSlots to idle.
// This handles cases where players disconnect during loading before the Fusion session starts.
func (r *Repository) CleanupStaleWaitingRooms(threshold time.Time) (int64, error) {
// 1. waiting 상태에서 threshold보다 오래된 방 조회
var staleRooms []BossRoom
if err := r.db.Where("status = ? AND created_at < ?", StatusWaiting, threshold).
Find(&staleRooms).Error; err != nil {
return 0, err
}
if len(staleRooms) == 0 {
return 0, nil
}
// 2. 연결된 슬롯을 idle로 리셋
staleSessionNames := make([]string, len(staleRooms))
for i, room := range staleRooms {
staleSessionNames[i] = room.SessionName
}
r.db.Model(&RoomSlot{}).
Where("session_name IN ? AND status = ?", staleSessionNames, SlotWaiting).
Updates(map[string]interface{}{
"status": SlotIdle,
"boss_room_id": nil,
})
// 3. BossRoom 레코드 하드 삭제
result := r.db.Unscoped().
Where("status = ? AND created_at < ?", StatusWaiting, threshold).
Delete(&BossRoom{})
return result.RowsAffected, 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) {