From befea9dd685ffc3f701dd0e1ea2f31ce822fdec8 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Mon, 16 Mar 2026 17:51:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Swagger=20API=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20+=20=EB=B3=B4=EC=8A=A4=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EB=93=9C/=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=20=EC=8B=9C=EC=8A=A4=ED=85=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개) - /swagger/ 경로에 Swagger UI 제공 - 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋) - 플레이어 레벨/경험치 시스템 및 스탯 성장 Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/docs.go | 4121 ++++++++++++++++++++++++++++++ docs/swagger.json | 4097 +++++++++++++++++++++++++++++ docs/swagger.yaml | 2686 +++++++++++++++++++ docs/swagger_types.go | 348 +++ go.mod | 56 +- go.sum | 86 + internal/announcement/handler.go | 53 + internal/auth/handler.go | 140 +- internal/bossraid/handler.go | 239 +- internal/bossraid/model.go | 38 + internal/bossraid/repository.go | 164 ++ internal/bossraid/service.go | 158 +- internal/chain/handler.go | 285 ++- internal/download/handler.go | 52 +- internal/player/handler.go | 83 +- internal/player/level.go | 71 + internal/player/service.go | 37 + main.go | 32 +- routes/routes.go | 8 + 19 files changed, 12692 insertions(+), 62 deletions(-) create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 docs/swagger_types.go create mode 100644 internal/player/level.go diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..1683683 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,4121 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/announcements/": { + "get": { + "description": "공지사항 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 공지사항을 생성합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 생성 (관리자)", + "parameters": [ + { + "description": "공지사항 내용", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CreateAnnouncementRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/announcements/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "공지사항을 수정합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 수정 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "공지사항 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "수정할 내용", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateAnnouncementRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "공지사항을 삭제합니다", + "tags": [ + "Announcements" + ], + "summary": "공지사항 삭제 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "공지사항 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/launch-ticket": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 런처용 일회성 티켓을 발급합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "런처 티켓 발급", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LaunchTicketResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "description": "사용자 인증 후 JWT 토큰을 발급합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "로그인", + "parameters": [ + { + "description": "로그인 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 세션을 무효화합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "로그아웃", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/redeem-ticket": { + "post": { + "description": "일회성 티켓을 Access 토큰으로 교환합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "런처 티켓 교환", + "parameters": [ + { + "description": "티켓", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RedeemTicketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RedeemTicketResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/refresh": { + "post": { + "description": "Refresh 토큰으로 새 Access 토큰을 발급합니다 (쿠키 또는 body)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "토큰 갱신", + "parameters": [ + { + "description": "Refresh 토큰 (쿠키 우선)", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/docs.RefreshRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RefreshResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/register": { + "post": { + "description": "새로운 사용자 계정을 생성합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "회원가입", + "parameters": [ + { + "description": "회원가입 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/ssafy/callback": { + "post": { + "description": "SSAFY 인가 코드를 교환하여 로그인합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "SSAFY OAuth 콜백", + "parameters": [ + { + "description": "인가 코드", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SSAFYCallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/ssafy/login": { + "get": { + "description": "SSAFY OAuth 로그인 URL을 생성합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "SSAFY 로그인 URL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.SSAFYLoginURLResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/bossraid/entry": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 클라이언트에서 보스 레이드 입장을 요청합니다. 인증된 유저가 입장 목록에 포함되어야 합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Boss Raid" + ], + "summary": "보스 레이드 입장 요청", + "parameters": [ + { + "description": "입장 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RequestEntryAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.RequestEntryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/bossraid/my-entry-token": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 대기 중인 입장 토큰을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Boss Raid" + ], + "summary": "내 입장 토큰 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MyEntryTokenResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/mint": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 에셋을 발행합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "에셋 발행 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "발행 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.MintAssetRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/reward": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저에게 토큰 및 에셋 보상을 지급합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "보상 지급 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "보상 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.GrantRewardRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/template": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 에셋 템플릿을 등록합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "템플릿 등록 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "템플릿 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/asset/transfer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "다른 유저에게 에셋을 전송합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "에셋 전송", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "전송 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.TransferAssetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/asset/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "특정 에셋의 상세 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "에셋 상세 조회", + "parameters": [ + { + "type": "string", + "description": "에셋 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/assets": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 블록체인 에셋 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "에셋 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/balance": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 토큰 잔액을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "잔액 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 인벤토리를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "인벤토리 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory/equip": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋을 장비 슬롯에 장착합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "아이템 장착", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "장착 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.EquipItemRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory/unequip": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋의 장비 슬롯 장착을 해제합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "아이템 장착 해제", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "해제 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UnequipItemRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에 등록된 매물 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "마켓 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/buy": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에서 매물을 구매합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 구매", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "구매 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.BuyFromMarketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에 등록한 매물을 취소합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 등록 취소", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "취소 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CancelListingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/list": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋을 마켓에 등록합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 등록", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "등록 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ListOnMarketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "특정 마켓 매물의 상세 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "마켓 매물 상세 조회", + "parameters": [ + { + "type": "string", + "description": "매물 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/transfer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "다른 유저에게 토큰을 전송합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "토큰 전송", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "전송 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.TransferRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/wallet": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 블록체인 지갑 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "지갑 정보 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.WalletInfoResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/file": { + "get": { + "description": "게임 zip 파일을 다운로드합니다", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Download" + ], + "summary": "게임 파일 다운로드", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/info": { + "get": { + "description": "게임 및 런처 다운로드 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "다운로드 정보 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/launcher": { + "get": { + "description": "런처 실행 파일을 다운로드합니다", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Download" + ], + "summary": "런처 다운로드", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/upload/game": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 zip 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.", + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "게임 파일 업로드 (관리자)", + "parameters": [ + { + "type": "string", + "default": "game.zip", + "description": "파일명", + "name": "filename", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/upload/launcher": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "런처 실행 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.", + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "런처 업로드 (관리자)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/auth/verify": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "JWT 토큰을 검증하고 username을 반환합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Auth" + ], + "summary": "토큰 검증 (내부 API)", + "parameters": [ + { + "description": "검증할 토큰", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.VerifyTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.VerifyTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/complete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "보스 처치 시 데디케이티드 서버에서 호출합니다. 보상을 분배합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 완료 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "완료 정보 및 보상", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CompleteRaidRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.CompleteRaidResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/entry": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "MMO 서버에서 파티의 보스 레이드 입장을 요청합니다. 모든 플레이어의 entry token을 반환합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "보스 레이드 입장 요청 (내부 API)", + "parameters": [ + { + "description": "입장 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RequestEntryRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.InternalRequestEntryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/fail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "타임아웃 또는 전멸 시 데디케이티드 서버에서 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 실패 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SessionNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RoomStatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/heartbeat": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버 컨테이너가 주기적으로 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "하트비트 (내부 API)", + "parameters": [ + { + "description": "인스턴스 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.HeartbeatRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/register": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버 컨테이너가 시작 시 호출합니다. 룸 슬롯을 자동 할당합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "데디케이티드 서버 등록 (내부 API)", + "parameters": [ + { + "description": "서버 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterServerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.RegisterServerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/reset-room": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "레이드 종료 후 슬롯을 idle 상태로 되돌립니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "룸 슬롯 리셋 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ResetRoomRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.ResetRoomResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/room": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "sessionName으로 보스 레이드 방 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "방 정보 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "세션 이름", + "name": "sessionName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/bossraid.BossRoom" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/server-status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버의 정보와 룸 슬롯 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "서버 상태 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "서버 이름", + "name": "serverName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/start": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fusion 세션이 시작될 때 데디케이티드 서버에서 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 시작 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SessionNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RoomStatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/validate-entry": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버에서 플레이어의 입장 토큰을 검증합니다. 일회성 소모.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "입장 토큰 검증 (내부 API)", + "parameters": [ + { + "description": "토큰", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ValidateEntryTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.ValidateEntryTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/assets": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 에셋 목록을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "에셋 목록 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/balance": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 잔액을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "잔액 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/inventory": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 인벤토리를 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "인벤토리 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/mint": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 에셋을 발행합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "에셋 발행 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "발행 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.InternalMintAssetRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/reward": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 유저에게 보상을 지급합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "보상 지급 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "보상 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.InternalGrantRewardRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/player/profile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 플레이어 프로필을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Player" + ], + "summary": "프로필 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.PlayerProfileResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/player/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 게임 데이터를 저장합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Player" + ], + "summary": "게임 데이터 저장 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + }, + { + "description": "게임 데이터", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.GameDataRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/player/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 플레이어 프로필을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Player" + ], + "summary": "내 프로필 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.PlayerProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 닉네임을 수정합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Player" + ], + "summary": "프로필 수정", + "parameters": [ + { + "description": "수정할 프로필", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateProfileRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/player.PlayerProfile" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "모든 유저 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "전체 유저 목록 (관리자)", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.UserResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저를 삭제합니다", + "tags": [ + "Users" + ], + "summary": "유저 삭제 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "유저 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저의 역할을 admin 또는 user로 변경합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "유저 권한 변경 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "유저 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "변경할 역할", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "bossraid.BossRoom": { + "type": "object", + "properties": { + "bossId": { + "type": "integer" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxPlayers": { + "type": "integer" + }, + "players": { + "description": "Players is stored as a JSON text column for simplicity.\nTODO: For better query performance, consider migrating to a junction table\n(boss_room_players with room_id + username columns).", + "type": "string" + }, + "sessionName": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/bossraid.RoomStatus" + }, + "updatedAt": { + "type": "string" + } + } + }, + "bossraid.RoomStatus": { + "type": "string", + "enum": [ + "waiting", + "in_progress", + "completed", + "failed" + ], + "x-enum-varnames": [ + "StatusWaiting", + "StatusInProgress", + "StatusCompleted", + "StatusFailed" + ] + }, + "docs.AnnouncementResponse": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "3월 16일 서버 점검이 예정되어 있습니다." + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "서버 점검 안내" + }, + "updatedAt": { + "type": "string" + } + } + }, + "docs.BuyFromMarketRequest": { + "type": "object", + "properties": { + "listingId": { + "type": "string", + "example": "listing_001" + } + } + }, + "docs.CancelListingRequest": { + "type": "object", + "properties": { + "listingId": { + "type": "string", + "example": "listing_001" + } + } + }, + "docs.CompleteRaidRequest": { + "type": "object", + "properties": { + "rewards": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.PlayerReward" + } + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.CompleteRaidResponse": { + "type": "object", + "properties": { + "rewardResults": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.RewardResult" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "completed" + } + } + }, + "docs.CreateAnnouncementRequest": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "3월 16일 서버 점검이 예정되어 있습니다." + }, + "title": { + "type": "string", + "example": "서버 점검 안내" + } + } + }, + "docs.DownloadInfoResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "fileHash": { + "type": "string", + "example": "a1b2c3d4e5f6..." + }, + "fileName": { + "type": "string", + "example": "A301_v1.0.zip" + }, + "fileSize": { + "type": "string", + "example": "1.5 GB" + }, + "id": { + "type": "integer", + "example": 1 + }, + "launcherSize": { + "type": "string", + "example": "25.3 MB" + }, + "launcherUrl": { + "type": "string", + "example": "https://a301.api.tolelom.xyz/api/download/launcher" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string", + "example": "https://a301.api.tolelom.xyz/api/download/file" + }, + "version": { + "type": "string", + "example": "1.0.0" + } + } + }, + "docs.EquipItemRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "slot": { + "type": "string", + "example": "weapon" + } + } + }, + "docs.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "오류 메시지" + } + } + }, + "docs.GameDataRequest": { + "type": "object", + "properties": { + "attackPower": { + "type": "number", + "example": 25 + }, + "attackRange": { + "type": "number", + "example": 3 + }, + "experience": { + "type": "integer", + "example": 1200 + }, + "lastPosX": { + "type": "number", + "example": 10.5 + }, + "lastPosY": { + "type": "number", + "example": 0 + }, + "lastPosZ": { + "type": "number", + "example": 20.3 + }, + "lastRotY": { + "type": "number", + "example": 90 + }, + "level": { + "type": "integer", + "example": 5 + }, + "maxHp": { + "type": "number", + "example": 150 + }, + "maxMp": { + "type": "number", + "example": 75 + }, + "sprintMultiplier": { + "type": "number", + "example": 1.8 + }, + "totalPlayTime": { + "type": "integer", + "example": 3600 + } + } + }, + "docs.GrantRewardRequest": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "recipientPubKey": { + "type": "string", + "example": "abcdef012345..." + }, + "tokenAmount": { + "type": "integer", + "example": 1000 + } + } + }, + "docs.HeartbeatRequest": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + } + } + }, + "docs.InternalGrantRewardRequest": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "tokenAmount": { + "type": "integer", + "example": 1000 + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.InternalMintAssetRequest": { + "type": "object", + "properties": { + "properties": { + "type": "object", + "additionalProperties": {} + }, + "templateId": { + "type": "string", + "example": "sword_template" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.InternalRequestEntryResponse": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "players": { + "type": "array", + "items": { + "type": "string" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "waiting" + }, + "tokens": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "docs.LaunchTicketResponse": { + "type": "object", + "properties": { + "ticket": { + "type": "string", + "example": "ticket_abc123" + } + } + }, + "docs.ListOnMarketRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "price": { + "type": "integer", + "example": 500 + } + } + }, + "docs.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "mypassword" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.LoginResponse": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "user" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.MessageResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "성공" + } + } + }, + "docs.MintAssetPayload": { + "type": "object", + "properties": { + "owner": { + "type": "string", + "example": "abcdef012345..." + }, + "properties": { + "type": "object", + "additionalProperties": {} + }, + "template_id": { + "type": "string", + "example": "sword_template" + } + } + }, + "docs.MintAssetRequest": { + "type": "object", + "properties": { + "ownerPubKey": { + "type": "string", + "example": "abcdef012345..." + }, + "properties": { + "type": "object", + "additionalProperties": {} + }, + "templateId": { + "type": "string", + "example": "sword_template" + } + } + }, + "docs.MyEntryTokenResponse": { + "type": "object", + "properties": { + "entryToken": { + "type": "string", + "example": "token_abc" + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.PlayerProfileResponse": { + "type": "object", + "properties": { + "attackPower": { + "type": "number", + "example": 25 + }, + "attackRange": { + "type": "number", + "example": 3 + }, + "createdAt": { + "type": "string" + }, + "experience": { + "type": "integer", + "example": 1200 + }, + "id": { + "type": "integer", + "example": 1 + }, + "lastPosX": { + "type": "number", + "example": 10.5 + }, + "lastPosY": { + "type": "number", + "example": 0 + }, + "lastPosZ": { + "type": "number", + "example": 20.3 + }, + "lastRotY": { + "type": "number", + "example": 90 + }, + "level": { + "type": "integer", + "example": 5 + }, + "maxHp": { + "type": "number", + "example": 150 + }, + "maxMp": { + "type": "number", + "example": 75 + }, + "nextExp": { + "type": "integer", + "example": 2000 + }, + "nickname": { + "type": "string", + "example": "용사" + }, + "sprintMultiplier": { + "type": "number", + "example": 1.8 + }, + "totalPlayTime": { + "type": "integer", + "example": 3600 + }, + "updatedAt": { + "type": "string" + }, + "userId": { + "type": "integer", + "example": 1 + } + } + }, + "docs.PlayerReward": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "experience": { + "type": "integer", + "example": 500 + }, + "tokenAmount": { + "type": "integer", + "example": 100 + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RedeemTicketRequest": { + "type": "object", + "properties": { + "ticket": { + "type": "string", + "example": "ticket_abc123" + } + } + }, + "docs.RedeemTicketResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.RefreshRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "docs.RefreshResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.RegisterRequest": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "mypassword" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RegisterServerRequest": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + }, + "maxRooms": { + "type": "integer", + "example": 10 + }, + "serverName": { + "type": "string", + "example": "Dedi1" + } + } + }, + "docs.RegisterServerResponse": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.RegisterTemplateRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "sword_template" + }, + "name": { + "type": "string", + "example": "Sword" + }, + "schema": { + "type": "object", + "additionalProperties": {} + }, + "tradeable": { + "type": "boolean", + "example": true + } + } + }, + "docs.RequestEntryAuthRequest": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "usernames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "docs.RequestEntryRequest": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "usernames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "player1", + "player2" + ] + } + } + }, + "docs.RequestEntryResponse": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "entryToken": { + "type": "string", + "example": "token_abc" + }, + "players": { + "type": "array", + "items": { + "type": "string" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "waiting" + } + } + }, + "docs.ResetRoomRequest": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.ResetRoomResponse": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "ok" + } + } + }, + "docs.RewardResult": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean", + "example": true + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RoomStatusResponse": { + "type": "object", + "properties": { + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "in_progress" + } + } + }, + "docs.SSAFYCallbackRequest": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "auth_code_123" + }, + "state": { + "type": "string", + "example": "random_state_string" + } + } + }, + "docs.SSAFYLoginURLResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "example": "https://edu.ssafy.com/oauth/authorize?..." + } + } + }, + "docs.SessionNameRequest": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.StatusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + } + } + }, + "docs.TransferAssetRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "to": { + "type": "string", + "example": "1a2b3c4d5e6f..." + } + } + }, + "docs.TransferRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "example": 100 + }, + "to": { + "type": "string", + "example": "1a2b3c4d5e6f..." + } + } + }, + "docs.UnequipItemRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + } + } + }, + "docs.UpdateAnnouncementRequest": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "수정된 내용" + }, + "title": { + "type": "string", + "example": "수정된 제목" + } + } + }, + "docs.UpdateProfileRequest": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "example": "용사" + } + } + }, + "docs.UpdateRoleRequest": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "admin" + } + } + }, + "docs.UserResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 1 + }, + "role": { + "type": "string", + "example": "user" + }, + "ssafyId": { + "type": "string", + "example": "ssafy_123" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.ValidateEntryTokenRequest": { + "type": "object", + "properties": { + "entryToken": { + "type": "string", + "example": "token_abc" + } + } + }, + "docs.ValidateEntryTokenResponse": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "username": { + "type": "string", + "example": "player1" + }, + "valid": { + "type": "boolean", + "example": true + } + } + }, + "docs.VerifyTokenRequest": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.VerifyTokenResponse": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.WalletInfoResponse": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "1a2b3c4d5e6f..." + }, + "pubKeyHex": { + "type": "string", + "example": "abcdef012345..." + } + } + }, + "player.PlayerProfile": { + "type": "object", + "properties": { + "attackPower": { + "type": "number" + }, + "attackRange": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "experience": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "lastPosX": { + "description": "마지막 위치", + "type": "number" + }, + "lastPosY": { + "type": "number" + }, + "lastPosZ": { + "type": "number" + }, + "lastRotY": { + "type": "number" + }, + "level": { + "description": "레벨 \u0026 경험치", + "type": "integer" + }, + "maxHp": { + "description": "전투 스탯", + "type": "number" + }, + "maxMp": { + "type": "number" + }, + "nickname": { + "type": "string" + }, + "sprintMultiplier": { + "type": "number" + }, + "totalPlayTime": { + "description": "플레이 시간 (초 단위)", + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "userId": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "내부 API 키 (게임 서버 ↔ API 서버 통신용)", + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, + "BearerAuth": { + "description": "JWT Bearer 토큰 (예: Bearer eyJhbGci...)", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "a301.api.tolelom.xyz", + BasePath: "/", + Schemes: []string{}, + Title: "One of the Plans API", + Description: "멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..8f24ca3 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,4097 @@ +{ + "swagger": "2.0", + "info": { + "description": "멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API", + "title": "One of the Plans API", + "contact": {}, + "version": "1.0" + }, + "host": "a301.api.tolelom.xyz", + "basePath": "/", + "paths": { + "/api/announcements/": { + "get": { + "description": "공지사항 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 공지사항을 생성합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 생성 (관리자)", + "parameters": [ + { + "description": "공지사항 내용", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CreateAnnouncementRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/announcements/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "공지사항을 수정합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Announcements" + ], + "summary": "공지사항 수정 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "공지사항 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "수정할 내용", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateAnnouncementRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.AnnouncementResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "공지사항을 삭제합니다", + "tags": [ + "Announcements" + ], + "summary": "공지사항 삭제 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "공지사항 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/launch-ticket": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 런처용 일회성 티켓을 발급합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "런처 티켓 발급", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LaunchTicketResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/login": { + "post": { + "description": "사용자 인증 후 JWT 토큰을 발급합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "로그인", + "parameters": [ + { + "description": "로그인 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 세션을 무효화합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "로그아웃", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/redeem-ticket": { + "post": { + "description": "일회성 티켓을 Access 토큰으로 교환합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "런처 티켓 교환", + "parameters": [ + { + "description": "티켓", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RedeemTicketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RedeemTicketResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/refresh": { + "post": { + "description": "Refresh 토큰으로 새 Access 토큰을 발급합니다 (쿠키 또는 body)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "토큰 갱신", + "parameters": [ + { + "description": "Refresh 토큰 (쿠키 우선)", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/docs.RefreshRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RefreshResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/register": { + "post": { + "description": "새로운 사용자 계정을 생성합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "회원가입", + "parameters": [ + { + "description": "회원가입 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/ssafy/callback": { + "post": { + "description": "SSAFY 인가 코드를 교환하여 로그인합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "SSAFY OAuth 콜백", + "parameters": [ + { + "description": "인가 코드", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SSAFYCallbackRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/auth/ssafy/login": { + "get": { + "description": "SSAFY OAuth 로그인 URL을 생성합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "SSAFY 로그인 URL", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.SSAFYLoginURLResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/bossraid/entry": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 클라이언트에서 보스 레이드 입장을 요청합니다. 인증된 유저가 입장 목록에 포함되어야 합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Boss Raid" + ], + "summary": "보스 레이드 입장 요청", + "parameters": [ + { + "description": "입장 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RequestEntryAuthRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.RequestEntryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/bossraid/my-entry-token": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 대기 중인 입장 토큰을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Boss Raid" + ], + "summary": "내 입장 토큰 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MyEntryTokenResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/mint": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 에셋을 발행합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "에셋 발행 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "발행 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.MintAssetRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/reward": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저에게 토큰 및 에셋 보상을 지급합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "보상 지급 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "보상 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.GrantRewardRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/admin/template": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "새 에셋 템플릿을 등록합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Admin" + ], + "summary": "템플릿 등록 (관리자)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "템플릿 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterTemplateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/asset/transfer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "다른 유저에게 에셋을 전송합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "에셋 전송", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "전송 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.TransferAssetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/asset/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "특정 에셋의 상세 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "에셋 상세 조회", + "parameters": [ + { + "type": "string", + "description": "에셋 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/assets": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 블록체인 에셋 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "에셋 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/balance": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 토큰 잔액을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "잔액 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 인벤토리를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "인벤토리 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory/equip": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋을 장비 슬롯에 장착합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "아이템 장착", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "장착 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.EquipItemRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/inventory/unequip": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋의 장비 슬롯 장착을 해제합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "아이템 장착 해제", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "해제 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UnequipItemRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에 등록된 매물 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "마켓 목록 조회", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/buy": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에서 매물을 구매합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 구매", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "구매 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.BuyFromMarketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/cancel": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "마켓에 등록한 매물을 취소합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 등록 취소", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "취소 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CancelListingRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/list": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "에셋을 마켓에 등록합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "마켓 등록", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "등록 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ListOnMarketRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/market/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "특정 마켓 매물의 상세 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "마켓 매물 상세 조회", + "parameters": [ + { + "type": "string", + "description": "매물 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/transfer": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "다른 유저에게 토큰을 전송합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Chain - Transactions" + ], + "summary": "토큰 전송", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "전송 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.TransferRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/chain/wallet": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 블록체인 지갑 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Chain" + ], + "summary": "지갑 정보 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.WalletInfoResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/file": { + "get": { + "description": "게임 zip 파일을 다운로드합니다", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Download" + ], + "summary": "게임 파일 다운로드", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/info": { + "get": { + "description": "게임 및 런처 다운로드 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "다운로드 정보 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/launcher": { + "get": { + "description": "런처 실행 파일을 다운로드합니다", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "Download" + ], + "summary": "런처 다운로드", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/upload/game": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "게임 zip 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.", + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "게임 파일 업로드 (관리자)", + "parameters": [ + { + "type": "string", + "default": "game.zip", + "description": "파일명", + "name": "filename", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/download/upload/launcher": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "런처 실행 파일을 스트리밍 업로드합니다. Body는 raw binary입니다.", + "consumes": [ + "application/octet-stream" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Download" + ], + "summary": "런처 업로드 (관리자)", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.DownloadInfoResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/auth/verify": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "JWT 토큰을 검증하고 username을 반환합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Auth" + ], + "summary": "토큰 검증 (내부 API)", + "parameters": [ + { + "description": "검증할 토큰", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.VerifyTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.VerifyTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/complete": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "보스 처치 시 데디케이티드 서버에서 호출합니다. 보상을 분배합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 완료 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "완료 정보 및 보상", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.CompleteRaidRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.CompleteRaidResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/entry": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "MMO 서버에서 파티의 보스 레이드 입장을 요청합니다. 모든 플레이어의 entry token을 반환합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "보스 레이드 입장 요청 (내부 API)", + "parameters": [ + { + "description": "입장 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RequestEntryRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.InternalRequestEntryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/fail": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "타임아웃 또는 전멸 시 데디케이티드 서버에서 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 실패 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SessionNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RoomStatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/heartbeat": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버 컨테이너가 주기적으로 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "하트비트 (내부 API)", + "parameters": [ + { + "description": "인스턴스 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.HeartbeatRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.StatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/register": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버 컨테이너가 시작 시 호출합니다. 룸 슬롯을 자동 할당합니다.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "데디케이티드 서버 등록 (내부 API)", + "parameters": [ + { + "description": "서버 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.RegisterServerRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/docs.RegisterServerResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/reset-room": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "레이드 종료 후 슬롯을 idle 상태로 되돌립니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "룸 슬롯 리셋 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ResetRoomRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.ResetRoomResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/room": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "sessionName으로 보스 레이드 방 정보를 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "방 정보 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "세션 이름", + "name": "sessionName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/bossraid.BossRoom" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/server-status": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버의 정보와 룸 슬롯 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "서버 상태 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "서버 이름", + "name": "serverName", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/start": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Fusion 세션이 시작될 때 데디케이티드 서버에서 호출합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "레이드 시작 (내부 API)", + "parameters": [ + { + "description": "세션 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.SessionNameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.RoomStatusResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/bossraid/validate-entry": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "데디케이티드 서버에서 플레이어의 입장 토큰을 검증합니다. 일회성 소모.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Boss Raid" + ], + "summary": "입장 토큰 검증 (내부 API)", + "parameters": [ + { + "description": "토큰", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.ValidateEntryTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.ValidateEntryTokenResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/assets": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 에셋 목록을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "에셋 목록 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/balance": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 잔액을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "잔액 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/inventory": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 인벤토리를 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "인벤토리 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/mint": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 에셋을 발행합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "에셋 발행 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "발행 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.InternalMintAssetRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/chain/reward": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 유저에게 보상을 지급합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Chain" + ], + "summary": "보상 지급 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "멱등성 키", + "name": "Idempotency-Key", + "in": "header", + "required": true + }, + { + "description": "보상 정보", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.InternalGrantRewardRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/player/profile": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 플레이어 프로필을 조회합니다 (게임 서버용)", + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Player" + ], + "summary": "프로필 조회 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.PlayerProfileResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/internal/player/save": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "username으로 게임 데이터를 저장합니다 (게임 서버용)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Internal - Player" + ], + "summary": "게임 데이터 저장 (내부 API)", + "parameters": [ + { + "type": "string", + "description": "유저명", + "name": "username", + "in": "query", + "required": true + }, + { + "description": "게임 데이터", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.GameDataRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/player/profile": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 플레이어 프로필을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Player" + ], + "summary": "내 프로필 조회", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.PlayerProfileResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "현재 유저의 닉네임을 수정합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Player" + ], + "summary": "프로필 수정", + "parameters": [ + { + "description": "수정할 프로필", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateProfileRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/player.PlayerProfile" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "모든 유저 목록을 조회합니다", + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "전체 유저 목록 (관리자)", + "parameters": [ + { + "type": "integer", + "default": 0, + "description": "시작 위치", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "조회 수", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.UserResponse" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저를 삭제합니다", + "tags": [ + "Users" + ], + "summary": "유저 삭제 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "유저 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + }, + "/api/users/{id}/role": { + "patch": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "유저의 역할을 admin 또는 user로 변경합니다", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "유저 권한 변경 (관리자)", + "parameters": [ + { + "type": "integer", + "description": "유저 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "변경할 역할", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/docs.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/docs.MessageResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/docs.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "bossraid.BossRoom": { + "type": "object", + "properties": { + "bossId": { + "type": "integer" + }, + "completedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "maxPlayers": { + "type": "integer" + }, + "players": { + "description": "Players is stored as a JSON text column for simplicity.\nTODO: For better query performance, consider migrating to a junction table\n(boss_room_players with room_id + username columns).", + "type": "string" + }, + "sessionName": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/bossraid.RoomStatus" + }, + "updatedAt": { + "type": "string" + } + } + }, + "bossraid.RoomStatus": { + "type": "string", + "enum": [ + "waiting", + "in_progress", + "completed", + "failed" + ], + "x-enum-varnames": [ + "StatusWaiting", + "StatusInProgress", + "StatusCompleted", + "StatusFailed" + ] + }, + "docs.AnnouncementResponse": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "3월 16일 서버 점검이 예정되어 있습니다." + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 1 + }, + "title": { + "type": "string", + "example": "서버 점검 안내" + }, + "updatedAt": { + "type": "string" + } + } + }, + "docs.BuyFromMarketRequest": { + "type": "object", + "properties": { + "listingId": { + "type": "string", + "example": "listing_001" + } + } + }, + "docs.CancelListingRequest": { + "type": "object", + "properties": { + "listingId": { + "type": "string", + "example": "listing_001" + } + } + }, + "docs.CompleteRaidRequest": { + "type": "object", + "properties": { + "rewards": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.PlayerReward" + } + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.CompleteRaidResponse": { + "type": "object", + "properties": { + "rewardResults": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.RewardResult" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "completed" + } + } + }, + "docs.CreateAnnouncementRequest": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "3월 16일 서버 점검이 예정되어 있습니다." + }, + "title": { + "type": "string", + "example": "서버 점검 안내" + } + } + }, + "docs.DownloadInfoResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "fileHash": { + "type": "string", + "example": "a1b2c3d4e5f6..." + }, + "fileName": { + "type": "string", + "example": "A301_v1.0.zip" + }, + "fileSize": { + "type": "string", + "example": "1.5 GB" + }, + "id": { + "type": "integer", + "example": 1 + }, + "launcherSize": { + "type": "string", + "example": "25.3 MB" + }, + "launcherUrl": { + "type": "string", + "example": "https://a301.api.tolelom.xyz/api/download/launcher" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string", + "example": "https://a301.api.tolelom.xyz/api/download/file" + }, + "version": { + "type": "string", + "example": "1.0.0" + } + } + }, + "docs.EquipItemRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "slot": { + "type": "string", + "example": "weapon" + } + } + }, + "docs.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "오류 메시지" + } + } + }, + "docs.GameDataRequest": { + "type": "object", + "properties": { + "attackPower": { + "type": "number", + "example": 25 + }, + "attackRange": { + "type": "number", + "example": 3 + }, + "experience": { + "type": "integer", + "example": 1200 + }, + "lastPosX": { + "type": "number", + "example": 10.5 + }, + "lastPosY": { + "type": "number", + "example": 0 + }, + "lastPosZ": { + "type": "number", + "example": 20.3 + }, + "lastRotY": { + "type": "number", + "example": 90 + }, + "level": { + "type": "integer", + "example": 5 + }, + "maxHp": { + "type": "number", + "example": 150 + }, + "maxMp": { + "type": "number", + "example": 75 + }, + "sprintMultiplier": { + "type": "number", + "example": 1.8 + }, + "totalPlayTime": { + "type": "integer", + "example": 3600 + } + } + }, + "docs.GrantRewardRequest": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "recipientPubKey": { + "type": "string", + "example": "abcdef012345..." + }, + "tokenAmount": { + "type": "integer", + "example": 1000 + } + } + }, + "docs.HeartbeatRequest": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + } + } + }, + "docs.InternalGrantRewardRequest": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "tokenAmount": { + "type": "integer", + "example": 1000 + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.InternalMintAssetRequest": { + "type": "object", + "properties": { + "properties": { + "type": "object", + "additionalProperties": {} + }, + "templateId": { + "type": "string", + "example": "sword_template" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.InternalRequestEntryResponse": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "players": { + "type": "array", + "items": { + "type": "string" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "waiting" + }, + "tokens": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "docs.LaunchTicketResponse": { + "type": "object", + "properties": { + "ticket": { + "type": "string", + "example": "ticket_abc123" + } + } + }, + "docs.ListOnMarketRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "price": { + "type": "integer", + "example": 500 + } + } + }, + "docs.LoginRequest": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "mypassword" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.LoginResponse": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "user" + }, + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.MessageResponse": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "성공" + } + } + }, + "docs.MintAssetPayload": { + "type": "object", + "properties": { + "owner": { + "type": "string", + "example": "abcdef012345..." + }, + "properties": { + "type": "object", + "additionalProperties": {} + }, + "template_id": { + "type": "string", + "example": "sword_template" + } + } + }, + "docs.MintAssetRequest": { + "type": "object", + "properties": { + "ownerPubKey": { + "type": "string", + "example": "abcdef012345..." + }, + "properties": { + "type": "object", + "additionalProperties": {} + }, + "templateId": { + "type": "string", + "example": "sword_template" + } + } + }, + "docs.MyEntryTokenResponse": { + "type": "object", + "properties": { + "entryToken": { + "type": "string", + "example": "token_abc" + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.PlayerProfileResponse": { + "type": "object", + "properties": { + "attackPower": { + "type": "number", + "example": 25 + }, + "attackRange": { + "type": "number", + "example": 3 + }, + "createdAt": { + "type": "string" + }, + "experience": { + "type": "integer", + "example": 1200 + }, + "id": { + "type": "integer", + "example": 1 + }, + "lastPosX": { + "type": "number", + "example": 10.5 + }, + "lastPosY": { + "type": "number", + "example": 0 + }, + "lastPosZ": { + "type": "number", + "example": 20.3 + }, + "lastRotY": { + "type": "number", + "example": 90 + }, + "level": { + "type": "integer", + "example": 5 + }, + "maxHp": { + "type": "number", + "example": 150 + }, + "maxMp": { + "type": "number", + "example": 75 + }, + "nextExp": { + "type": "integer", + "example": 2000 + }, + "nickname": { + "type": "string", + "example": "용사" + }, + "sprintMultiplier": { + "type": "number", + "example": 1.8 + }, + "totalPlayTime": { + "type": "integer", + "example": 3600 + }, + "updatedAt": { + "type": "string" + }, + "userId": { + "type": "integer", + "example": 1 + } + } + }, + "docs.PlayerReward": { + "type": "object", + "properties": { + "assets": { + "type": "array", + "items": { + "$ref": "#/definitions/docs.MintAssetPayload" + } + }, + "experience": { + "type": "integer", + "example": 500 + }, + "tokenAmount": { + "type": "integer", + "example": 100 + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RedeemTicketRequest": { + "type": "object", + "properties": { + "ticket": { + "type": "string", + "example": "ticket_abc123" + } + } + }, + "docs.RedeemTicketResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.RefreshRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "docs.RefreshResponse": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.RegisterRequest": { + "type": "object", + "properties": { + "password": { + "type": "string", + "example": "mypassword" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RegisterServerRequest": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + }, + "maxRooms": { + "type": "integer", + "example": 10 + }, + "serverName": { + "type": "string", + "example": "Dedi1" + } + } + }, + "docs.RegisterServerResponse": { + "type": "object", + "properties": { + "instanceId": { + "type": "string", + "example": "container_abc" + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.RegisterTemplateRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "sword_template" + }, + "name": { + "type": "string", + "example": "Sword" + }, + "schema": { + "type": "object", + "additionalProperties": {} + }, + "tradeable": { + "type": "boolean", + "example": true + } + } + }, + "docs.RequestEntryAuthRequest": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "usernames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "docs.RequestEntryRequest": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "usernames": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "player1", + "player2" + ] + } + } + }, + "docs.RequestEntryResponse": { + "type": "object", + "properties": { + "bossId": { + "type": "integer", + "example": 1 + }, + "entryToken": { + "type": "string", + "example": "token_abc" + }, + "players": { + "type": "array", + "items": { + "type": "string" + } + }, + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "waiting" + } + } + }, + "docs.ResetRoomRequest": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.ResetRoomResponse": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "ok" + } + } + }, + "docs.RewardResult": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "success": { + "type": "boolean", + "example": true + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.RoomStatusResponse": { + "type": "object", + "properties": { + "roomId": { + "type": "integer", + "example": 1 + }, + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "status": { + "type": "string", + "example": "in_progress" + } + } + }, + "docs.SSAFYCallbackRequest": { + "type": "object", + "properties": { + "code": { + "type": "string", + "example": "auth_code_123" + }, + "state": { + "type": "string", + "example": "random_state_string" + } + } + }, + "docs.SSAFYLoginURLResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "example": "https://edu.ssafy.com/oauth/authorize?..." + } + } + }, + "docs.SessionNameRequest": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + } + } + }, + "docs.StatusResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + } + } + }, + "docs.TransferAssetRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + }, + "to": { + "type": "string", + "example": "1a2b3c4d5e6f..." + } + } + }, + "docs.TransferRequest": { + "type": "object", + "properties": { + "amount": { + "type": "integer", + "example": 100 + }, + "to": { + "type": "string", + "example": "1a2b3c4d5e6f..." + } + } + }, + "docs.UnequipItemRequest": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "example": "asset_001" + } + } + }, + "docs.UpdateAnnouncementRequest": { + "type": "object", + "properties": { + "content": { + "type": "string", + "example": "수정된 내용" + }, + "title": { + "type": "string", + "example": "수정된 제목" + } + } + }, + "docs.UpdateProfileRequest": { + "type": "object", + "properties": { + "nickname": { + "type": "string", + "example": "용사" + } + } + }, + "docs.UpdateRoleRequest": { + "type": "object", + "properties": { + "role": { + "type": "string", + "example": "admin" + } + } + }, + "docs.UserResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "id": { + "type": "integer", + "example": 1 + }, + "role": { + "type": "string", + "example": "user" + }, + "ssafyId": { + "type": "string", + "example": "ssafy_123" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.ValidateEntryTokenRequest": { + "type": "object", + "properties": { + "entryToken": { + "type": "string", + "example": "token_abc" + } + } + }, + "docs.ValidateEntryTokenResponse": { + "type": "object", + "properties": { + "sessionName": { + "type": "string", + "example": "Dedi1_Room0" + }, + "username": { + "type": "string", + "example": "player1" + }, + "valid": { + "type": "boolean", + "example": true + } + } + }, + "docs.VerifyTokenRequest": { + "type": "object", + "properties": { + "token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIs..." + } + } + }, + "docs.VerifyTokenResponse": { + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "player1" + } + } + }, + "docs.WalletInfoResponse": { + "type": "object", + "properties": { + "address": { + "type": "string", + "example": "1a2b3c4d5e6f..." + }, + "pubKeyHex": { + "type": "string", + "example": "abcdef012345..." + } + } + }, + "player.PlayerProfile": { + "type": "object", + "properties": { + "attackPower": { + "type": "number" + }, + "attackRange": { + "type": "number" + }, + "createdAt": { + "type": "string" + }, + "experience": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "lastPosX": { + "description": "마지막 위치", + "type": "number" + }, + "lastPosY": { + "type": "number" + }, + "lastPosZ": { + "type": "number" + }, + "lastRotY": { + "type": "number" + }, + "level": { + "description": "레벨 \u0026 경험치", + "type": "integer" + }, + "maxHp": { + "description": "전투 스탯", + "type": "number" + }, + "maxMp": { + "type": "number" + }, + "nickname": { + "type": "string" + }, + "sprintMultiplier": { + "type": "number" + }, + "totalPlayTime": { + "description": "플레이 시간 (초 단위)", + "type": "integer" + }, + "updatedAt": { + "type": "string" + }, + "userId": { + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "내부 API 키 (게임 서버 ↔ API 서버 통신용)", + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, + "BearerAuth": { + "description": "JWT Bearer 토큰 (예: Bearer eyJhbGci...)", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..f340976 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,2686 @@ +basePath: / +definitions: + bossraid.BossRoom: + properties: + bossId: + type: integer + completedAt: + type: string + createdAt: + type: string + id: + type: integer + maxPlayers: + type: integer + players: + description: |- + Players is stored as a JSON text column for simplicity. + TODO: For better query performance, consider migrating to a junction table + (boss_room_players with room_id + username columns). + type: string + sessionName: + type: string + startedAt: + type: string + status: + $ref: '#/definitions/bossraid.RoomStatus' + updatedAt: + type: string + type: object + bossraid.RoomStatus: + enum: + - waiting + - in_progress + - completed + - failed + type: string + x-enum-varnames: + - StatusWaiting + - StatusInProgress + - StatusCompleted + - StatusFailed + docs.AnnouncementResponse: + properties: + content: + example: 3월 16일 서버 점검이 예정되어 있습니다. + type: string + createdAt: + type: string + id: + example: 1 + type: integer + title: + example: 서버 점검 안내 + type: string + updatedAt: + type: string + type: object + docs.BuyFromMarketRequest: + properties: + listingId: + example: listing_001 + type: string + type: object + docs.CancelListingRequest: + properties: + listingId: + example: listing_001 + type: string + type: object + docs.CompleteRaidRequest: + properties: + rewards: + items: + $ref: '#/definitions/docs.PlayerReward' + type: array + sessionName: + example: Dedi1_Room0 + type: string + type: object + docs.CompleteRaidResponse: + properties: + rewardResults: + items: + $ref: '#/definitions/docs.RewardResult' + type: array + roomId: + example: 1 + type: integer + sessionName: + example: Dedi1_Room0 + type: string + status: + example: completed + type: string + type: object + docs.CreateAnnouncementRequest: + properties: + content: + example: 3월 16일 서버 점검이 예정되어 있습니다. + type: string + title: + example: 서버 점검 안내 + type: string + type: object + docs.DownloadInfoResponse: + properties: + createdAt: + type: string + fileHash: + example: a1b2c3d4e5f6... + type: string + fileName: + example: A301_v1.0.zip + type: string + fileSize: + example: 1.5 GB + type: string + id: + example: 1 + type: integer + launcherSize: + example: 25.3 MB + type: string + launcherUrl: + example: https://a301.api.tolelom.xyz/api/download/launcher + type: string + updatedAt: + type: string + url: + example: https://a301.api.tolelom.xyz/api/download/file + type: string + version: + example: 1.0.0 + type: string + type: object + docs.EquipItemRequest: + properties: + assetId: + example: asset_001 + type: string + slot: + example: weapon + type: string + type: object + docs.ErrorResponse: + properties: + error: + example: 오류 메시지 + type: string + type: object + docs.GameDataRequest: + properties: + attackPower: + example: 25 + type: number + attackRange: + example: 3 + type: number + experience: + example: 1200 + type: integer + lastPosX: + example: 10.5 + type: number + lastPosY: + example: 0 + type: number + lastPosZ: + example: 20.3 + type: number + lastRotY: + example: 90 + type: number + level: + example: 5 + type: integer + maxHp: + example: 150 + type: number + maxMp: + example: 75 + type: number + sprintMultiplier: + example: 1.8 + type: number + totalPlayTime: + example: 3600 + type: integer + type: object + docs.GrantRewardRequest: + properties: + assets: + items: + $ref: '#/definitions/docs.MintAssetPayload' + type: array + recipientPubKey: + example: abcdef012345... + type: string + tokenAmount: + example: 1000 + type: integer + type: object + docs.HeartbeatRequest: + properties: + instanceId: + example: container_abc + type: string + type: object + docs.InternalGrantRewardRequest: + properties: + assets: + items: + $ref: '#/definitions/docs.MintAssetPayload' + type: array + tokenAmount: + example: 1000 + type: integer + username: + example: player1 + type: string + type: object + docs.InternalMintAssetRequest: + properties: + properties: + additionalProperties: {} + type: object + templateId: + example: sword_template + type: string + username: + example: player1 + type: string + type: object + docs.InternalRequestEntryResponse: + properties: + bossId: + example: 1 + type: integer + players: + items: + type: string + type: array + roomId: + example: 1 + type: integer + sessionName: + example: Dedi1_Room0 + type: string + status: + example: waiting + type: string + tokens: + additionalProperties: + type: string + type: object + type: object + docs.LaunchTicketResponse: + properties: + ticket: + example: ticket_abc123 + type: string + type: object + docs.ListOnMarketRequest: + properties: + assetId: + example: asset_001 + type: string + price: + example: 500 + type: integer + type: object + docs.LoginRequest: + properties: + password: + example: mypassword + type: string + username: + example: player1 + type: string + type: object + docs.LoginResponse: + properties: + role: + example: user + type: string + token: + example: eyJhbGciOiJIUzI1NiIs... + type: string + username: + example: player1 + type: string + type: object + docs.MessageResponse: + properties: + message: + example: 성공 + type: string + type: object + docs.MintAssetPayload: + properties: + owner: + example: abcdef012345... + type: string + properties: + additionalProperties: {} + type: object + template_id: + example: sword_template + type: string + type: object + docs.MintAssetRequest: + properties: + ownerPubKey: + example: abcdef012345... + type: string + properties: + additionalProperties: {} + type: object + templateId: + example: sword_template + type: string + type: object + docs.MyEntryTokenResponse: + properties: + entryToken: + example: token_abc + type: string + sessionName: + example: Dedi1_Room0 + type: string + type: object + docs.PlayerProfileResponse: + properties: + attackPower: + example: 25 + type: number + attackRange: + example: 3 + type: number + createdAt: + type: string + experience: + example: 1200 + type: integer + id: + example: 1 + type: integer + lastPosX: + example: 10.5 + type: number + lastPosY: + example: 0 + type: number + lastPosZ: + example: 20.3 + type: number + lastRotY: + example: 90 + type: number + level: + example: 5 + type: integer + maxHp: + example: 150 + type: number + maxMp: + example: 75 + type: number + nextExp: + example: 2000 + type: integer + nickname: + example: 용사 + type: string + sprintMultiplier: + example: 1.8 + type: number + totalPlayTime: + example: 3600 + type: integer + updatedAt: + type: string + userId: + example: 1 + type: integer + type: object + docs.PlayerReward: + properties: + assets: + items: + $ref: '#/definitions/docs.MintAssetPayload' + type: array + experience: + example: 500 + type: integer + tokenAmount: + example: 100 + type: integer + username: + example: player1 + type: string + type: object + docs.RedeemTicketRequest: + properties: + ticket: + example: ticket_abc123 + type: string + type: object + docs.RedeemTicketResponse: + properties: + token: + example: eyJhbGciOiJIUzI1NiIs... + type: string + type: object + docs.RefreshRequest: + properties: + refreshToken: + type: string + type: object + docs.RefreshResponse: + properties: + token: + example: eyJhbGciOiJIUzI1NiIs... + type: string + type: object + docs.RegisterRequest: + properties: + password: + example: mypassword + type: string + username: + example: player1 + type: string + type: object + docs.RegisterServerRequest: + properties: + instanceId: + example: container_abc + type: string + maxRooms: + example: 10 + type: integer + serverName: + example: Dedi1 + type: string + type: object + docs.RegisterServerResponse: + properties: + instanceId: + example: container_abc + type: string + sessionName: + example: Dedi1_Room0 + type: string + type: object + docs.RegisterTemplateRequest: + properties: + id: + example: sword_template + type: string + name: + example: Sword + type: string + schema: + additionalProperties: {} + type: object + tradeable: + example: true + type: boolean + type: object + docs.RequestEntryAuthRequest: + properties: + bossId: + example: 1 + type: integer + usernames: + items: + type: string + type: array + type: object + docs.RequestEntryRequest: + properties: + bossId: + example: 1 + type: integer + usernames: + example: + - player1 + - player2 + items: + type: string + type: array + type: object + docs.RequestEntryResponse: + properties: + bossId: + example: 1 + type: integer + entryToken: + example: token_abc + type: string + players: + items: + type: string + type: array + roomId: + example: 1 + type: integer + sessionName: + example: Dedi1_Room0 + type: string + status: + example: waiting + type: string + type: object + docs.ResetRoomRequest: + properties: + sessionName: + example: Dedi1_Room0 + type: string + type: object + docs.ResetRoomResponse: + properties: + sessionName: + example: Dedi1_Room0 + type: string + status: + example: ok + type: string + type: object + docs.RewardResult: + properties: + error: + type: string + success: + example: true + type: boolean + username: + example: player1 + type: string + type: object + docs.RoomStatusResponse: + properties: + roomId: + example: 1 + type: integer + sessionName: + example: Dedi1_Room0 + type: string + status: + example: in_progress + type: string + type: object + docs.SSAFYCallbackRequest: + properties: + code: + example: auth_code_123 + type: string + state: + example: random_state_string + type: string + type: object + docs.SSAFYLoginURLResponse: + properties: + url: + example: https://edu.ssafy.com/oauth/authorize?... + type: string + type: object + docs.SessionNameRequest: + properties: + sessionName: + example: Dedi1_Room0 + type: string + type: object + docs.StatusResponse: + properties: + status: + example: ok + type: string + type: object + docs.TransferAssetRequest: + properties: + assetId: + example: asset_001 + type: string + to: + example: 1a2b3c4d5e6f... + type: string + type: object + docs.TransferRequest: + properties: + amount: + example: 100 + type: integer + to: + example: 1a2b3c4d5e6f... + type: string + type: object + docs.UnequipItemRequest: + properties: + assetId: + example: asset_001 + type: string + type: object + docs.UpdateAnnouncementRequest: + properties: + content: + example: 수정된 내용 + type: string + title: + example: 수정된 제목 + type: string + type: object + docs.UpdateProfileRequest: + properties: + nickname: + example: 용사 + type: string + type: object + docs.UpdateRoleRequest: + properties: + role: + example: admin + type: string + type: object + docs.UserResponse: + properties: + createdAt: + type: string + id: + example: 1 + type: integer + role: + example: user + type: string + ssafyId: + example: ssafy_123 + type: string + updatedAt: + type: string + username: + example: player1 + type: string + type: object + docs.ValidateEntryTokenRequest: + properties: + entryToken: + example: token_abc + type: string + type: object + docs.ValidateEntryTokenResponse: + properties: + sessionName: + example: Dedi1_Room0 + type: string + username: + example: player1 + type: string + valid: + example: true + type: boolean + type: object + docs.VerifyTokenRequest: + properties: + token: + example: eyJhbGciOiJIUzI1NiIs... + type: string + type: object + docs.VerifyTokenResponse: + properties: + username: + example: player1 + type: string + type: object + docs.WalletInfoResponse: + properties: + address: + example: 1a2b3c4d5e6f... + type: string + pubKeyHex: + example: abcdef012345... + type: string + type: object + player.PlayerProfile: + properties: + attackPower: + type: number + attackRange: + type: number + createdAt: + type: string + experience: + type: integer + id: + type: integer + lastPosX: + description: 마지막 위치 + type: number + lastPosY: + type: number + lastPosZ: + type: number + lastRotY: + type: number + level: + description: 레벨 & 경험치 + type: integer + maxHp: + description: 전투 스탯 + type: number + maxMp: + type: number + nickname: + type: string + sprintMultiplier: + type: number + totalPlayTime: + description: 플레이 시간 (초 단위) + type: integer + updatedAt: + type: string + userId: + type: integer + type: object +host: a301.api.tolelom.xyz +info: + contact: {} + description: 멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API + title: One of the Plans API + version: "1.0" +paths: + /api/announcements/: + get: + description: 공지사항 목록을 조회합니다 + parameters: + - default: 0 + description: 시작 위치 + in: query + name: offset + type: integer + - default: 20 + description: 조회 수 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/docs.AnnouncementResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 공지사항 목록 조회 + tags: + - Announcements + post: + consumes: + - application/json + description: 새 공지사항을 생성합니다 + parameters: + - description: 공지사항 내용 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.CreateAnnouncementRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/docs.AnnouncementResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 공지사항 생성 (관리자) + tags: + - Announcements + /api/announcements/{id}: + delete: + description: 공지사항을 삭제합니다 + parameters: + - description: 공지사항 ID + in: path + name: id + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 공지사항 삭제 (관리자) + tags: + - Announcements + put: + consumes: + - application/json + description: 공지사항을 수정합니다 + parameters: + - description: 공지사항 ID + in: path + name: id + required: true + type: integer + - description: 수정할 내용 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.UpdateAnnouncementRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.AnnouncementResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 공지사항 수정 (관리자) + tags: + - Announcements + /api/auth/launch-ticket: + post: + description: 게임 런처용 일회성 티켓을 발급합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.LaunchTicketResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 런처 티켓 발급 + tags: + - Auth + /api/auth/login: + post: + consumes: + - application/json + description: 사용자 인증 후 JWT 토큰을 발급합니다 + parameters: + - description: 로그인 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 로그인 + tags: + - Auth + /api/auth/logout: + post: + description: 현재 세션을 무효화합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.MessageResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 로그아웃 + tags: + - Auth + /api/auth/redeem-ticket: + post: + consumes: + - application/json + description: 일회성 티켓을 Access 토큰으로 교환합니다 + parameters: + - description: 티켓 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RedeemTicketRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.RedeemTicketResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 런처 티켓 교환 + tags: + - Auth + /api/auth/refresh: + post: + consumes: + - application/json + description: Refresh 토큰으로 새 Access 토큰을 발급합니다 (쿠키 또는 body) + parameters: + - description: Refresh 토큰 (쿠키 우선) + in: body + name: body + schema: + $ref: '#/definitions/docs.RefreshRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.RefreshResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 토큰 갱신 + tags: + - Auth + /api/auth/register: + post: + consumes: + - application/json + description: 새로운 사용자 계정을 생성합니다 + parameters: + - description: 회원가입 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RegisterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/docs.MessageResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 회원가입 + tags: + - Auth + /api/auth/ssafy/callback: + post: + consumes: + - application/json + description: SSAFY 인가 코드를 교환하여 로그인합니다 + parameters: + - description: 인가 코드 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.SSAFYCallbackRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.LoginResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: SSAFY OAuth 콜백 + tags: + - Auth + /api/auth/ssafy/login: + get: + description: SSAFY OAuth 로그인 URL을 생성합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.SSAFYLoginURLResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: SSAFY 로그인 URL + tags: + - Auth + /api/bossraid/entry: + post: + consumes: + - application/json + description: 게임 클라이언트에서 보스 레이드 입장을 요청합니다. 인증된 유저가 입장 목록에 포함되어야 합니다. + parameters: + - description: 입장 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RequestEntryAuthRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/docs.RequestEntryResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 보스 레이드 입장 요청 + tags: + - Boss Raid + /api/bossraid/my-entry-token: + get: + description: 현재 유저의 대기 중인 입장 토큰을 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.MyEntryTokenResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 내 입장 토큰 조회 + tags: + - Boss Raid + /api/chain/admin/mint: + post: + consumes: + - application/json + description: 새 에셋을 발행합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 발행 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.MintAssetRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 에셋 발행 (관리자) + tags: + - Chain - Admin + /api/chain/admin/reward: + post: + consumes: + - application/json + description: 유저에게 토큰 및 에셋 보상을 지급합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 보상 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.GrantRewardRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 보상 지급 (관리자) + tags: + - Chain - Admin + /api/chain/admin/template: + post: + consumes: + - application/json + description: 새 에셋 템플릿을 등록합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 템플릿 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RegisterTemplateRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 템플릿 등록 (관리자) + tags: + - Chain - Admin + /api/chain/asset/{id}: + get: + description: 특정 에셋의 상세 정보를 조회합니다 + parameters: + - description: 에셋 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 에셋 상세 조회 + tags: + - Chain + /api/chain/asset/transfer: + post: + consumes: + - application/json + description: 다른 유저에게 에셋을 전송합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 전송 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.TransferAssetRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 에셋 전송 + tags: + - Chain - Transactions + /api/chain/assets: + get: + description: 현재 유저의 블록체인 에셋 목록을 조회합니다 + parameters: + - default: 0 + description: 시작 위치 + in: query + name: offset + type: integer + - default: 50 + description: 조회 수 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 에셋 목록 조회 + tags: + - Chain + /api/chain/balance: + get: + description: 현재 유저의 토큰 잔액을 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 잔액 조회 + tags: + - Chain + /api/chain/inventory: + get: + description: 현재 유저의 인벤토리를 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 인벤토리 조회 + tags: + - Chain + /api/chain/inventory/equip: + post: + consumes: + - application/json + description: 에셋을 장비 슬롯에 장착합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 장착 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.EquipItemRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 아이템 장착 + tags: + - Chain - Transactions + /api/chain/inventory/unequip: + post: + consumes: + - application/json + description: 에셋의 장비 슬롯 장착을 해제합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 해제 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.UnequipItemRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 아이템 장착 해제 + tags: + - Chain - Transactions + /api/chain/market: + get: + description: 마켓에 등록된 매물 목록을 조회합니다 + parameters: + - default: 0 + description: 시작 위치 + in: query + name: offset + type: integer + - default: 50 + description: 조회 수 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 마켓 목록 조회 + tags: + - Chain + /api/chain/market/{id}: + get: + description: 특정 마켓 매물의 상세 정보를 조회합니다 + parameters: + - description: 매물 ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 마켓 매물 상세 조회 + tags: + - Chain + /api/chain/market/buy: + post: + consumes: + - application/json + description: 마켓에서 매물을 구매합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 구매 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.BuyFromMarketRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 마켓 구매 + tags: + - Chain - Transactions + /api/chain/market/cancel: + post: + consumes: + - application/json + description: 마켓에 등록한 매물을 취소합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 취소 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.CancelListingRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 마켓 등록 취소 + tags: + - Chain - Transactions + /api/chain/market/list: + post: + consumes: + - application/json + description: 에셋을 마켓에 등록합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 등록 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.ListOnMarketRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 마켓 등록 + tags: + - Chain - Transactions + /api/chain/transfer: + post: + consumes: + - application/json + description: 다른 유저에게 토큰을 전송합니다 + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 전송 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.TransferRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 토큰 전송 + tags: + - Chain - Transactions + /api/chain/wallet: + get: + description: 현재 유저의 블록체인 지갑 정보를 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.WalletInfoResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 지갑 정보 조회 + tags: + - Chain + /api/download/file: + get: + description: 게임 zip 파일을 다운로드합니다 + produces: + - application/octet-stream + responses: + "200": + description: OK + schema: + type: file + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 게임 파일 다운로드 + tags: + - Download + /api/download/info: + get: + description: 게임 및 런처 다운로드 정보를 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.DownloadInfoResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 다운로드 정보 조회 + tags: + - Download + /api/download/launcher: + get: + description: 런처 실행 파일을 다운로드합니다 + produces: + - application/octet-stream + responses: + "200": + description: OK + schema: + type: file + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + summary: 런처 다운로드 + tags: + - Download + /api/download/upload/game: + post: + consumes: + - application/octet-stream + description: 게임 zip 파일을 스트리밍 업로드합니다. Body는 raw binary입니다. + parameters: + - default: game.zip + description: 파일명 + in: query + name: filename + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.DownloadInfoResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 게임 파일 업로드 (관리자) + tags: + - Download + /api/download/upload/launcher: + post: + consumes: + - application/octet-stream + description: 런처 실행 파일을 스트리밍 업로드합니다. Body는 raw binary입니다. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.DownloadInfoResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 런처 업로드 (관리자) + tags: + - Download + /api/internal/auth/verify: + post: + consumes: + - application/json + description: JWT 토큰을 검증하고 username을 반환합니다 + parameters: + - description: 검증할 토큰 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.VerifyTokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.VerifyTokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 토큰 검증 (내부 API) + tags: + - Internal - Auth + /api/internal/bossraid/complete: + post: + consumes: + - application/json + description: 보스 처치 시 데디케이티드 서버에서 호출합니다. 보상을 분배합니다. + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 완료 정보 및 보상 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.CompleteRaidRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.CompleteRaidResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 레이드 완료 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/entry: + post: + consumes: + - application/json + description: MMO 서버에서 파티의 보스 레이드 입장을 요청합니다. 모든 플레이어의 entry token을 반환합니다. + parameters: + - description: 입장 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RequestEntryRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/docs.InternalRequestEntryResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 보스 레이드 입장 요청 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/fail: + post: + consumes: + - application/json + description: 타임아웃 또는 전멸 시 데디케이티드 서버에서 호출합니다 + parameters: + - description: 세션 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.SessionNameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.RoomStatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 레이드 실패 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/heartbeat: + post: + consumes: + - application/json + description: 데디케이티드 서버 컨테이너가 주기적으로 호출합니다 + parameters: + - description: 인스턴스 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.HeartbeatRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.StatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 하트비트 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/register: + post: + consumes: + - application/json + description: 데디케이티드 서버 컨테이너가 시작 시 호출합니다. 룸 슬롯을 자동 할당합니다. + parameters: + - description: 서버 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.RegisterServerRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/docs.RegisterServerResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 데디케이티드 서버 등록 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/reset-room: + post: + consumes: + - application/json + description: 레이드 종료 후 슬롯을 idle 상태로 되돌립니다 + parameters: + - description: 세션 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.ResetRoomRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.ResetRoomResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 룸 슬롯 리셋 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/room: + get: + description: sessionName으로 보스 레이드 방 정보를 조회합니다 + parameters: + - description: 세션 이름 + in: query + name: sessionName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/bossraid.BossRoom' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 방 정보 조회 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/server-status: + get: + description: 데디케이티드 서버의 정보와 룸 슬롯 목록을 조회합니다 + parameters: + - description: 서버 이름 + in: query + name: serverName + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 서버 상태 조회 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/start: + post: + consumes: + - application/json + description: Fusion 세션이 시작될 때 데디케이티드 서버에서 호출합니다 + parameters: + - description: 세션 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.SessionNameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.RoomStatusResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 레이드 시작 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/bossraid/validate-entry: + post: + consumes: + - application/json + description: 데디케이티드 서버에서 플레이어의 입장 토큰을 검증합니다. 일회성 소모. + parameters: + - description: 토큰 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.ValidateEntryTokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.ValidateEntryTokenResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 입장 토큰 검증 (내부 API) + tags: + - Internal - Boss Raid + /api/internal/chain/assets: + get: + description: username으로 에셋 목록을 조회합니다 (게임 서버용) + parameters: + - description: 유저명 + in: query + name: username + required: true + type: string + - default: 0 + description: 시작 위치 + in: query + name: offset + type: integer + - default: 50 + description: 조회 수 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 에셋 목록 조회 (내부 API) + tags: + - Internal - Chain + /api/internal/chain/balance: + get: + description: username으로 잔액을 조회합니다 (게임 서버용) + parameters: + - description: 유저명 + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 잔액 조회 (내부 API) + tags: + - Internal - Chain + /api/internal/chain/inventory: + get: + description: username으로 인벤토리를 조회합니다 (게임 서버용) + parameters: + - description: 유저명 + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 인벤토리 조회 (내부 API) + tags: + - Internal - Chain + /api/internal/chain/mint: + post: + consumes: + - application/json + description: username으로 에셋을 발행합니다 (게임 서버용) + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 발행 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.InternalMintAssetRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 에셋 발행 (내부 API) + tags: + - Internal - Chain + /api/internal/chain/reward: + post: + consumes: + - application/json + description: username으로 유저에게 보상을 지급합니다 (게임 서버용) + parameters: + - description: 멱등성 키 + in: header + name: Idempotency-Key + required: true + type: string + - description: 보상 정보 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.InternalGrantRewardRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + additionalProperties: true + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 보상 지급 (내부 API) + tags: + - Internal - Chain + /api/internal/player/profile: + get: + description: username으로 플레이어 프로필을 조회합니다 (게임 서버용) + parameters: + - description: 유저명 + in: query + name: username + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.PlayerProfileResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 프로필 조회 (내부 API) + tags: + - Internal - Player + /api/internal/player/save: + post: + consumes: + - application/json + description: username으로 게임 데이터를 저장합니다 (게임 서버용) + parameters: + - description: 유저명 + in: query + name: username + required: true + type: string + - description: 게임 데이터 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.GameDataRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.MessageResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - ApiKeyAuth: [] + summary: 게임 데이터 저장 (내부 API) + tags: + - Internal - Player + /api/player/profile: + get: + description: 현재 유저의 플레이어 프로필을 조회합니다 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.PlayerProfileResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 내 프로필 조회 + tags: + - Player + put: + consumes: + - application/json + description: 현재 유저의 닉네임을 수정합니다 + parameters: + - description: 수정할 프로필 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.UpdateProfileRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/player.PlayerProfile' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 프로필 수정 + tags: + - Player + /api/users/: + get: + description: 모든 유저 목록을 조회합니다 + parameters: + - default: 0 + description: 시작 위치 + in: query + name: offset + type: integer + - default: 50 + description: 조회 수 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/docs.UserResponse' + type: array + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 전체 유저 목록 (관리자) + tags: + - Users + /api/users/{id}: + delete: + description: 유저를 삭제합니다 + parameters: + - description: 유저 ID + in: path + name: id + required: true + type: integer + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 유저 삭제 (관리자) + tags: + - Users + /api/users/{id}/role: + patch: + consumes: + - application/json + description: 유저의 역할을 admin 또는 user로 변경합니다 + parameters: + - description: 유저 ID + in: path + name: id + required: true + type: integer + - description: 변경할 역할 + in: body + name: body + required: true + schema: + $ref: '#/definitions/docs.UpdateRoleRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/docs.MessageResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/docs.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/docs.ErrorResponse' + "403": + description: Forbidden + schema: + $ref: '#/definitions/docs.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/docs.ErrorResponse' + security: + - BearerAuth: [] + summary: 유저 권한 변경 (관리자) + tags: + - Users +securityDefinitions: + ApiKeyAuth: + description: 내부 API 키 (게임 서버 ↔ API 서버 통신용) + in: header + name: X-API-Key + type: apiKey + BearerAuth: + description: 'JWT Bearer 토큰 (예: Bearer eyJhbGci...)' + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/docs/swagger_types.go b/docs/swagger_types.go new file mode 100644 index 0000000..233e8ef --- /dev/null +++ b/docs/swagger_types.go @@ -0,0 +1,348 @@ +// Package docs contains Swagger DTO types for API documentation. +// These types are only used by swag to generate OpenAPI specs. +package docs + +import "time" + +// --- Common --- + +// ErrorResponse is a standard error response. +type ErrorResponse struct { + Error string `json:"error" example:"오류 메시지"` +} + +// MessageResponse is a standard success message response. +type MessageResponse struct { + Message string `json:"message" example:"성공"` +} + +// StatusResponse is a simple status response. +type StatusResponse struct { + Status string `json:"status" example:"ok"` +} + +// --- Auth --- + +type RegisterRequest struct { + Username string `json:"username" example:"player1"` + Password string `json:"password" example:"mypassword"` +} + +type LoginRequest struct { + Username string `json:"username" example:"player1"` + Password string `json:"password" example:"mypassword"` +} + +type LoginResponse struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."` + Username string `json:"username" example:"player1"` + Role string `json:"role" example:"user"` +} + +type RefreshRequest struct { + RefreshToken string `json:"refreshToken,omitempty"` +} + +type RefreshResponse struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."` +} + +type UpdateRoleRequest struct { + Role string `json:"role" example:"admin"` +} + +type VerifyTokenRequest struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."` +} + +type VerifyTokenResponse struct { + Username string `json:"username" example:"player1"` +} + +type SSAFYLoginURLResponse struct { + URL string `json:"url" example:"https://edu.ssafy.com/oauth/authorize?..."` +} + +type SSAFYCallbackRequest struct { + Code string `json:"code" example:"auth_code_123"` + State string `json:"state" example:"random_state_string"` +} + +type LaunchTicketResponse struct { + Ticket string `json:"ticket" example:"ticket_abc123"` +} + +type RedeemTicketRequest struct { + Ticket string `json:"ticket" example:"ticket_abc123"` +} + +type RedeemTicketResponse struct { + Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIs..."` +} + +// UserResponse is a user in the admin user list. +type UserResponse struct { + ID uint `json:"id" example:"1"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Username string `json:"username" example:"player1"` + Role string `json:"role" example:"user"` + SsafyID *string `json:"ssafyId,omitempty" example:"ssafy_123"` +} + +// --- Announcement --- + +type AnnouncementResponse struct { + ID uint `json:"id" example:"1"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + Title string `json:"title" example:"서버 점검 안내"` + Content string `json:"content" example:"3월 16일 서버 점검이 예정되어 있습니다."` +} + +type CreateAnnouncementRequest struct { + Title string `json:"title" example:"서버 점검 안내"` + Content string `json:"content" example:"3월 16일 서버 점검이 예정되어 있습니다."` +} + +type UpdateAnnouncementRequest struct { + Title string `json:"title,omitempty" example:"수정된 제목"` + Content string `json:"content,omitempty" example:"수정된 내용"` +} + +// --- Download --- + +type DownloadInfoResponse struct { + ID uint `json:"id" example:"1"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + URL string `json:"url" example:"https://a301.api.tolelom.xyz/api/download/file"` + Version string `json:"version" example:"1.0.0"` + FileName string `json:"fileName" example:"A301_v1.0.zip"` + FileSize string `json:"fileSize" example:"1.5 GB"` + FileHash string `json:"fileHash" example:"a1b2c3d4e5f6..."` + LauncherURL string `json:"launcherUrl" example:"https://a301.api.tolelom.xyz/api/download/launcher"` + LauncherSize string `json:"launcherSize" example:"25.3 MB"` +} + +// --- Chain --- + +type WalletInfoResponse struct { + Address string `json:"address" example:"1a2b3c4d5e6f..."` + PubKeyHex string `json:"pubKeyHex" example:"abcdef012345..."` +} + +type TransferRequest struct { + To string `json:"to" example:"1a2b3c4d5e6f..."` + Amount uint64 `json:"amount" example:"100"` +} + +type TransferAssetRequest struct { + AssetID string `json:"assetId" example:"asset_001"` + To string `json:"to" example:"1a2b3c4d5e6f..."` +} + +type ListOnMarketRequest struct { + AssetID string `json:"assetId" example:"asset_001"` + Price uint64 `json:"price" example:"500"` +} + +type BuyFromMarketRequest struct { + ListingID string `json:"listingId" example:"listing_001"` +} + +type CancelListingRequest struct { + ListingID string `json:"listingId" example:"listing_001"` +} + +type EquipItemRequest struct { + AssetID string `json:"assetId" example:"asset_001"` + Slot string `json:"slot" example:"weapon"` +} + +type UnequipItemRequest struct { + AssetID string `json:"assetId" example:"asset_001"` +} + +type MintAssetRequest struct { + TemplateID string `json:"templateId" example:"sword_template"` + OwnerPubKey string `json:"ownerPubKey" example:"abcdef012345..."` + Properties map[string]any `json:"properties"` +} + +type GrantRewardRequest struct { + RecipientPubKey string `json:"recipientPubKey" example:"abcdef012345..."` + TokenAmount uint64 `json:"tokenAmount" example:"1000"` + Assets []MintAssetPayload `json:"assets"` +} + +type RegisterTemplateRequest struct { + ID string `json:"id" example:"sword_template"` + Name string `json:"name" example:"Sword"` + Schema map[string]any `json:"schema"` + Tradeable bool `json:"tradeable" example:"true"` +} + +type InternalGrantRewardRequest struct { + Username string `json:"username" example:"player1"` + TokenAmount uint64 `json:"tokenAmount" example:"1000"` + Assets []MintAssetPayload `json:"assets"` +} + +type InternalMintAssetRequest struct { + TemplateID string `json:"templateId" example:"sword_template"` + Username string `json:"username" example:"player1"` + Properties map[string]any `json:"properties"` +} + +type MintAssetPayload struct { + TemplateID string `json:"template_id" example:"sword_template"` + Owner string `json:"owner" example:"abcdef012345..."` + Properties map[string]any `json:"properties"` +} + +// --- Boss Raid --- + +type RequestEntryRequest struct { + Usernames []string `json:"usernames" example:"player1,player2"` + BossID int `json:"bossId" example:"1"` +} + +type RequestEntryAuthRequest struct { + Usernames []string `json:"usernames,omitempty"` + BossID int `json:"bossId" example:"1"` +} + +type RequestEntryResponse struct { + RoomID uint `json:"roomId" example:"1"` + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + BossID int `json:"bossId" example:"1"` + Players []string `json:"players"` + Status string `json:"status" example:"waiting"` + EntryToken string `json:"entryToken,omitempty" example:"token_abc"` +} + +type InternalRequestEntryResponse struct { + RoomID uint `json:"roomId" example:"1"` + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + BossID int `json:"bossId" example:"1"` + Players []string `json:"players"` + Status string `json:"status" example:"waiting"` + Tokens map[string]string `json:"tokens"` +} + +type SessionNameRequest struct { + SessionName string `json:"sessionName" example:"Dedi1_Room0"` +} + +type RoomStatusResponse struct { + RoomID uint `json:"roomId" example:"1"` + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + Status string `json:"status" example:"in_progress"` +} + +type PlayerReward struct { + Username string `json:"username" example:"player1"` + TokenAmount uint64 `json:"tokenAmount" example:"100"` + Assets []MintAssetPayload `json:"assets"` + Experience int `json:"experience" example:"500"` +} + +type CompleteRaidRequest struct { + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + Rewards []PlayerReward `json:"rewards"` +} + +type CompleteRaidResponse struct { + RoomID uint `json:"roomId" example:"1"` + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + Status string `json:"status" example:"completed"` + RewardResults []RewardResult `json:"rewardResults"` +} + +type RewardResult struct { + Username string `json:"username" example:"player1"` + Success bool `json:"success" example:"true"` + Error string `json:"error,omitempty"` +} + +type ValidateEntryTokenRequest struct { + EntryToken string `json:"entryToken" example:"token_abc"` +} + +type ValidateEntryTokenResponse struct { + Valid bool `json:"valid" example:"true"` + Username string `json:"username,omitempty" example:"player1"` + SessionName string `json:"sessionName,omitempty" example:"Dedi1_Room0"` +} + +type MyEntryTokenResponse struct { + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + EntryToken string `json:"entryToken" example:"token_abc"` +} + +type RegisterServerRequest struct { + ServerName string `json:"serverName" example:"Dedi1"` + InstanceID string `json:"instanceId" example:"container_abc"` + MaxRooms int `json:"maxRooms" example:"10"` +} + +type RegisterServerResponse struct { + SessionName string `json:"sessionName" example:"Dedi1_Room0"` + InstanceID string `json:"instanceId" example:"container_abc"` +} + +type HeartbeatRequest struct { + InstanceID string `json:"instanceId" example:"container_abc"` +} + +type ResetRoomRequest struct { + SessionName string `json:"sessionName" example:"Dedi1_Room0"` +} + +type ResetRoomResponse struct { + Status string `json:"status" example:"ok"` + SessionName string `json:"sessionName" example:"Dedi1_Room0"` +} + +// --- Player --- + +type PlayerProfileResponse struct { + ID uint `json:"id" example:"1"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + UserID uint `json:"userId" example:"1"` + Nickname string `json:"nickname" example:"용사"` + Level int `json:"level" example:"5"` + Experience int `json:"experience" example:"1200"` + NextExp int `json:"nextExp" example:"2000"` + MaxHP float64 `json:"maxHp" example:"150"` + MaxMP float64 `json:"maxMp" example:"75"` + AttackPower float64 `json:"attackPower" example:"25"` + AttackRange float64 `json:"attackRange" example:"3"` + SprintMultiplier float64 `json:"sprintMultiplier" example:"1.8"` + LastPosX float64 `json:"lastPosX" example:"10.5"` + LastPosY float64 `json:"lastPosY" example:"0"` + LastPosZ float64 `json:"lastPosZ" example:"20.3"` + LastRotY float64 `json:"lastRotY" example:"90"` + TotalPlayTime int64 `json:"totalPlayTime" example:"3600"` +} + +type UpdateProfileRequest struct { + Nickname string `json:"nickname" example:"용사"` +} + +type GameDataRequest struct { + Level *int `json:"level,omitempty" example:"5"` + Experience *int `json:"experience,omitempty" example:"1200"` + MaxHP *float64 `json:"maxHp,omitempty" example:"150"` + MaxMP *float64 `json:"maxMp,omitempty" example:"75"` + AttackPower *float64 `json:"attackPower,omitempty" example:"25"` + AttackRange *float64 `json:"attackRange,omitempty" example:"3"` + SprintMultiplier *float64 `json:"sprintMultiplier,omitempty" example:"1.8"` + LastPosX *float64 `json:"lastPosX,omitempty" example:"10.5"` + LastPosY *float64 `json:"lastPosY,omitempty" example:"0"` + LastPosZ *float64 `json:"lastPosZ,omitempty" example:"20.3"` + LastRotY *float64 `json:"lastRotY,omitempty" example:"90"` + TotalPlayTime *int64 `json:"totalPlayTime,omitempty" example:"3600"` +} diff --git a/go.mod b/go.mod index 93d2d86..3dc513c 100644 --- a/go.mod +++ b/go.mod @@ -1,49 +1,81 @@ module a301_server -go 1.25 +go 1.25.0 require ( - github.com/gofiber/fiber/v2 v2.52.11 + github.com/gofiber/fiber/v2 v2.52.12 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/joho/godotenv v1.5.1 github.com/redis/go-redis/v9 v9.18.0 github.com/tolelom/tolchain v0.0.0 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/andybalholm/brotli v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.2.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/gofiber/swagger v1.1.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mailru/easyjson v0.9.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/swaggo/files/v2 v2.0.2 // indirect + github.com/swaggo/swag v1.16.6 // indirect github.com/tinylib/msgp v1.2.5 // indirect + github.com/urfave/cli/v2 v2.27.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect go.uber.org/atomic v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) replace github.com/tolelom/tolchain => ../tolchain diff --git a/go.sum b/go.sum index 5260a7f..2662d88 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,15 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28= +github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -10,15 +18,45 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gofiber/fiber/v2 v2.52.11 h1:5f4yzKLcBcF8ha1GQTWB+mpblWz3Vz6nSAbTL31HkWs= github.com/gofiber/fiber/v2 v2.52.11/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw= +github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA= +github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -31,8 +69,12 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -41,13 +83,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= @@ -66,18 +114,34 @@ github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfS github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= +github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= +github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -86,22 +150,44 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/announcement/handler.go b/internal/announcement/handler.go index 6a8b594..caa7d5b 100644 --- a/internal/announcement/handler.go +++ b/internal/announcement/handler.go @@ -16,6 +16,16 @@ func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } +// GetAll godoc +// @Summary 공지사항 목록 조회 +// @Description 공지사항 목록을 조회합니다 +// @Tags Announcements +// @Produce json +// @Param offset query int false "시작 위치" default(0) +// @Param limit query int false "조회 수" default(20) +// @Success 200 {array} docs.AnnouncementResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/announcements/ [get] func (h *Handler) GetAll(c *fiber.Ctx) error { offset := c.QueryInt("offset", 0) limit := c.QueryInt("limit", 20) @@ -32,6 +42,20 @@ func (h *Handler) GetAll(c *fiber.Ctx) error { return c.JSON(list) } +// Create godoc +// @Summary 공지사항 생성 (관리자) +// @Description 새 공지사항을 생성합니다 +// @Tags Announcements +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body docs.CreateAnnouncementRequest true "공지사항 내용" +// @Success 201 {object} docs.AnnouncementResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/announcements/ [post] func (h *Handler) Create(c *fiber.Ctx) error { var body struct { Title string `json:"title"` @@ -53,6 +77,22 @@ func (h *Handler) Create(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(a) } +// Update godoc +// @Summary 공지사항 수정 (관리자) +// @Description 공지사항을 수정합니다 +// @Tags Announcements +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "공지사항 ID" +// @Param body body docs.UpdateAnnouncementRequest true "수정할 내용" +// @Success 200 {object} docs.AnnouncementResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/announcements/{id} [put] func (h *Handler) Update(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { @@ -85,6 +125,19 @@ func (h *Handler) Update(c *fiber.Ctx) error { return c.JSON(a) } +// Delete godoc +// @Summary 공지사항 삭제 (관리자) +// @Description 공지사항을 삭제합니다 +// @Tags Announcements +// @Security BearerAuth +// @Param id path int true "공지사항 ID" +// @Success 204 +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/announcements/{id} [delete] func (h *Handler) Delete(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { diff --git a/internal/auth/handler.go b/internal/auth/handler.go index 1e7537f..d51394d 100644 --- a/internal/auth/handler.go +++ b/internal/auth/handler.go @@ -20,6 +20,18 @@ func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } +// Register godoc +// @Summary 회원가입 +// @Description 새로운 사용자 계정을 생성합니다 +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body docs.RegisterRequest true "회원가입 정보" +// @Success 201 {object} docs.MessageResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 409 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/auth/register [post] func (h *Handler) Register(c *fiber.Ctx) error { var req struct { Username string `json:"username"` @@ -50,6 +62,17 @@ func (h *Handler) Register(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "회원가입이 완료되었습니다"}) } +// Login godoc +// @Summary 로그인 +// @Description 사용자 인증 후 JWT 토큰을 발급합니다 +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body docs.LoginRequest true "로그인 정보" +// @Success 200 {object} docs.LoginResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/auth/login [post] func (h *Handler) Login(c *fiber.Ctx) error { var req struct { Username string `json:"username"` @@ -91,6 +114,17 @@ func (h *Handler) Login(c *fiber.Ctx) error { }) } +// Refresh godoc +// @Summary 토큰 갱신 +// @Description Refresh 토큰으로 새 Access 토큰을 발급합니다 (쿠키 또는 body) +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body docs.RefreshRequest false "Refresh 토큰 (쿠키 우선)" +// @Success 200 {object} docs.RefreshResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/auth/refresh [post] func (h *Handler) Refresh(c *fiber.Ctx) error { refreshTokenStr := c.Cookies("refresh_token") if refreshTokenStr == "" { @@ -126,6 +160,16 @@ func (h *Handler) Refresh(c *fiber.Ctx) error { }) } +// Logout godoc +// @Summary 로그아웃 +// @Description 현재 세션을 무효화합니다 +// @Tags Auth +// @Produce json +// @Security BearerAuth +// @Success 200 {object} docs.MessageResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/auth/logout [post] func (h *Handler) Logout(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { @@ -146,6 +190,19 @@ func (h *Handler) Logout(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "로그아웃 되었습니다"}) } +// GetAllUsers godoc +// @Summary 전체 유저 목록 (관리자) +// @Description 모든 유저 목록을 조회합니다 +// @Tags Users +// @Produce json +// @Security BearerAuth +// @Param offset query int false "시작 위치" default(0) +// @Param limit query int false "조회 수" default(50) +// @Success 200 {array} docs.UserResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/users/ [get] func (h *Handler) GetAllUsers(c *fiber.Ctx) error { offset := c.QueryInt("offset", 0) limit := c.QueryInt("limit", 50) @@ -162,6 +219,21 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error { return c.JSON(users) } +// UpdateRole godoc +// @Summary 유저 권한 변경 (관리자) +// @Description 유저의 역할을 admin 또는 user로 변경합니다 +// @Tags Users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path int true "유저 ID" +// @Param body body docs.UpdateRoleRequest true "변경할 역할" +// @Success 200 {object} docs.MessageResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/users/{id}/role [patch] func (h *Handler) UpdateRole(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { @@ -182,6 +254,18 @@ func (h *Handler) UpdateRole(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "권한이 변경되었습니다"}) } +// VerifyToken godoc +// @Summary 토큰 검증 (내부 API) +// @Description JWT 토큰을 검증하고 username을 반환합니다 +// @Tags Internal - Auth +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.VerifyTokenRequest true "검증할 토큰" +// @Success 200 {object} docs.VerifyTokenResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/internal/auth/verify [post] func (h *Handler) VerifyToken(c *fiber.Ctx) error { var req struct { Token string `json:"token"` @@ -200,6 +284,14 @@ func (h *Handler) VerifyToken(c *fiber.Ctx) error { }) } +// SSAFYLoginURL godoc +// @Summary SSAFY 로그인 URL +// @Description SSAFY OAuth 로그인 URL을 생성합니다 +// @Tags Auth +// @Produce json +// @Success 200 {object} docs.SSAFYLoginURLResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/auth/ssafy/login [get] func (h *Handler) SSAFYLoginURL(c *fiber.Ctx) error { loginURL, err := h.svc.GetSSAFYLoginURL() if err != nil { @@ -208,6 +300,17 @@ func (h *Handler) SSAFYLoginURL(c *fiber.Ctx) error { return c.JSON(fiber.Map{"url": loginURL}) } +// SSAFYCallback godoc +// @Summary SSAFY OAuth 콜백 +// @Description SSAFY 인가 코드를 교환하여 로그인합니다 +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body docs.SSAFYCallbackRequest true "인가 코드" +// @Success 200 {object} docs.LoginResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/auth/ssafy/callback [post] func (h *Handler) SSAFYCallback(c *fiber.Ctx) error { var req struct { Code string `json:"code"` @@ -242,8 +345,16 @@ func (h *Handler) SSAFYCallback(c *fiber.Ctx) error { }) } -// CreateLaunchTicket issues a one-time ticket for the game launcher. -// The launcher uses this ticket instead of receiving the JWT directly in the URL. +// CreateLaunchTicket godoc +// @Summary 런처 티켓 발급 +// @Description 게임 런처용 일회성 티켓을 발급합니다 +// @Tags Auth +// @Produce json +// @Security BearerAuth +// @Success 200 {object} docs.LaunchTicketResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/auth/launch-ticket [post] func (h *Handler) CreateLaunchTicket(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(uint) if !ok { @@ -256,8 +367,17 @@ func (h *Handler) CreateLaunchTicket(c *fiber.Ctx) error { return c.JSON(fiber.Map{"ticket": ticket}) } -// RedeemLaunchTicket exchanges a one-time ticket for an access token. -// Called by the game launcher, not the web browser. +// RedeemLaunchTicket godoc +// @Summary 런처 티켓 교환 +// @Description 일회성 티켓을 Access 토큰으로 교환합니다 +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body docs.RedeemTicketRequest true "티켓" +// @Success 200 {object} docs.RedeemTicketResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/auth/redeem-ticket [post] func (h *Handler) RedeemLaunchTicket(c *fiber.Ctx) error { var req struct { Ticket string `json:"ticket"` @@ -273,6 +393,18 @@ func (h *Handler) RedeemLaunchTicket(c *fiber.Ctx) error { return c.JSON(fiber.Map{"token": token}) } +// DeleteUser godoc +// @Summary 유저 삭제 (관리자) +// @Description 유저를 삭제합니다 +// @Tags Users +// @Security BearerAuth +// @Param id path int true "유저 ID" +// @Success 204 +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/users/{id} [delete] func (h *Handler) DeleteUser(c *fiber.Ctx) error { id, err := strconv.ParseUint(c.Params("id"), 10, 64) if err != nil { diff --git a/internal/bossraid/handler.go b/internal/bossraid/handler.go index fe875a2..31a4ac6 100644 --- a/internal/bossraid/handler.go +++ b/internal/bossraid/handler.go @@ -19,8 +19,18 @@ func bossError(c *fiber.Ctx, status int, userMsg string, err error) error { return c.Status(status).JSON(fiber.Map{"error": userMsg}) } -// RequestEntry handles POST /api/internal/bossraid/entry -// Called by MMO server when a party requests boss raid entry. +// RequestEntry godoc +// @Summary 보스 레이드 입장 요청 (내부 API) +// @Description MMO 서버에서 파티의 보스 레이드 입장을 요청합니다. 모든 플레이어의 entry token을 반환합니다. +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.RequestEntryRequest true "입장 정보" +// @Success 201 {object} docs.InternalRequestEntryResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 409 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/entry [post] func (h *Handler) RequestEntry(c *fiber.Ctx) error { var req struct { Usernames []string `json:"usernames"` @@ -38,7 +48,7 @@ func (h *Handler) RequestEntry(c *fiber.Ctx) error { } } - room, err := h.svc.RequestEntry(req.Usernames, req.BossID) + room, tokens, err := h.svc.RequestEntryWithTokens(req.Usernames, req.BossID) if err != nil { return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err) } @@ -49,11 +59,21 @@ func (h *Handler) RequestEntry(c *fiber.Ctx) error { "bossId": room.BossID, "players": req.Usernames, "status": room.Status, + "tokens": tokens, }) } -// StartRaid handles POST /api/internal/bossraid/start -// Called by dedicated server when the Fusion session begins. +// StartRaid godoc +// @Summary 레이드 시작 (내부 API) +// @Description Fusion 세션이 시작될 때 데디케이티드 서버에서 호출합니다 +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.SessionNameRequest true "세션 정보" +// @Success 200 {object} docs.RoomStatusResponse +// @Failure 400 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/start [post] func (h *Handler) StartRaid(c *fiber.Ctx) error { var req struct { SessionName string `json:"sessionName"` @@ -77,8 +97,18 @@ func (h *Handler) StartRaid(c *fiber.Ctx) error { }) } -// CompleteRaid handles POST /api/internal/bossraid/complete -// Called by dedicated server when the boss is killed. Distributes rewards. +// CompleteRaid godoc +// @Summary 레이드 완료 (내부 API) +// @Description 보스 처치 시 데디케이티드 서버에서 호출합니다. 보상을 분배합니다. +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param Idempotency-Key header string true "멱등성 키" +// @Param body body docs.CompleteRaidRequest true "완료 정보 및 보상" +// @Success 200 {object} docs.CompleteRaidResponse +// @Failure 400 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/complete [post] func (h *Handler) CompleteRaid(c *fiber.Ctx) error { var req struct { SessionName string `json:"sessionName"` @@ -104,8 +134,17 @@ func (h *Handler) CompleteRaid(c *fiber.Ctx) error { }) } -// FailRaid handles POST /api/internal/bossraid/fail -// Called by dedicated server on timeout or party wipe. +// FailRaid godoc +// @Summary 레이드 실패 (내부 API) +// @Description 타임아웃 또는 전멸 시 데디케이티드 서버에서 호출합니다 +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.SessionNameRequest true "세션 정보" +// @Success 200 {object} docs.RoomStatusResponse +// @Failure 400 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/fail [post] func (h *Handler) FailRaid(c *fiber.Ctx) error { var req struct { SessionName string `json:"sessionName"` @@ -129,9 +168,20 @@ func (h *Handler) FailRaid(c *fiber.Ctx) error { }) } -// RequestEntryAuth handles POST /api/bossraid/entry (JWT authenticated). -// Called by the game client to request boss raid entry. -// The authenticated user must be included in the usernames list. +// RequestEntryAuth godoc +// @Summary 보스 레이드 입장 요청 +// @Description 게임 클라이언트에서 보스 레이드 입장을 요청합니다. 인증된 유저가 입장 목록에 포함되어야 합니다. +// @Tags Boss Raid +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param body body docs.RequestEntryAuthRequest true "입장 정보" +// @Success 201 {object} docs.RequestEntryResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 409 {object} docs.ErrorResponse +// @Router /api/bossraid/entry [post] func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error { var req struct { Usernames []string `json:"usernames"` @@ -188,9 +238,16 @@ func (h *Handler) RequestEntryAuth(c *fiber.Ctx) error { }) } -// GetMyEntryToken handles GET /api/bossraid/my-entry-token (JWT authenticated). -// Returns the pending entry token for the authenticated user. -// Called by party members after the leader requests entry. +// GetMyEntryToken godoc +// @Summary 내 입장 토큰 조회 +// @Description 현재 유저의 대기 중인 입장 토큰을 조회합니다 +// @Tags Boss Raid +// @Produce json +// @Security BearerAuth +// @Success 200 {object} docs.MyEntryTokenResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/bossraid/my-entry-token [get] func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error { username, _ := c.Locals("username").(string) if username == "" { @@ -208,9 +265,18 @@ func (h *Handler) GetMyEntryToken(c *fiber.Ctx) error { }) } -// ValidateEntryToken handles POST /api/internal/bossraid/validate-entry (ServerAuth). -// Called by the dedicated server to validate a player's entry token. -// Consumes the token (one-time use). +// ValidateEntryToken godoc +// @Summary 입장 토큰 검증 (내부 API) +// @Description 데디케이티드 서버에서 플레이어의 입장 토큰을 검증합니다. 일회성 소모. +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.ValidateEntryTokenRequest true "토큰" +// @Success 200 {object} docs.ValidateEntryTokenResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/validate-entry [post] func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error { var req struct { EntryToken string `json:"entryToken"` @@ -237,8 +303,17 @@ func (h *Handler) ValidateEntryToken(c *fiber.Ctx) error { }) } -// GetRoom handles GET /api/internal/bossraid/room -// Query param: sessionName +// GetRoom godoc +// @Summary 방 정보 조회 (내부 API) +// @Description sessionName으로 보스 레이드 방 정보를 조회합니다 +// @Tags Internal - Boss Raid +// @Produce json +// @Security ApiKeyAuth +// @Param sessionName query string true "세션 이름" +// @Success 200 {object} bossraid.BossRoom +// @Failure 400 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/room [get] func (h *Handler) GetRoom(c *fiber.Ctx) error { sessionName := c.Query("sessionName") if sessionName == "" { @@ -252,3 +327,127 @@ func (h *Handler) GetRoom(c *fiber.Ctx) error { return c.JSON(room) } + +// RegisterServer godoc +// @Summary 데디케이티드 서버 등록 (내부 API) +// @Description 데디케이티드 서버 컨테이너가 시작 시 호출합니다. 룸 슬롯을 자동 할당합니다. +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.RegisterServerRequest true "서버 정보" +// @Success 201 {object} docs.RegisterServerResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 409 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/register [post] +func (h *Handler) RegisterServer(c *fiber.Ctx) error { + var req struct { + ServerName string `json:"serverName"` + InstanceID string `json:"instanceId"` + MaxRooms int `json:"maxRooms"` + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) + } + if req.ServerName == "" || req.InstanceID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "serverName과 instanceId는 필수입니다"}) + } + + sessionName, err := h.svc.RegisterServer(req.ServerName, req.InstanceID, req.MaxRooms) + if err != nil { + return bossError(c, fiber.StatusConflict, "서버 등록에 실패했습니다", err) + } + + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "sessionName": sessionName, + "instanceId": req.InstanceID, + }) +} + +// Heartbeat godoc +// @Summary 하트비트 (내부 API) +// @Description 데디케이티드 서버 컨테이너가 주기적으로 호출합니다 +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.HeartbeatRequest true "인스턴스 정보" +// @Success 200 {object} docs.StatusResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/heartbeat [post] +func (h *Handler) Heartbeat(c *fiber.Ctx) error { + var req struct { + InstanceID string `json:"instanceId"` + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) + } + if req.InstanceID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "instanceId는 필수입니다"}) + } + + if err := h.svc.Heartbeat(req.InstanceID); err != nil { + return bossError(c, fiber.StatusNotFound, "인스턴스를 찾을 수 없습니다", err) + } + + return c.JSON(fiber.Map{"status": "ok"}) +} + +// ResetRoom godoc +// @Summary 룸 슬롯 리셋 (내부 API) +// @Description 레이드 종료 후 슬롯을 idle 상태로 되돌립니다 +// @Tags Internal - Boss Raid +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param body body docs.ResetRoomRequest true "세션 정보" +// @Success 200 {object} docs.ResetRoomResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/reset-room [post] +func (h *Handler) ResetRoom(c *fiber.Ctx) error { + var req struct { + SessionName string `json:"sessionName"` + } + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) + } + if req.SessionName == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"}) + } + + if err := h.svc.ResetRoom(req.SessionName); err != nil { + return bossError(c, fiber.StatusInternalServerError, "슬롯 리셋에 실패했습니다", err) + } + + return c.JSON(fiber.Map{"status": "ok", "sessionName": req.SessionName}) +} + +// GetServerStatus godoc +// @Summary 서버 상태 조회 (내부 API) +// @Description 데디케이티드 서버의 정보와 룸 슬롯 목록을 조회합니다 +// @Tags Internal - Boss Raid +// @Produce json +// @Security ApiKeyAuth +// @Param serverName query string true "서버 이름" +// @Success 200 {object} map[string]interface{} +// @Failure 400 {object} docs.ErrorResponse +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/internal/bossraid/server-status [get] +func (h *Handler) GetServerStatus(c *fiber.Ctx) error { + serverName := c.Query("serverName") + if serverName == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "serverName은 필수입니다"}) + } + + server, slots, err := h.svc.GetServerStatus(serverName) + if err != nil { + return bossError(c, fiber.StatusNotFound, "서버를 찾을 수 없습니다", err) + } + + return c.JSON(fiber.Map{ + "server": server, + "slots": slots, + }) +} diff --git a/internal/bossraid/model.go b/internal/bossraid/model.go index 11d7d43..1d50e79 100644 --- a/internal/bossraid/model.go +++ b/internal/bossraid/model.go @@ -32,3 +32,41 @@ type BossRoom struct { StartedAt *time.Time `json:"startedAt,omitempty"` CompletedAt *time.Time `json:"completedAt,omitempty"` } + +// SlotStatus represents the status of a dedicated server room slot. +type SlotStatus string + +const ( + SlotIdle SlotStatus = "idle" + SlotWaiting SlotStatus = "waiting" + SlotInProgress SlotStatus = "in_progress" +) + + +// DedicatedServer represents a server group (e.g., "Dedi1"). +// Multiple containers (replicas) share the same server group name. +type DedicatedServer struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + ServerName string `json:"serverName" gorm:"type:varchar(100);uniqueIndex;not null"` + MaxRooms int `json:"maxRooms" gorm:"default:10;not null"` +} + +// RoomSlot represents a room slot on a dedicated server. +// Each slot has a stable session name that the Fusion NetworkRunner uses. +// InstanceID tracks which container process currently owns this slot. +type RoomSlot struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` + DedicatedServerID uint `json:"dedicatedServerId" gorm:"index;not null"` + SlotIndex int `json:"slotIndex" gorm:"not null"` + SessionName string `json:"sessionName" gorm:"type:varchar(100);uniqueIndex;not null"` + Status SlotStatus `json:"status" gorm:"type:varchar(20);index;default:idle;not null"` + BossRoomID *uint `json:"bossRoomId" gorm:"index"` + InstanceID string `json:"instanceId" gorm:"type:varchar(100);index"` + LastHeartbeat *time.Time `json:"lastHeartbeat"` +} diff --git a/internal/bossraid/repository.go b/internal/bossraid/repository.go index 99f848a..a7809f0 100644 --- a/internal/bossraid/repository.go +++ b/internal/bossraid/repository.go @@ -1,7 +1,9 @@ package bossraid import ( + "fmt" "strings" + "time" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -61,3 +63,165 @@ func (r *Repository) CountActiveByUsername(username string) (int64, error) { ).Count(&count).Error return count, err } + +// --- DedicatedServer & RoomSlot --- + +// UpsertDedicatedServer creates or updates a server group by name. +func (r *Repository) UpsertDedicatedServer(server *DedicatedServer) error { + var existing DedicatedServer + err := r.db.Where("server_name = ?", server.ServerName).First(&existing).Error + if err == gorm.ErrRecordNotFound { + return r.db.Create(server).Error + } + if err != nil { + return err + } + existing.MaxRooms = server.MaxRooms + return r.db.Save(&existing).Error +} + +// FindDedicatedServerByName finds a server group by name. +func (r *Repository) FindDedicatedServerByName(serverName string) (*DedicatedServer, error) { + var server DedicatedServer + if err := r.db.Where("server_name = ?", serverName).First(&server).Error; err != nil { + return nil, err + } + return &server, nil +} + +// EnsureRoomSlots ensures the correct number of room slots exist for a server. +func (r *Repository) EnsureRoomSlots(serverID uint, serverName string, maxRooms int) error { + for i := 0; i < maxRooms; i++ { + sessionName := fmt.Sprintf("%s_Room%d", serverName, i) + var existing RoomSlot + err := r.db.Where("session_name = ?", sessionName).First(&existing).Error + if err == gorm.ErrRecordNotFound { + slot := RoomSlot{ + DedicatedServerID: serverID, + SlotIndex: i, + SessionName: sessionName, + Status: SlotIdle, + } + if err := r.db.Create(&slot).Error; err != nil { + return err + } + } else if err != nil { + return err + } + } + return nil +} + +// AssignSlotToInstance finds an unassigned (or stale) slot and assigns it to the given instanceID. +// Returns the assigned slot with its sessionName. +func (r *Repository) AssignSlotToInstance(serverID uint, instanceID string, staleThreshold time.Time) (*RoomSlot, error) { + // First check if this instance already has a slot assigned + var existing RoomSlot + err := r.db. + Clauses(clause.Locking{Strength: "UPDATE"}). + Where("dedicated_server_id = ? AND instance_id = ?", serverID, instanceID). + First(&existing).Error + if err == nil { + // Already assigned — refresh heartbeat + now := time.Now() + existing.LastHeartbeat = &now + r.db.Save(&existing) + return &existing, nil + } + + // Find an unassigned slot (instance_id is empty or heartbeat is stale) + var slot RoomSlot + err = r.db. + Clauses(clause.Locking{Strength: "UPDATE"}). + Where("dedicated_server_id = ? AND (instance_id = '' OR instance_id IS NULL OR last_heartbeat < ?)", + serverID, staleThreshold). + Order("slot_index ASC"). + First(&slot).Error + if err != nil { + return nil, fmt.Errorf("사용 가능한 슬롯이 없습니다") + } + + // Assign this instance to the slot + now := time.Now() + slot.InstanceID = instanceID + slot.LastHeartbeat = &now + slot.Status = SlotIdle + slot.BossRoomID = nil + if err := r.db.Save(&slot).Error; err != nil { + return nil, err + } + return &slot, nil +} + +// UpdateHeartbeat updates the heartbeat for a specific instance. +func (r *Repository) UpdateHeartbeat(instanceID string) error { + now := time.Now() + result := r.db.Model(&RoomSlot{}). + Where("instance_id = ?", instanceID). + Update("last_heartbeat", now) + if result.RowsAffected == 0 { + return fmt.Errorf("인스턴스를 찾을 수 없습니다: %s", instanceID) + } + return result.Error +} + +// FindIdleRoomSlot finds an idle room slot with a live instance (with row-level lock). +func (r *Repository) FindIdleRoomSlot(staleThreshold time.Time) (*RoomSlot, error) { + var slot RoomSlot + err := r.db. + Clauses(clause.Locking{Strength: "UPDATE"}). + Where("status = ? AND instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat >= ?", + SlotIdle, staleThreshold). + Order("id ASC"). + First(&slot).Error + if err != nil { + return nil, err + } + return &slot, nil +} + +// UpdateRoomSlot updates a room slot. +func (r *Repository) UpdateRoomSlot(slot *RoomSlot) error { + return r.db.Save(slot).Error +} + +// FindRoomSlotBySession finds a room slot by its session name. +func (r *Repository) FindRoomSlotBySession(sessionName string) (*RoomSlot, error) { + var slot RoomSlot + if err := r.db.Where("session_name = ?", sessionName).First(&slot).Error; err != nil { + return nil, err + } + return &slot, nil +} + +// ResetRoomSlot sets a room slot back to idle and clears its BossRoomID. +// Does NOT clear InstanceID — the container still owns the slot. +func (r *Repository) ResetRoomSlot(sessionName string) error { + result := r.db.Model(&RoomSlot{}). + Where("session_name = ?", sessionName). + Updates(map[string]interface{}{ + "status": SlotIdle, + "boss_room_id": nil, + }) + return result.Error +} + +// ResetStaleSlots clears instanceID for slots with stale heartbeats +// and resets any active raids on those slots. +func (r *Repository) ResetStaleSlots(threshold time.Time) (int64, error) { + result := r.db.Model(&RoomSlot{}). + Where("instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat < ?", threshold). + Updates(map[string]interface{}{ + "instance_id": "", + "status": SlotIdle, + "boss_room_id": nil, + }) + return result.RowsAffected, result.Error +} + +// GetRoomSlotsByServer returns all room slots for a given server. +func (r *Repository) GetRoomSlotsByServer(serverID uint) ([]RoomSlot, error) { + var slots []RoomSlot + err := r.db.Where("dedicated_server_id = ?", serverID).Order("slot_index ASC").Find(&slots).Error + return slots, err +} diff --git a/internal/bossraid/service.go b/internal/bossraid/service.go index 5418834..f683b7c 100644 --- a/internal/bossraid/service.go +++ b/internal/bossraid/service.go @@ -34,6 +34,7 @@ type Service struct { repo *Repository rdb *redis.Client rewardGrant func(username string, tokenAmount uint64, assets []core.MintAssetPayload) error + expGrant func(username string, exp int) error } func NewService(repo *Repository, rdb *redis.Client) *Service { @@ -45,7 +46,13 @@ func (s *Service) SetRewardGranter(fn func(username string, tokenAmount uint64, s.rewardGrant = fn } +// SetExpGranter sets the callback for granting experience to players. +func (s *Service) SetExpGranter(fn func(username string, exp int) error) { + s.expGrant = fn +} + // RequestEntry creates a new boss room for a party. +// Allocates an idle room slot from a registered dedicated server. // Returns the room with assigned session name. func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error) { if len(usernames) == 0 { @@ -69,18 +76,17 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error return nil, fmt.Errorf("플레이어 목록 직렬화 실패: %w", err) } - sessionName := fmt.Sprintf("BossRaid_%d_%d", bossID, time.Now().UnixNano()) + var room *BossRoom - room := &BossRoom{ - SessionName: sessionName, - BossID: bossID, - Status: StatusWaiting, - MaxPlayers: defaultMaxPlayers, - Players: string(playersJSON), - } - - // Wrap active-room check + creation in a transaction to prevent TOCTOU race. + // Wrap slot allocation + active-room check + creation in a transaction. err = s.repo.Transaction(func(txRepo *Repository) error { + // Find an idle room slot from a live dedicated server instance + staleThreshold := time.Now().Add(-30 * time.Second) + slot, err := txRepo.FindIdleRoomSlot(staleThreshold) + if err != nil { + return fmt.Errorf("현재 이용 가능한 보스 레이드 방이 없습니다") + } + for _, username := range usernames { count, err := txRepo.CountActiveByUsername(username) if err != nil { @@ -90,9 +96,24 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error return fmt.Errorf("플레이어 %s가 이미 보스 레이드 중입니다", username) } } + + room = &BossRoom{ + SessionName: slot.SessionName, + BossID: bossID, + Status: StatusWaiting, + MaxPlayers: defaultMaxPlayers, + Players: string(playersJSON), + } if err := txRepo.Create(room); err != nil { return fmt.Errorf("방 생성 실패: %w", err) } + + // Mark slot as waiting and link to the boss room + slot.Status = SlotWaiting + slot.BossRoomID = &room.ID + if err := txRepo.UpdateRoomSlot(slot); err != nil { + return fmt.Errorf("슬롯 상태 업데이트 실패: %w", err) + } return nil }) if err != nil { @@ -102,7 +123,7 @@ func (s *Service) RequestEntry(usernames []string, bossID int) (*BossRoom, error return room, nil } -// StartRaid marks a room as in_progress. +// StartRaid marks a room as in_progress and updates the slot status. // Uses row-level locking to prevent concurrent state transitions. func (s *Service) StartRaid(sessionName string) (*BossRoom, error) { var resultRoom *BossRoom @@ -122,6 +143,14 @@ func (s *Service) StartRaid(sessionName string) (*BossRoom, error) { if err := txRepo.Update(room); err != nil { return fmt.Errorf("상태 업데이트 실패: %w", err) } + + // Update slot status to in_progress + slot, err := txRepo.FindRoomSlotBySession(sessionName) + if err == nil { + slot.Status = SlotInProgress + txRepo.UpdateRoomSlot(slot) + } + resultRoom = room return nil }) @@ -136,6 +165,7 @@ type PlayerReward struct { Username string `json:"username"` TokenAmount uint64 `json:"tokenAmount"` Assets []core.MintAssetPayload `json:"assets"` + Experience int `json:"experience"` // 경험치 보상 } // RewardResult holds the result of granting a reward to one player. @@ -204,10 +234,26 @@ func (s *Service) CompleteRaid(sessionName string, rewards []PlayerReward) (*Bos } } + // Grant experience to players + if s.expGrant != nil { + for _, r := range rewards { + if r.Experience > 0 { + if expErr := s.expGrant(r.Username, r.Experience); expErr != nil { + log.Printf("경험치 지급 실패: %s: %v", r.Username, expErr) + } + } + } + } + + // Reset slot to idle so it can accept new raids + if err := s.repo.ResetRoomSlot(sessionName); err != nil { + log.Printf("슬롯 리셋 실패 (complete): %s: %v", sessionName, err) + } + return resultRoom, resultRewards, nil } -// FailRaid marks a room as failed. +// FailRaid marks a room as failed and resets the slot. // Uses row-level locking to prevent concurrent state transitions. func (s *Service) FailRaid(sessionName string) (*BossRoom, error) { var resultRoom *BossRoom @@ -233,6 +279,12 @@ func (s *Service) FailRaid(sessionName string) (*BossRoom, error) { if err != nil { return nil, err } + + // Reset slot to idle so it can accept new raids + if err := s.repo.ResetRoomSlot(sessionName); err != nil { + log.Printf("슬롯 리셋 실패 (fail): %s: %v", sessionName, err) + } + return resultRoom, nil } @@ -345,3 +397,85 @@ func (s *Service) RequestEntryWithTokens(usernames []string, bossID int) (*BossR return room, tokens, nil } + +// --- Dedicated Server Management --- + +const staleTimeout = 30 * time.Second + +// RegisterServer registers a dedicated server instance (container). +// Creates the server group + slots if needed, then assigns a slot to this instance. +// Returns the assigned sessionName. +func (s *Service) RegisterServer(serverName, instanceID string, maxRooms int) (string, error) { + if serverName == "" || instanceID == "" { + return "", fmt.Errorf("serverName과 instanceId는 필수입니다") + } + if maxRooms <= 0 { + maxRooms = 10 + } + + // Ensure server group exists + server := &DedicatedServer{ + ServerName: serverName, + MaxRooms: maxRooms, + } + if err := s.repo.UpsertDedicatedServer(server); err != nil { + return "", fmt.Errorf("서버 그룹 등록 실패: %w", err) + } + + // Re-fetch to get the ID + server, err := s.repo.FindDedicatedServerByName(serverName) + if err != nil { + return "", fmt.Errorf("서버 조회 실패: %w", err) + } + + // Ensure all room slots exist + if err := s.repo.EnsureRoomSlots(server.ID, serverName, maxRooms); err != nil { + return "", fmt.Errorf("슬롯 생성 실패: %w", err) + } + + // Assign a slot to this instance + staleThreshold := time.Now().Add(-staleTimeout) + slot, err := s.repo.AssignSlotToInstance(server.ID, instanceID, staleThreshold) + if err != nil { + return "", fmt.Errorf("슬롯 배정 실패: %w", err) + } + + return slot.SessionName, nil +} + +// Heartbeat updates the heartbeat for a container instance. +func (s *Service) Heartbeat(instanceID string) error { + return s.repo.UpdateHeartbeat(instanceID) +} + +// CheckStaleSlots resets slots whose instances have gone silent. +func (s *Service) CheckStaleSlots() { + threshold := time.Now().Add(-staleTimeout) + count, err := s.repo.ResetStaleSlots(threshold) + if err != nil { + log.Printf("스태일 슬롯 체크 실패: %v", err) + return + } + if count > 0 { + log.Printf("스태일 슬롯 %d개 리셋", count) + } +} + +// ResetRoom resets a room slot back to idle. +// Called by the dedicated server after a raid ends and the runner is recycled. +func (s *Service) ResetRoom(sessionName string) error { + return s.repo.ResetRoomSlot(sessionName) +} + +// GetServerStatus returns a server group and its room slots. +func (s *Service) GetServerStatus(serverName string) (*DedicatedServer, []RoomSlot, error) { + server, err := s.repo.FindDedicatedServerByName(serverName) + if err != nil { + return nil, nil, fmt.Errorf("서버를 찾을 수 없습니다: %w", err) + } + slots, err := s.repo.GetRoomSlotsByServer(server.ID) + if err != nil { + return nil, nil, fmt.Errorf("슬롯 조회 실패: %w", err) + } + return server, slots, nil +} diff --git a/internal/chain/handler.go b/internal/chain/handler.go index c098673..3857d99 100644 --- a/internal/chain/handler.go +++ b/internal/chain/handler.go @@ -52,6 +52,16 @@ func chainError(c *fiber.Ctx, status int, userMsg string, err error) error { // ---- 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 { @@ -67,6 +77,16 @@ func (h *Handler) GetWalletInfo(c *fiber.Ctx) error { }) } +// 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 { @@ -79,6 +99,18 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error { 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 { @@ -93,6 +125,18 @@ func (h *Handler) GetAssets(c *fiber.Ctx) error { 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) { @@ -106,6 +150,16 @@ func (h *Handler) GetAsset(c *fiber.Ctx) error { 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 { @@ -119,6 +173,17 @@ func (h *Handler) GetInventory(c *fiber.Ctx) error { 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) @@ -129,6 +194,17 @@ func (h *Handler) GetMarketListings(c *fiber.Ctx) error { 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) { @@ -144,6 +220,20 @@ func (h *Handler) GetMarketListing(c *fiber.Ctx) error { // ---- 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 { @@ -166,6 +256,20 @@ func (h *Handler) Transfer(c *fiber.Ctx) error { 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 { @@ -188,6 +292,20 @@ func (h *Handler) TransferAsset(c *fiber.Ctx) error { 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 { @@ -210,6 +328,20 @@ func (h *Handler) ListOnMarket(c *fiber.Ctx) error { 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 { @@ -231,6 +363,20 @@ func (h *Handler) BuyFromMarket(c *fiber.Ctx) error { 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 { @@ -252,6 +398,20 @@ func (h *Handler) CancelListing(c *fiber.Ctx) error { 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 { @@ -274,6 +434,20 @@ func (h *Handler) EquipItem(c *fiber.Ctx) error { 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 { @@ -297,6 +471,21 @@ func (h *Handler) UnequipItem(c *fiber.Ctx) error { // ---- 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"` @@ -316,6 +505,21 @@ func (h *Handler) MintAsset(c *fiber.Ctx) error { 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"` @@ -335,6 +539,21 @@ func (h *Handler) GrantReward(c *fiber.Ctx) error { 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"` @@ -357,7 +576,19 @@ func (h *Handler) RegisterTemplate(c *fiber.Ctx) error { // ---- Internal Handlers (game server, username-based) ---- -// InternalGrantReward grants reward by username. For game server use. +// 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"` @@ -377,7 +608,19 @@ func (h *Handler) InternalGrantReward(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(result) } -// InternalMintAsset mints an asset by username. For game server use. +// 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"` @@ -397,7 +640,17 @@ func (h *Handler) InternalMintAsset(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(result) } -// InternalGetBalance returns balance by username. For game server use. +// 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) { @@ -410,7 +663,19 @@ func (h *Handler) InternalGetBalance(c *fiber.Ctx) error { return c.JSON(result) } -// InternalGetAssets returns assets by username. For game server use. +// 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) { @@ -425,7 +690,17 @@ func (h *Handler) InternalGetAssets(c *fiber.Ctx) error { return c.Send(result) } -// InternalGetInventory returns inventory by username. For game server use. +// 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) { diff --git a/internal/download/handler.go b/internal/download/handler.go index fe4754b..dec46f8 100644 --- a/internal/download/handler.go +++ b/internal/download/handler.go @@ -19,6 +19,14 @@ func NewHandler(svc *Service, baseURL string) *Handler { return &Handler{svc: svc, baseURL: baseURL} } +// GetInfo godoc +// @Summary 다운로드 정보 조회 +// @Description 게임 및 런처 다운로드 정보를 조회합니다 +// @Tags Download +// @Produce json +// @Success 200 {object} docs.DownloadInfoResponse +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/download/info [get] func (h *Handler) GetInfo(c *fiber.Ctx) error { info, err := h.svc.GetInfo() if err != nil { @@ -27,8 +35,20 @@ func (h *Handler) GetInfo(c *fiber.Ctx) error { return c.JSON(info) } -// Upload accepts a raw binary body (application/octet-stream). -// The filename is passed as a query parameter: ?filename=A301_v1.0.zip +// Upload godoc +// @Summary 게임 파일 업로드 (관리자) +// @Description 게임 zip 파일을 스트리밍 업로드합니다. Body는 raw binary입니다. +// @Tags Download +// @Accept application/octet-stream +// @Produce json +// @Security BearerAuth +// @Param filename query string false "파일명" default(game.zip) +// @Success 200 {object} docs.DownloadInfoResponse +// @Failure 400 {object} docs.ErrorResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/download/upload/game [post] func (h *Handler) Upload(c *fiber.Ctx) error { filename := strings.TrimSpace(c.Query("filename", "game.zip")) // 경로 순회 방지: 디렉토리 구분자 제거, 기본 파일명만 사용 @@ -49,6 +69,14 @@ func (h *Handler) Upload(c *fiber.Ctx) error { return c.JSON(info) } +// ServeFile godoc +// @Summary 게임 파일 다운로드 +// @Description 게임 zip 파일을 다운로드합니다 +// @Tags Download +// @Produce application/octet-stream +// @Success 200 {file} binary +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/download/file [get] func (h *Handler) ServeFile(c *fiber.Ctx) error { path := h.svc.GameFilePath() if _, err := os.Stat(path); err != nil { @@ -63,6 +91,18 @@ func (h *Handler) ServeFile(c *fiber.Ctx) error { return c.SendFile(path) } +// UploadLauncher godoc +// @Summary 런처 업로드 (관리자) +// @Description 런처 실행 파일을 스트리밍 업로드합니다. Body는 raw binary입니다. +// @Tags Download +// @Accept application/octet-stream +// @Produce json +// @Security BearerAuth +// @Success 200 {object} docs.DownloadInfoResponse +// @Failure 401 {object} docs.ErrorResponse +// @Failure 403 {object} docs.ErrorResponse +// @Failure 500 {object} docs.ErrorResponse +// @Router /api/download/upload/launcher [post] func (h *Handler) UploadLauncher(c *fiber.Ctx) error { body := c.Request().BodyStream() info, err := h.svc.UploadLauncher(body, h.baseURL) @@ -73,6 +113,14 @@ func (h *Handler) UploadLauncher(c *fiber.Ctx) error { return c.JSON(info) } +// ServeLauncher godoc +// @Summary 런처 다운로드 +// @Description 런처 실행 파일을 다운로드합니다 +// @Tags Download +// @Produce application/octet-stream +// @Success 200 {file} binary +// @Failure 404 {object} docs.ErrorResponse +// @Router /api/download/launcher [get] func (h *Handler) ServeLauncher(c *fiber.Ctx) error { path := h.svc.LauncherFilePath() if _, err := os.Stat(path); err != nil { diff --git a/internal/player/handler.go b/internal/player/handler.go index 9afec82..afc4bfd 100644 --- a/internal/player/handler.go +++ b/internal/player/handler.go @@ -16,7 +16,16 @@ func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } -// GetProfile 자신의 프로필 조회 (JWT 인증) +// 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 { @@ -28,10 +37,22 @@ func (h *Handler) GetProfile(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) } - return c.JSON(profile) + return c.JSON(profileWithNextExp(profile)) } -// UpdateProfile 자신의 프로필 수정 (JWT 인증) +// 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 { @@ -67,7 +88,17 @@ func (h *Handler) UpdateProfile(c *fiber.Ctx) error { return c.JSON(profile) } -// InternalGetProfile 내부 API: username 쿼리 파라미터로 프로필 조회 +// 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 == "" { @@ -79,10 +110,50 @@ func (h *Handler) InternalGetProfile(c *fiber.Ctx) error { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) } - return c.JSON(profile) + return c.JSON(profileWithNextExp(profile)) } -// InternalSaveGameData 내부 API: username 쿼리 파라미터로 게임 데이터 저장 +// 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 == "" { diff --git a/internal/player/level.go b/internal/player/level.go new file mode 100644 index 0000000..3ab97f9 --- /dev/null +++ b/internal/player/level.go @@ -0,0 +1,71 @@ +package player + +// MaxLevel is the maximum player level. +const MaxLevel = 50 + +// RequiredExp returns the experience needed to reach the next level. +// Formula: level^2 * 100 +func RequiredExp(level int) int { + return level * level * 100 +} + +// CalcStatsForLevel computes combat stats for a given level. +func CalcStatsForLevel(level int) (maxHP, maxMP, attackPower float64) { + maxHP = 100 + float64(level-1)*10 + maxMP = 50 + float64(level-1)*5 + attackPower = 10 + float64(level-1)*2 + return +} + +// LevelUpResult holds the result of applying experience gain. +type LevelUpResult struct { + OldLevel int `json:"oldLevel"` + NewLevel int `json:"newLevel"` + Experience int `json:"experience"` + NextExp int `json:"nextExp"` + MaxHP float64 `json:"maxHp"` + MaxMP float64 `json:"maxMp"` + AttackPower float64 `json:"attackPower"` + LeveledUp bool `json:"leveledUp"` +} + +// ApplyExperience adds exp to current level/exp and applies level ups. +// Returns the result including new stats if leveled up. +func ApplyExperience(currentLevel, currentExp, gainedExp int) LevelUpResult { + level := currentLevel + exp := currentExp + gainedExp + + // Process level ups + for level < MaxLevel { + required := RequiredExp(level) + if exp < required { + break + } + exp -= required + level++ + } + + // Cap at max level + if level >= MaxLevel { + level = MaxLevel + exp = 0 // No more exp needed at max level + } + + maxHP, maxMP, attackPower := CalcStatsForLevel(level) + + nextExp := 0 + if level < MaxLevel { + nextExp = RequiredExp(level) + } + + return LevelUpResult{ + OldLevel: currentLevel, + NewLevel: level, + Experience: exp, + NextExp: nextExp, + MaxHP: maxHP, + MaxMP: maxMP, + AttackPower: attackPower, + LeveledUp: level > currentLevel, + } +} diff --git a/internal/player/service.go b/internal/player/service.go index c198c34..4439bac 100644 --- a/internal/player/service.go +++ b/internal/player/service.go @@ -165,6 +165,43 @@ func (s *Service) SaveGameDataByUsername(username string, data *GameDataRequest) return s.SaveGameData(userID, data) } +// GrantExperience adds experience to a player and handles level ups + stat recalculation. +func (s *Service) GrantExperience(userID uint, exp int) (*LevelUpResult, error) { + profile, err := s.repo.FindByUserID(userID) + if err != nil { + return nil, fmt.Errorf("프로필이 존재하지 않습니다") + } + + result := ApplyExperience(profile.Level, profile.Experience, exp) + + updates := map[string]interface{}{ + "level": result.NewLevel, + "experience": result.Experience, + "max_hp": result.MaxHP, + "max_mp": result.MaxMP, + "attack_power": result.AttackPower, + } + + if err := s.repo.UpdateStats(userID, updates); err != nil { + return nil, fmt.Errorf("레벨업 저장 실패: %w", err) + } + + return &result, nil +} + +// GrantExperienceByUsername grants experience to a player by username. +func (s *Service) GrantExperienceByUsername(username string, exp int) error { + if s.userResolver == nil { + return fmt.Errorf("userResolver가 설정되지 않았습니다") + } + userID, err := s.userResolver(username) + if err != nil { + return fmt.Errorf("존재하지 않는 유저입니다") + } + _, err = s.GrantExperience(userID, exp) + return err +} + // GameDataRequest 게임 데이터 저장 요청 (nil 필드는 변경하지 않음). type GameDataRequest struct { Level *int `json:"level,omitempty"` diff --git a/main.go b/main.go index 5a4497e..a1e5f36 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( "a301_server/internal/download" "a301_server/internal/player" + _ "a301_server/docs" // swagger docs + "github.com/tolelom/tolchain/core" "a301_server/pkg/config" "a301_server/pkg/database" @@ -27,6 +29,22 @@ import ( "github.com/gofiber/fiber/v2/middleware/logger" ) +// @title One of the Plans API +// @version 1.0 +// @description 멀티플레이어 보스 레이드 게임 플랫폼 백엔드 API +// @host a301.api.tolelom.xyz +// @BasePath / + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description JWT Bearer 토큰 (예: Bearer eyJhbGci...) + +// @securityDefinitions.apikey ApiKeyAuth +// @in header +// @name X-API-Key +// @description 내부 API 키 (게임 서버 ↔ API 서버 통신용) + func main() { config.Load() config.WarnInsecureDefaults() @@ -37,7 +55,7 @@ func main() { log.Println("MySQL 연결 성공") // AutoMigrate - if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &player.PlayerProfile{}); err != nil { + if err := database.DB.AutoMigrate(&auth.User{}, &announcement.Announcement{}, &download.Info{}, &chain.UserWallet{}, &bossraid.BossRoom{}, &bossraid.DedicatedServer{}, &bossraid.RoomSlot{}, &player.PlayerProfile{}); err != nil { log.Fatalf("AutoMigrate 실패: %v", err) } @@ -106,6 +124,9 @@ func main() { _, err := chainSvc.GrantRewardByUsername(username, tokenAmount, assets) return err }) + brSvc.SetExpGranter(func(username string, exp int) error { + return playerSvc.GrantExperienceByUsername(username, exp) + }) brHandler := bossraid.NewHandler(brSvc) if config.C.InternalAPIKey == "" { @@ -196,6 +217,15 @@ func main() { routes.Register(app, authHandler, annHandler, dlHandler, chainHandler, brHandler, playerHandler, authLimiter, apiLimiter, healthCheck, readyCheck, chainUserLimiter) + // Background: stale dedicated server detection + go func() { + ticker := time.NewTicker(15 * time.Second) + defer ticker.Stop() + for range ticker.C { + brSvc.CheckStaleSlots() + } + }() + // Graceful shutdown go func() { sigCh := make(chan os.Signal, 1) diff --git a/routes/routes.go b/routes/routes.go index c42bff9..302891e 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -9,6 +9,7 @@ import ( "a301_server/internal/player" "a301_server/pkg/middleware" "github.com/gofiber/fiber/v2" + "github.com/gofiber/swagger" ) func Register( @@ -25,6 +26,9 @@ func Register( readyCheck fiber.Handler, chainUserLimiter fiber.Handler, ) { + // Swagger UI + app.Get("/swagger/*", swagger.HandlerDefault) + // Health / Ready (rate limiter 밖) app.Get("/health", healthCheck) app.Get("/ready", readyCheck) @@ -104,6 +108,10 @@ func Register( br.Post("/fail", brH.FailRaid) br.Get("/room", brH.GetRoom) br.Post("/validate-entry", brH.ValidateEntryToken) + br.Post("/register", brH.RegisterServer) + br.Post("/heartbeat", brH.Heartbeat) + br.Post("/reset-room", brH.ResetRoom) + br.Get("/server-status", brH.GetServerStatus) // Player Profile (authenticated) p := api.Group("/player", middleware.Auth)