Compare commits

4 Commits

Author SHA1 Message Date
688d4b34df Fix: CORS AllowHeaders에 X-Requested-With 추가
웹 클라이언트의 fetch 요청에서 X-Requested-With 헤더가
CORS preflight에서 차단되는 문제 수정.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:01:06 +09:00
4393503245 Fix: GrantExperience에서 프로필 미존재 시 자동 생성 fallback 추가
FindByUserID → GetProfile(auto-create 포함)로 변경하여
프로필 없는 유저의 경험치 지급 실패 방지.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:15:49 +09:00
d6c75dcaad Fix: BossRoom 레코드 미삭제로 데디서버 슬롯 재사용 불가 수정
CompleteRaid/FailRaid에서 슬롯 리셋 전 BossRoom hard-delete 추가.
기존에는 BossRoom.SessionName uniqueIndex 충돌로 한 번 사용된 슬롯의
재사용이 불가능했음 (10개 중 점점 사용 가능 슬롯 감소).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:00:14 +09:00
83c583c04d Fix: 지갑 미발견 시 자동 생성 fallback 추가
resolveUsername()에서 user_wallets 레코드가 없는 유저(레거시/마이그레이션 누락)에 대해
CreateWallet을 자동 호출하여 지갑을 즉시 생성. unique constraint 충돌 시 재조회로
동시성 안전 처리.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 11:28:38 +09:00
4 changed files with 30 additions and 10 deletions

View File

@@ -252,7 +252,10 @@ func (s *Service) CompleteRaid(sessionName string, rewards []PlayerReward) (*Bos
} }
} }
// Reset slot to idle so it can accept new raids // BossRoom 삭제 후 슬롯 리셋 — 다음 파티가 즉시 슬롯 재사용 가능
if err := s.repo.DeleteRoomBySessionName(sessionName); err != nil {
log.Printf("BossRoom 삭제 실패 (complete): %s: %v", sessionName, err)
}
if err := s.repo.ResetRoomSlot(sessionName); err != nil { if err := s.repo.ResetRoomSlot(sessionName); err != nil {
log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err) log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err)
} }
@@ -276,15 +279,20 @@ func (s *Service) FailRaid(sessionName string) (*BossRoom, error) {
return nil, fmt.Errorf("상태 업데이트 실패: %w", err) return nil, fmt.Errorf("상태 업데이트 실패: %w", err)
} }
// Reset slot to idle so it can accept new raids // 응답용 room 조회 (삭제 전에 수행)
if err := s.repo.ResetRoomSlot(sessionName); err != nil {
log.Printf("슬롯 리셋 실패 (fail): %s: %v", sessionName, err)
}
room, err := s.repo.FindBySessionName(sessionName) room, err := s.repo.FindBySessionName(sessionName)
if err != nil { if err != nil {
return nil, fmt.Errorf("방을 찾을 수 없습니다: %w", err) return nil, fmt.Errorf("방을 찾을 수 없습니다: %w", err)
} }
// BossRoom 삭제 후 슬롯 리셋 — 다음 파티가 즉시 슬롯 재사용 가능
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)
}
return room, nil return room, nil
} }

View File

@@ -34,6 +34,8 @@ func (s *Service) SetUserResolver(fn func(username string) (uint, error)) {
} }
// resolveUsername converts a username to the user's on-chain pubKeyHex. // resolveUsername converts a username to the user's on-chain pubKeyHex.
// If the user exists but has no wallet (e.g. legacy user or failed creation),
// a wallet is auto-created on the fly.
func (s *Service) resolveUsername(username string) (string, error) { func (s *Service) resolveUsername(username string) (string, error) {
if s.userResolver == nil { if s.userResolver == nil {
return "", fmt.Errorf("user resolver not configured") return "", fmt.Errorf("user resolver not configured")
@@ -44,7 +46,17 @@ func (s *Service) resolveUsername(username string) (string, error) {
} }
uw, err := s.repo.FindByUserID(userID) uw, err := s.repo.FindByUserID(userID)
if err != nil { if err != nil {
return "", fmt.Errorf("wallet not found") // 지갑이 없으면 자동 생성 시도
uw, err = s.CreateWallet(userID)
if err != nil {
// unique constraint 위반 — 다른 고루틴이 먼저 생성 완료
uw, err = s.repo.FindByUserID(userID)
if err != nil {
return "", fmt.Errorf("wallet auto-creation failed: %w", err)
}
} else {
log.Printf("INFO: auto-created wallet for userID=%d (username=%s)", userID, username)
}
} }
return uw.PubKeyHex, nil return uw.PubKeyHex, nil
} }

View File

@@ -167,9 +167,9 @@ func (s *Service) SaveGameDataByUsername(username string, data *GameDataRequest)
// GrantExperience adds experience to a player and handles level ups + stat recalculation. // GrantExperience adds experience to a player and handles level ups + stat recalculation.
func (s *Service) GrantExperience(userID uint, exp int) (*LevelUpResult, error) { func (s *Service) GrantExperience(userID uint, exp int) (*LevelUpResult, error) {
profile, err := s.repo.FindByUserID(userID) profile, err := s.GetProfile(userID)
if err != nil { if err != nil {
return nil, fmt.Errorf("프로필이 존재하지 않습니다") return nil, fmt.Errorf("프로필 조회/생성 실패: %w", err)
} }
result := ApplyExperience(profile.Level, profile.Experience, exp) result := ApplyExperience(profile.Level, profile.Experience, exp)

View File

@@ -33,7 +33,7 @@ func New() *fiber.App {
app.Use(middleware.SecurityHeaders) app.Use(middleware.SecurityHeaders)
app.Use(cors.New(cors.Config{ app.Use(cors.New(cors.Config{
AllowOrigins: "https://a301.tolelom.xyz", AllowOrigins: "https://a301.tolelom.xyz",
AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key", AllowHeaders: "Origin, Content-Type, Authorization, Idempotency-Key, X-API-Key, X-Requested-With",
AllowMethods: "GET, POST, PUT, PATCH, DELETE", AllowMethods: "GET, POST, PUT, PATCH, DELETE",
AllowCredentials: true, AllowCredentials: true,
})) }))