Fix: 보스 레이드 재입장 불가 버그 수정

- BossRoom 세션명을 매 입장마다 고유하게 생성 (슬롯명_타임스탬프)
- SlotSessionName 필드 추가로 슬롯 리셋 시 원래 슬롯명 사용
- DeleteRoomBySlotSessionName 추가 (dedicated server ResetRoom 대응)
- CompleteRaid/FailRaid/cleanup에서 슬롯 리셋 로직 수정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-23 22:04:03 +09:00
parent b006fe77c2
commit ac6827aae5
3 changed files with 49 additions and 15 deletions

View File

@@ -26,8 +26,9 @@ type BossRoom struct {
CreatedAt time.Time `json:"createdAt" gorm:"index"` CreatedAt time.Time `json:"createdAt" gorm:"index"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;not null"` SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;not null"`
BossID int `json:"bossId" gorm:"index;not null"` SlotSessionName string `json:"slotSessionName" gorm:"type:varchar(100);index;not null"`
BossID int `json:"bossId" gorm:"index;not null"`
Status RoomStatus `json:"status" gorm:"type:varchar(20);index;default:waiting;not null"` Status RoomStatus `json:"status" gorm:"type:varchar(20);index;default:waiting;not null"`
MaxPlayers int `json:"maxPlayers" gorm:"default:3;not null"` MaxPlayers int `json:"maxPlayers" gorm:"default:3;not null"`
// Players is stored as a JSON text column for simplicity. // Players is stored as a JSON text column for simplicity.

View File

@@ -224,6 +224,12 @@ func (r *Repository) DeleteRoomBySessionName(sessionName string) error {
return r.db.Unscoped().Where("session_name = ?", sessionName).Delete(&BossRoom{}).Error return r.db.Unscoped().Where("session_name = ?", sessionName).Delete(&BossRoom{}).Error
} }
// DeleteRoomBySlotSessionName removes BossRoom records matching the original slot session name.
// Used when dedicated server calls ResetRoom with the slot name (not the unique per-entry name).
func (r *Repository) DeleteRoomBySlotSessionName(slotSessionName string) error {
return r.db.Unscoped().Where("slot_session_name = ?", slotSessionName).Delete(&BossRoom{}).Error
}
// CleanupStaleWaitingRooms deletes BossRoom records stuck in "waiting" status // CleanupStaleWaitingRooms deletes BossRoom records stuck in "waiting" status
// past the given threshold and resets their associated RoomSlots to idle. // past the given threshold and resets their associated RoomSlots to idle.
// This handles cases where players disconnect during loading before the Fusion session starts. // This handles cases where players disconnect during loading before the Fusion session starts.

View File

