package chain import ( "log" "strconv" "github.com/gofiber/fiber/v2" "github.com/tolelom/tolchain/core" ) const maxLimit = 200 const maxIDLength = 256 // max length for string IDs (assetId, listingId, etc.) type Handler struct { svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } func getUserID(c *fiber.Ctx) (uint, error) { uid, ok := c.Locals("userID").(uint) if !ok { return 0, fiber.NewError(fiber.StatusUnauthorized, "인증이 필요합니다") } return uid, nil } func parsePagination(c *fiber.Ctx) (int, int) { offset, _ := strconv.Atoi(c.Query("offset", "0")) limit, _ := strconv.Atoi(c.Query("limit", "50")) if offset < 0 { offset = 0 } if limit <= 0 { limit = 50 } else if limit > maxLimit { limit = maxLimit } return offset, limit } func validID(s string) bool { return s != "" && len(s) <= maxIDLength } func chainError(c *fiber.Ctx, status int, userMsg string, err error) error { log.Printf("chain error: %s: %v", userMsg, err) return c.Status(status).JSON(fiber.Map{"error": userMsg}) } // ---- Query Handlers ---- // GetWalletInfo godoc // @Summary 지갑 정보 조회 // @Description 현재 유저의 블록체인 지갑 정보를 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Success 200 {object} docs.WalletInfoResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 404 {object} docs.ErrorResponse // @Router /api/chain/wallet [get] func (h *Handler) GetWalletInfo(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } w, err := h.svc.GetWallet(userID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "지갑을 찾을 수 없습니다"}) } return c.JSON(fiber.Map{ "address": w.Address, "pubKeyHex": w.PubKeyHex, }) } // GetBalance godoc // @Summary 잔액 조회 // @Description 현재 유저의 토큰 잔액을 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/balance [get] func (h *Handler) GetBalance(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } result, err := h.svc.GetBalance(userID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "잔액 조회에 실패했습니다", err) } return c.JSON(result) } // GetAssets godoc // @Summary 에셋 목록 조회 // @Description 현재 유저의 블록체인 에셋 목록을 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Param offset query int false "시작 위치" default(0) // @Param limit query int false "조회 수" default(50) // @Success 200 {object} map[string]interface{} // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/assets [get] func (h *Handler) GetAssets(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } offset, limit := parsePagination(c) result, err := h.svc.GetAssets(userID, offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // GetAsset godoc // @Summary 에셋 상세 조회 // @Description 특정 에셋의 상세 정보를 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Param id path string true "에셋 ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/asset/{id} [get] func (h *Handler) GetAsset(c *fiber.Ctx) error { assetID := c.Params("id") if !validID(assetID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 asset id가 필요합니다"}) } result, err := h.svc.GetAsset(assetID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // GetInventory godoc // @Summary 인벤토리 조회 // @Description 현재 유저의 인벤토리를 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Success 200 {object} map[string]interface{} // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/inventory [get] func (h *Handler) GetInventory(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } result, err := h.svc.GetInventory(userID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "인벤토리 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // GetMarketListings godoc // @Summary 마켓 목록 조회 // @Description 마켓에 등록된 매물 목록을 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Param offset query int false "시작 위치" default(0) // @Param limit query int false "조회 수" default(50) // @Success 200 {object} map[string]interface{} // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/market [get] func (h *Handler) GetMarketListings(c *fiber.Ctx) error { offset, limit := parsePagination(c) result, err := h.svc.GetMarketListings(offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // GetMarketListing godoc // @Summary 마켓 매물 상세 조회 // @Description 특정 마켓 매물의 상세 정보를 조회합니다 // @Tags Chain // @Produce json // @Security BearerAuth // @Param id path string true "매물 ID" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/market/{id} [get] func (h *Handler) GetMarketListing(c *fiber.Ctx) error { listingID := c.Params("id") if !validID(listingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효한 listing id가 필요합니다"}) } result, err := h.svc.GetListing(listingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // ---- User Transaction Handlers ---- // Transfer godoc // @Summary 토큰 전송 // @Description 다른 유저에게 토큰을 전송합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.TransferRequest true "전송 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/transfer [post] func (h *Handler) Transfer(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { To string `json:"to"` Amount uint64 `json:"amount"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.To) || req.Amount == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "to와 amount는 필수입니다"}) } result, err := h.svc.Transfer(userID, req.To, req.Amount) if err != nil { return chainError(c, fiber.StatusInternalServerError, "전송에 실패했습니다", err) } return c.JSON(result) } // TransferAsset godoc // @Summary 에셋 전송 // @Description 다른 유저에게 에셋을 전송합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.TransferAssetRequest true "전송 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/asset/transfer [post] func (h *Handler) TransferAsset(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` To string `json:"to"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || !validID(req.To) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 to는 필수입니다"}) } result, err := h.svc.TransferAsset(userID, req.AssetID, req.To) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 전송에 실패했습니다", err) } return c.JSON(result) } // ListOnMarket godoc // @Summary 마켓 등록 // @Description 에셋을 마켓에 등록합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.ListOnMarketRequest true "등록 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/market/list [post] func (h *Handler) ListOnMarket(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` Price uint64 `json:"price"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || req.Price == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 price는 필수입니다"}) } result, err := h.svc.ListOnMarket(userID, req.AssetID, req.Price) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 등록에 실패했습니다", err) } return c.JSON(result) } // BuyFromMarket godoc // @Summary 마켓 구매 // @Description 마켓에서 매물을 구매합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.BuyFromMarketRequest true "구매 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/market/buy [post] func (h *Handler) BuyFromMarket(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { ListingID string `json:"listingId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ListingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"}) } result, err := h.svc.BuyFromMarket(userID, req.ListingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 구매에 실패했습니다", err) } return c.JSON(result) } // CancelListing godoc // @Summary 마켓 등록 취소 // @Description 마켓에 등록한 매물을 취소합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.CancelListingRequest true "취소 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/market/cancel [post] func (h *Handler) CancelListing(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { ListingID string `json:"listingId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ListingID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "listingId는 필수입니다"}) } result, err := h.svc.CancelListing(userID, req.ListingID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "마켓 취소에 실패했습니다", err) } return c.JSON(result) } // EquipItem godoc // @Summary 아이템 장착 // @Description 에셋을 장비 슬롯에 장착합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.EquipItemRequest true "장착 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/inventory/equip [post] func (h *Handler) EquipItem(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` Slot string `json:"slot"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) || !validID(req.Slot) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId와 slot은 필수입니다"}) } result, err := h.svc.EquipItem(userID, req.AssetID, req.Slot) if err != nil { return chainError(c, fiber.StatusInternalServerError, "장착에 실패했습니다", err) } return c.JSON(result) } // UnequipItem godoc // @Summary 아이템 장착 해제 // @Description 에셋의 장비 슬롯 장착을 해제합니다 // @Tags Chain - Transactions // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.UnequipItemRequest true "해제 정보" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/inventory/unequip [post] func (h *Handler) UnequipItem(c *fiber.Ctx) error { userID, err := getUserID(c) if err != nil { return err } var req struct { AssetID string `json:"assetId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.AssetID) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "assetId는 필수입니다"}) } result, err := h.svc.UnequipItem(userID, req.AssetID) if err != nil { return chainError(c, fiber.StatusInternalServerError, "장착 해제에 실패했습니다", err) } return c.JSON(result) } // ---- Operator (Admin) Transaction Handlers ---- // MintAsset godoc // @Summary 에셋 발행 (관리자) // @Description 새 에셋을 발행합니다 // @Tags Chain - Admin // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.MintAssetRequest true "발행 정보" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 403 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/admin/mint [post] func (h *Handler) MintAsset(c *fiber.Ctx) error { var req struct { TemplateID string `json:"templateId"` OwnerPubKey string `json:"ownerPubKey"` Properties map[string]any `json:"properties"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.TemplateID) || !validID(req.OwnerPubKey) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 ownerPubKey는 필수입니다"}) } result, err := h.svc.MintAsset(req.TemplateID, req.OwnerPubKey, req.Properties) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 발행에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // GrantReward godoc // @Summary 보상 지급 (관리자) // @Description 유저에게 토큰 및 에셋 보상을 지급합니다 // @Tags Chain - Admin // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.GrantRewardRequest true "보상 정보" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 403 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/admin/reward [post] func (h *Handler) GrantReward(c *fiber.Ctx) error { var req struct { RecipientPubKey string `json:"recipientPubKey"` TokenAmount uint64 `json:"tokenAmount"` Assets []core.MintAssetPayload `json:"assets"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.RecipientPubKey) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "recipientPubKey는 필수입니다"}) } result, err := h.svc.GrantReward(req.RecipientPubKey, req.TokenAmount, req.Assets) if err != nil { return chainError(c, fiber.StatusInternalServerError, "보상 지급에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // RegisterTemplate godoc // @Summary 템플릿 등록 (관리자) // @Description 새 에셋 템플릿을 등록합니다 // @Tags Chain - Admin // @Accept json // @Produce json // @Security BearerAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.RegisterTemplateRequest true "템플릿 정보" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 401 {object} docs.ErrorResponse // @Failure 403 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/chain/admin/template [post] func (h *Handler) RegisterTemplate(c *fiber.Ctx) error { var req struct { ID string `json:"id"` Name string `json:"name"` Schema map[string]any `json:"schema"` Tradeable bool `json:"tradeable"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.ID) || !validID(req.Name) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id와 name은 필수입니다"}) } result, err := h.svc.RegisterTemplate(req.ID, req.Name, req.Schema, req.Tradeable) if err != nil { return chainError(c, fiber.StatusInternalServerError, "템플릿 등록에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // ---- Internal Handlers (game server, username-based) ---- // InternalGrantReward godoc // @Summary 보상 지급 (내부 API) // @Description username으로 유저에게 보상을 지급합니다 (게임 서버용) // @Tags Internal - Chain // @Accept json // @Produce json // @Security ApiKeyAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.InternalGrantRewardRequest true "보상 정보" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/internal/chain/reward [post] func (h *Handler) InternalGrantReward(c *fiber.Ctx) error { var req struct { Username string `json:"username"` TokenAmount uint64 `json:"tokenAmount"` Assets []core.MintAssetPayload `json:"assets"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.Username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GrantRewardByUsername(req.Username, req.TokenAmount, req.Assets) if err != nil { return chainError(c, fiber.StatusInternalServerError, "보상 지급에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // InternalMintAsset godoc // @Summary 에셋 발행 (내부 API) // @Description username으로 에셋을 발행합니다 (게임 서버용) // @Tags Internal - Chain // @Accept json // @Produce json // @Security ApiKeyAuth // @Param Idempotency-Key header string true "멱등성 키" // @Param body body docs.InternalMintAssetRequest true "발행 정보" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/internal/chain/mint [post] func (h *Handler) InternalMintAsset(c *fiber.Ctx) error { var req struct { TemplateID string `json:"templateId"` Username string `json:"username"` Properties map[string]any `json:"properties"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if !validID(req.TemplateID) || !validID(req.Username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "templateId와 username은 필수입니다"}) } result, err := h.svc.MintAssetByUsername(req.TemplateID, req.Username, req.Properties) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 발행에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(result) } // InternalGetBalance godoc // @Summary 잔액 조회 (내부 API) // @Description username으로 잔액을 조회합니다 (게임 서버용) // @Tags Internal - Chain // @Produce json // @Security ApiKeyAuth // @Param username query string true "유저명" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/internal/chain/balance [get] func (h *Handler) InternalGetBalance(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GetBalanceByUsername(username) if err != nil { return chainError(c, fiber.StatusInternalServerError, "잔액 조회에 실패했습니다", err) } return c.JSON(result) } // InternalGetAssets godoc // @Summary 에셋 목록 조회 (내부 API) // @Description username으로 에셋 목록을 조회합니다 (게임 서버용) // @Tags Internal - Chain // @Produce json // @Security ApiKeyAuth // @Param username query string true "유저명" // @Param offset query int false "시작 위치" default(0) // @Param limit query int false "조회 수" default(50) // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/internal/chain/assets [get] func (h *Handler) InternalGetAssets(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } offset, limit := parsePagination(c) result, err := h.svc.GetAssetsByUsername(username, offset, limit) if err != nil { return chainError(c, fiber.StatusInternalServerError, "에셋 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) } // InternalGetInventory godoc // @Summary 인벤토리 조회 (내부 API) // @Description username으로 인벤토리를 조회합니다 (게임 서버용) // @Tags Internal - Chain // @Produce json // @Security ApiKeyAuth // @Param username query string true "유저명" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} docs.ErrorResponse // @Failure 500 {object} docs.ErrorResponse // @Router /api/internal/chain/inventory [get] func (h *Handler) InternalGetInventory(c *fiber.Ctx) error { username := c.Query("username") if !validID(username) { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "username은 필수입니다"}) } result, err := h.svc.GetInventoryByUsername(username) if err != nil { return chainError(c, fiber.StatusInternalServerError, "인벤토리 조회에 실패했습니다", err) } c.Set("Content-Type", "application/json") return c.Send(result) }