feat: 보상 재시도 + TX 확정 대기 + 에러 포맷 통일 + 품질 고도화
- 보상 지급 실패 시 즉시 재시도(3회 backoff) + DB 기록 + 백그라운드 워커 재시도 - WaitForTx 폴링으로 블록체인 TX 확정 대기, SendTxAndWait 편의 메서드 - chain 트랜잭션 코드 중복 제거 (userTx/operatorTx 헬퍼, 50% 감소) - AppError 기반 에러 응답 포맷 통일 (8개 코드, 전 핸들러 마이그레이션) - TX 에러 분류 + 한국어 사용자 메시지 매핑 (11가지 패턴) - player 서비스 테스트 20개 + chain WaitForTx 테스트 10개 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"a301_server/pkg/apperror"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
@@ -29,12 +31,12 @@ func NewHandler(svc *Service) *Handler {
|
||||
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": "인증 정보가 올바르지 않습니다"})
|
||||
return apperror.Unauthorized("인증 정보가 올바르지 않습니다")
|
||||
}
|
||||
|
||||
profile, err := h.svc.GetProfile(userID)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
|
||||
return apperror.NotFound(err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(profileWithNextExp(profile))
|
||||
@@ -56,25 +58,25 @@ func (h *Handler) GetProfile(c *fiber.Ctx) error {
|
||||
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": "인증 정보가 올바르지 않습니다"})
|
||||
return apperror.Unauthorized("인증 정보가 올바르지 않습니다")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Nickname string `json:"nickname"`
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
return apperror.ErrBadRequest
|
||||
}
|
||||
|
||||
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자여야 합니다"})
|
||||
return apperror.BadRequest("닉네임은 2~30자여야 합니다")
|
||||
}
|
||||
for _, r := range nicknameRunes {
|
||||
if unicode.IsControl(r) {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "닉네임에 허용되지 않는 문자가 포함되어 있습니다"})
|
||||
return apperror.BadRequest("닉네임에 허용되지 않는 문자가 포함되어 있습니다")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,7 +84,7 @@ func (h *Handler) UpdateProfile(c *fiber.Ctx) 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 apperror.ErrInternal
|
||||
}
|
||||
|
||||
return c.JSON(profile)
|
||||
@@ -102,12 +104,12 @@ func (h *Handler) UpdateProfile(c *fiber.Ctx) error {
|
||||
func (h *Handler) InternalGetProfile(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username 파라미터가 필요합니다"})
|
||||
return apperror.BadRequest("username 파라미터가 필요합니다")
|
||||
}
|
||||
|
||||
profile, err := h.svc.GetProfileByUsername(username)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()})
|
||||
return apperror.NotFound(err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(profileWithNextExp(profile))
|
||||
@@ -157,18 +159,18 @@ func profileWithNextExp(p *PlayerProfile) fiber.Map {
|
||||
func (h *Handler) InternalSaveGameData(c *fiber.Ctx) error {
|
||||
username := c.Query("username")
|
||||
if username == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username 파라미터가 필요합니다"})
|
||||
return apperror.BadRequest("username 파라미터가 필요합니다")
|
||||
}
|
||||
|
||||
var req GameDataRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"})
|
||||
return apperror.ErrBadRequest
|
||||
}
|
||||
|
||||
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 apperror.ErrInternal
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{"message": "게임 데이터가 저장되었습니다"})
|
||||
|
||||
Reference in New Issue
Block a user