Files
a301_server/internal/player/handler.go
tolelom befea9dd68
Some checks failed
Server CI/CD / lint-and-build (push) Failing after 12m3s
Server CI/CD / deploy (push) Has been cancelled
feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
- swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개)
- /swagger/ 경로에 Swagger UI 제공
- 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋)
- 플레이어 레벨/경험치 시스템 및 스탯 성장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 17:51:37 +09:00

176 lines
5.9 KiB
Go

package player
import (
"log"
"strings"
"unicode"
"github.com/gofiber/fiber/v2"
)
type Handler struct {
svc *Service
}
func NewHandler(svc *Service) *Handler {
return &Handler{svc: svc}
}
// 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 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 올바르지 않습니다"})
}
profile, err := h.svc.GetProfile(userID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(profileWithNextExp(profile))
}
// 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 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 올바르지 않습니다"})
}
var req struct {
Nickname string `json:"nickname"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
req.Nickname = strings.TrimSpace(req.Nickname)
if req.Nickname != "" {
nicknameRunes := []rune(req.Nickname)
if len(nicknameRunes) < 2 || len(nicknameRunes) > 30 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "닉네임은 2~30자여야 합니다"})
}
for _, r := range nicknameRunes {
if unicode.IsControl(r) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "닉네임에 허용되지 않는 문자가 포함되어 있습니다"})
}
}
}
profile, err := h.svc.UpdateProfile(userID, req.Nickname)
if err != nil {
log.Printf("프로필 수정 실패 (userID=%d): %v", userID, err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "서버 오류가 발생했습니다"})
}
return c.JSON(profile)
}
// 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 == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username 파라미터가 필요합니다"})
}
profile, err := h.svc.GetProfileByUsername(username)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(profileWithNextExp(profile))
}
// 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 == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username 파라미터가 필요합니다"})
}
var req GameDataRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
}
if err := h.svc.SaveGameDataByUsername(username, &req); err != nil {
// Username from internal API (ServerAuth protected) — low risk of injection
log.Printf("게임 데이터 저장 실패 (username=%s): %v", username, err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "서버 오류가 발생했습니다"})
}
return c.JSON(fiber.Map{"message": "게임 데이터가 저장되었습니다"})
}