@@ -106,12 +106,17 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error
} }
} }
// 세션명에 타임스탬프를 붙여 매 입장마다 고유하게 만듦
// (이전 Fusion 세션이 아직 살아있어도 충돌하지 않음)
uniqueSession := fmt.Sprintf("%s_%d", slot.SessionName, time.Now().UnixNano())
room = &BossRoom{ room = &BossRoom{
SessionName: slot.SessionName, SessionName: uniqueSession,
BossID: bossID, SlotSessionName: slot.SessionName, // 슬롯 리셋용 원래 이름 보존
Status: StatusWaiting, BossID: bossID,
MaxPlayers: len(usernames), Status: StatusWaiting,
Players: string(playersJSON), MaxPlayers: len(usernames),
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)
@@ -262,8 +267,13 @@ func (s *Service) CompleteRaid(sessionName string, rewards []PlayerReward) (*Bos
if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil { if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil {
log.Printf("BossRoom 삭제 실패 (complete): %s: %v", sessionName, err) log.Printf("BossRoom 삭제 실패 (complete): %s: %v", sessionName, err)
} }
if err := s.repo.ResetRoomSlot(sessionName); err != nil { // SlotSessionName으로 슬롯 리셋 (고유 세션명이 아닌 원래 슬롯명)
log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err) slotName := resultRoom.SlotSessionName
if slotName == "" {
slotName = sessionName // 하위 호환
}
if err := s.repo.ResetRoomSlot(slotName); err != nil {
log.Printf("슬롯 리셋 실패 (complete): %s: %v", slotName, err)
} }
return resultRoom, resultRewards, nil return resultRoom, resultRewards, nil
@@ -295,8 +305,12 @@ func (s *Service) FailRaid(sessionName string) (*BossRoom, error) {
if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil { if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil {
log.Printf("BossRoom 삭제 실패 (fail): %s: %v", sessionName, err) log.Printf("BossRoom 삭제 실패 (fail): %s: %v", sessionName, err)
} }
if err := s.repo.ResetRoomSlot(sessionName); err != nil { slotName := room.SlotSessionName
log.Printf("슬롯 리셋 실패 (fail): %s: %v", sessionName, err) if slotName == "" {
slotName = sessionName
}
if err := s.repo.ResetRoomSlot(slotName); err != nil {
log.Printf("슬롯 리셋 실패 (fail): %s: %v", slotName, err)
} }
return room, nil return room, nil
@@ -411,7 +425,11 @@ func (s *Service) RequestEntryWithTokens(usernames []string, bossID int) (*BossR
if delErr := s.repo.DeleteRoomBySessionName(room.SessionName); delErr != nil { if delErr := s.repo.DeleteRoomBySessionName(room.SessionName); delErr != nil {
log.Printf("롤백 중 방 삭제 실패: %v", delErr) log.Printf("롤백 중 방 삭제 실패: %v", delErr)
} }
if resetErr := s.repo.ResetRoomSlot(room.SessionName); resetErr != nil { rollbackSlot := room.SlotSessionName
if rollbackSlot == "" {
rollbackSlot = room.SessionName
}
if resetErr := s.repo.ResetRoomSlot(rollbackSlot); resetErr != nil {
log.Printf("롤백 중 슬롯 리셋 실패: %v", resetErr) log.Printf("롤백 중 슬롯 리셋 실패: %v", resetErr)
} }
return nil, nil, fmt.Errorf("입장 토큰 생성 실패: %w", err) return nil, nil, fmt.Errorf("입장 토큰 생성 실패: %w", err)
@@ -445,7 +463,11 @@ func (s *Service) cleanupStaleWaitingForUsers(usernames []string) {
if err := s.repo.DeleteRoomBySessionName(room.SessionName); err != nil { if err := s.repo.DeleteRoomBySessionName(room.SessionName); err != nil {
log.Printf("대기방 삭제 실패: %v", err) log.Printf("대기방 삭제 실패: %v", err)
} }
_ = s.repo.ResetRoomSlot(room.SessionName) cleanupSlot := room.SlotSessionName
if cleanupSlot == "" {
cleanupSlot = room.SessionName
}
_ = s.repo.ResetRoomSlot(cleanupSlot)
} }
} }
} }
@@ -527,10 +549,15 @@ func (s *Service) CheckStaleSlots() {
// ResetRoom resets a room slot back to idle and cleans up any lingering BossRoom records. // ResetRoom resets a room slot back to idle and cleans up any lingering BossRoom records.
// Called by the dedicated server after a raid ends and the runner is recycled. // Called by the dedicated server after a raid ends and the runner is recycled.
// sessionName here is the slot's original session name (not the unique per-entry name).
func (s *Service) ResetRoom(sessionName string) error { func (s *Service) ResetRoom(sessionName string) error {
// 완료/실패되지 않은 BossRoom 레코드 정리 (waiting/in_progress 상태) // 고유 세션명 BossRoom 정리 (slot_session_name으로 검색)
if err := s.repo.DeleteRoomBySlotSessionName(sessionName); err != nil {
log.Printf("BossRoom 레코드 정리 실패 (by slot): %s: %v", sessionName, err)
}
// 하위 호환: 원래 세션명으로도 시도
if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil { if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil {
log.Printf("BossRoom 레코드 정리 실패: %s: %v", sessionName, err) // 이미 삭제되었을 수 있으므로 무시
} }
return s.repo.ResetRoomSlot(sessionName) return s.repo.ResetRoomSlot(sessionName)
} }