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": "게임 데이터가 저장되었습니다"}) }