From ac6827aae5008183585d62211e33973081ce6747 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 23 Mar 2026 22:04:03 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=EB=B3=B4=EC=8A=A4=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9E=AC=EC=9E=85=EC=9E=A5=20=EB=B6=88=EA=B0=80=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BossRoom 세션명을 매 입장마다 고유하게 생성 (슬롯명_타임스탬프) - SlotSessionName 필드 추가로 슬롯 리셋 시 원래 슬롯명 사용 - DeleteRoomBySlotSessionName 추가 (dedicated server ResetRoom 대응) - CompleteRaid/FailRaid/cleanup에서 슬롯 리셋 로직 수정 Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/bossraid/model.go | 5 ++-- internal/bossraid/repository.go | 6 ++++ internal/bossraid/service.go | 53 +++++++++++++++++++++++++-------- 3 files changed, 49 insertions(+), 15 deletions(-) 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) }