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:
2026-03-18 16:42:03 +09:00
parent 8da2bdab12
commit f4d862b47f
19 changed files with 1570 additions and 322 deletions

View File

@@ -7,6 +7,8 @@ import (
"path/filepath"
"strings"
"a301_server/pkg/apperror"
"github.com/gofiber/fiber/v2"
)
@@ -30,7 +32,7 @@ func NewHandler(svc *Service, baseURL string) *Handler {
func (h *Handler) GetInfo(c *fiber.Ctx) error {
info, err := h.svc.GetInfo()
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "다운로드 정보가 없습니다"})
return apperror.NotFound("다운로드 정보가 없습니다")
}
return c.JSON(info)
}
@@ -54,17 +56,17 @@ func (h *Handler) Upload(c *fiber.Ctx) error {
// 경로 순회 방지: 디렉토리 구분자 제거, 기본 파일명만 사용
filename = filepath.Base(filename)
if !strings.HasSuffix(strings.ToLower(filename), ".zip") {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "zip 파일만 업로드 가능합니다"})
return apperror.BadRequest("zip 파일만 업로드 가능합니다")
}
if len(filename) > 200 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "파일명이 너무 깁니다"})
return apperror.BadRequest("파일명이 너무 깁니다")
}
body := c.Request().BodyStream()
info, err := h.svc.Upload(filename, body, h.baseURL)
if err != nil {
log.Printf("game upload failed: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "게임 파일 업로드에 실패했습니다"})
return apperror.Internal("게임 파일 업로드에 실패했습니다")
}
return c.JSON(info)
}
@@ -80,7 +82,7 @@ func (h *Handler) Upload(c *fiber.Ctx) error {
func (h *Handler) ServeFile(c *fiber.Ctx) error {
path := h.svc.GameFilePath()
if _, err := os.Stat(path); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"})
return apperror.NotFound("파일이 없습니다")
}
info, _ := h.svc.GetInfo()
filename := "game.zip"
@@ -108,7 +110,7 @@ func (h *Handler) UploadLauncher(c *fiber.Ctx) error {
info, err := h.svc.UploadLauncher(body, h.baseURL)
if err != nil {
log.Printf("launcher upload failed: %v", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "런처 업로드에 실패했습니다"})
return apperror.Internal("런처 업로드에 실패했습니다")
}
return c.JSON(info)
}
@@ -124,7 +126,7 @@ func (h *Handler) UploadLauncher(c *fiber.Ctx) error {
func (h *Handler) ServeLauncher(c *fiber.Ctx) error {
path := h.svc.LauncherFilePath()
if _, err := os.Stat(path); err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "파일이 없습니다"})
return apperror.NotFound("파일이 없습니다")
}
c.Set("Content-Disposition", `attachment; filename="launcher.exe"`)
return c.SendFile(path)