feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
- swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개) - /swagger/ 경로에 Swagger UI 제공 - 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋) - 플레이어 레벨/경험치 시스템 및 스탯 성장 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,16 @@ func NewHandler(svc *Service) *Handler {
|
||||
return &Handler{svc: svc}
|
||||
}
|
||||
|
||||
// GetProfile 자신의 프로필 조회 (JWT 인증)
|
||||
// GetProfile godoc
|
||||
// @Summary 내 프로필 조회
|
||||
// @Description 현재 유저의 플레이어 프로필을 조회합니다
|
||||
// @Tags Player
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} docs.PlayerProfileResponse
|
||||
// @Failure 401 {object} docs.ErrorResponse
|
||||
// @Failure 404 {object} docs.ErrorResponse
|
||||
// @Router /api/player/profile [get]
|
||||
func (h *Handler) GetProfile(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
@@ -28,10 +37,22 @@ func (h *Handler) GetProfile(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(profile)
|
||||
return c.JSON(profileWithNextExp(profile))
|
||||
}
|
||||
|
||||
// UpdateProfile 자신의 프로필 수정 (JWT 인증)
|
||||
// UpdateProfile godoc
|
||||
// @Summary 프로필 수정
|
||||
// @Description 현재 유저의 닉네임을 수정합니다
|
||||
// @Tags Player
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param body body docs.UpdateProfileRequest true "수정할 프로필"
|
||||
// @Success 200 {object} player.PlayerProfile
|
||||
// @Failure 400 {object} docs.ErrorResponse
|
||||
// @Failure 401 {object} docs.ErrorResponse
|
||||
// @Failure 500 {object} docs.ErrorResponse
|
||||
// @Router /api/player/profile [put]
|
||||
func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("userID").(uint)
|
||||
if !ok {
|
||||
@@ -67,7 +88,17 @@ func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
|
||||
return c.JSON(profile)
|
||||
}
|
||||
|
||||
// InternalGetProfile 내부 API: username 쿼리 파라미터로 프로필 조회
|
||||
// InternalGetProfile godoc
|
||||
// @Summary 프로필 조회 (내부 API)
|
||||
// @Description username으로 플레이어 프로필을 조회합니다 (게임 서버용)
|
||||
// @Tags Internal - Player
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "유저명"
|
||||
// @Success 200 {object} docs.PlayerProfileResponse
|
||||
// @Failure 400 {object} docs.ErrorResponse
|
||||
// @Failure 404 {object} docs.ErrorResponse
|
||||
// @Router /api/internal/player/profile [get]
|
||||
func (h *Handler) InternalGetProfile(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
@@ -79,10 +110,50 @@ func (h *Handler) InternalGetProfile(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(profile)
|
||||
return c.JSON(profileWithNextExp(profile))
|
||||
}
|
||||
|
||||
// InternalSaveGameData 내부 API: username 쿼리 파라미터로 게임 데이터 저장
|
||||
// profileWithNextExp wraps a PlayerProfile with nextExp for JSON response.
|
||||
func profileWithNextExp(p *PlayerProfile) fiber.Map {
|
||||
nextExp := 0
|
||||
if p.Level < MaxLevel {
|
||||
nextExp = RequiredExp(p.Level)
|
||||
}
|
||||
return fiber.Map{
|
||||
"id": p.ID,
|
||||
"createdAt": p.CreatedAt,
|
||||
"updatedAt": p.UpdatedAt,
|
||||
"userId": p.UserID,
|
||||
"nickname": p.Nickname,
|
||||
"level": p.Level,
|
||||
"experience": p.Experience,
|
||||
"nextExp": nextExp,
|
||||
"maxHp": p.MaxHP,
|
||||
"maxMp": p.MaxMP,
|
||||
"attackPower": p.AttackPower,
|
||||
"attackRange": p.AttackRange,
|
||||
"sprintMultiplier": p.SprintMultiplier,
|
||||
"lastPosX": p.LastPosX,
|
||||
"lastPosY": p.LastPosY,
|
||||
"lastPosZ": p.LastPosZ,
|
||||
"lastRotY": p.LastRotY,
|
||||
"totalPlayTime": p.TotalPlayTime,
|
||||
}
|
||||
}
|
||||
|
||||
// InternalSaveGameData godoc
|
||||
// @Summary 게임 데이터 저장 (내부 API)
|
||||
// @Description username으로 게임 데이터를 저장합니다 (게임 서버용)
|
||||
// @Tags Internal - Player
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param username query string true "유저명"
|
||||
// @Param body body docs.GameDataRequest true "게임 데이터"
|
||||
// @Success 200 {object} docs.MessageResponse
|
||||
// @Failure 400 {object} docs.ErrorResponse
|
||||
// @Failure 500 {object} docs.ErrorResponse
|
||||
// @Router /api/internal/player/save [post]
|
||||
func (h *Handler) InternalSaveGameData(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
|
||||
71
internal/player/level.go
Normal file
71
internal/player/level.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package player
|
||||
|
||||
// MaxLevel is the maximum player level.
|
||||
const MaxLevel = 50
|
||||
|
||||
// RequiredExp returns the experience needed to reach the next level.
|
||||
// Formula: level^2 * 100
|
||||
func RequiredExp(level int) int {
|
||||
return level * level * 100
|
||||
}
|
||||
|
||||
// CalcStatsForLevel computes combat stats for a given level.
|
||||
func CalcStatsForLevel(level int) (maxHP, maxMP, attackPower float64) {
|
||||
maxHP = 100 + float64(level-1)*10
|
||||
maxMP = 50 + float64(level-1)*5
|
||||
attackPower = 10 + float64(level-1)*2
|
||||
return
|
||||
}
|
||||
|
||||
// LevelUpResult holds the result of applying experience gain.
|
||||
type LevelUpResult struct {
|
||||
OldLevel int `json:"oldLevel"`
|
||||
NewLevel int `json:"newLevel"`
|
||||
Experience int `json:"experience"`
|
||||
NextExp int `json:"nextExp"`
|
||||
MaxHP float64 `json:"maxHp"`
|
||||
MaxMP float64 `json:"maxMp"`
|
||||
AttackPower float64 `json:"attackPower"`
|
||||
LeveledUp bool `json:"leveledUp"`
|
||||
}
|
||||
|
||||
// ApplyExperience adds exp to current level/exp and applies level ups.
|
||||
// Returns the result including new stats if leveled up.
|
||||
func ApplyExperience(currentLevel, currentExp, gainedExp int) LevelUpResult {
|
||||
level := currentLevel
|
||||
exp := currentExp + gainedExp
|
||||
|
||||
// Process level ups
|
||||
for level < MaxLevel {
|
||||
required := RequiredExp(level)
|
||||
if exp < required {
|
||||
break
|
||||
}
|
||||
exp -= required
|
||||
level++
|
||||
}
|
||||
|
||||
// Cap at max level
|
||||
if level >= MaxLevel {
|
||||
level = MaxLevel
|
||||
exp = 0 // No more exp needed at max level
|
||||
}
|
||||
|
||||
maxHP, maxMP, attackPower := CalcStatsForLevel(level)
|
||||
|
||||
nextExp := 0
|
||||
if level < MaxLevel {
|
||||
nextExp = RequiredExp(level)
|
||||
}
|
||||
|
||||
return LevelUpResult{
|
||||
OldLevel: currentLevel,
|
||||
NewLevel: level,
|
||||
Experience: exp,
|
||||
NextExp: nextExp,
|
||||
MaxHP: maxHP,
|
||||
MaxMP: maxMP,
|
||||
AttackPower: attackPower,
|
||||
LeveledUp: level > currentLevel,
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,43 @@ func (s *Service) SaveGameDataByUsername(username string, data *GameDataRequest)
|
||||
return s.SaveGameData(userID, data)
|
||||
}
|
||||
|
||||
// GrantExperience adds experience to a player and handles level ups + stat recalculation.
|
||||
func (s *Service) GrantExperience(userID uint, exp int) (*LevelUpResult, error) {
|
||||
profile, err := s.repo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("프로필이 존재하지 않습니다")
|
||||
}
|
||||
|
||||
result := ApplyExperience(profile.Level, profile.Experience, exp)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"level": result.NewLevel,
|
||||
"experience": result.Experience,
|
||||
"max_hp": result.MaxHP,
|
||||
"max_mp": result.MaxMP,
|
||||
"attack_power": result.AttackPower,
|
||||
}
|
||||
|
||||
if err := s.repo.UpdateStats(userID, updates); err != nil {
|
||||
return nil, fmt.Errorf("레벨업 저장 실패: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GrantExperienceByUsername grants experience to a player by username.
|
||||
func (s *Service) GrantExperienceByUsername(username string, exp int) error {
|
||||
if s.userResolver == nil {
|
||||
return fmt.Errorf("userResolver가 설정되지 않았습니다")
|
||||
}
|
||||
userID, err := s.userResolver(username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("존재하지 않는 유저입니다")
|
||||
}
|
||||
_, err = s.GrantExperience(userID, exp)
|
||||
return err
|
||||
}
|
||||
|
||||
// GameDataRequest 게임 데이터 저장 요청 (nil 필드는 변경하지 않음).
|
||||
type GameDataRequest struct {
|
||||
Level *int `json:"level,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user