diff --git a/internal/bossraid/model.go b/internal/bossraid/model.go index b763ef3..bdd670c 100644 --- a/internal/bossraid/model.go +++ b/internal/bossraid/model.go @@ -26,8 +26,9 @@ type BossRoom struct { CreatedAt time.Time `json:"createdAt" gorm:"index"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` - SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;not null"` - BossID int `json:"bossId" gorm:"index;not null"` + SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;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"` MaxPlayers int `json:"maxPlayers" gorm:"default:3;not null"` // Players is stored as a JSON text column for simplicity. diff --git a/internal/bossraid/repository.go b/internal/bossraid/repository.go index 8baa55c..521e57b 100644 --- a/internal/bossraid/repository.go +++ b/internal/bossraid/repository.go @@ -224,6 +224,12 @@ func (r *Repository) DeleteRoomBySessionName(sessionName string) 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 // past the given threshold and resets their associated RoomSlots to idle. // This handles cases where players disconnect during loading before the Fusion session starts. diff --git a/internal/bossraid/service.go b/internal/bossraid/service.go index 3d76a5c..7b26b9d 100644 --- a/internal/bossraid/service.go +++ b/internal/bossraid/service.go @@ -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{ - SessionName: slot.SessionName, - BossID: bossID, - Status: StatusWaiting, - MaxPlayers: len(usernames), - Players: string(playersJSON), + SessionName: uniqueSession, + SlotSessionName: slot.SessionName, // 슬롯 리셋용 원래 이름 보존 + BossID: bossID, + Status: StatusWaiting, + MaxPlayers: len(usernames), + Players: string(playersJSON), } if err := txRepo.Create(room); err != nil { 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 { log.Printf("BossRoom 삭제 실패 (complete): %s: %v", sessionName, err) } - if err := s.repo.ResetRoomSlot(sessionName); err != nil { - log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err) + // SlotSessionName으로 슬롯 리셋 (고유 세션명이 아닌 원래 슬롯명) + 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 @@ -295,8 +305,12 @@ func (s *Service) FailRaid(sessionName string) (*BossRoom, error) { if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil { log.Printf("BossRoom 삭제 실패 (fail): %s: %v", sessionName, err) } - if err := s.repo.ResetRoomSlot(sessionName); err != nil { - log.Printf("슬롯 리셋 실패 (fail): %s: %v", sessionName, err) + slotName := room.SlotSessionName + if slotName == "" { + slotName = sessionName + } + if err := s.repo.ResetRoomSlot(slotName); err != nil { + log.Printf("슬롯 리셋 실패 (fail): %s: %v", slotName, err) } 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 { 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) } 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 { 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. // 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 { - // 완료/실패되지 않은 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 { - log.Printf("BossRoom 레코드 정리 실패: %s: %v", sessionName, err) + // 이미 삭제되었을 수 있으므로 무시 } return s.repo.ResetRoomSlot(sessionName) }