package bossraid import ( "log" "github.com/gofiber/fiber/v2" ) type Handler struct { svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } func bossError(c *fiber.Ctx, status int, userMsg string, err error) error { log.Printf("bossraid error: %s: %v", userMsg, err) return c.Status(status).JSON(fiber.Map{"error": userMsg}) } // 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"` BossID int `json:"bossId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if len(req.Usernames) == 0 || req.BossID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "usernames와 bossId는 필수입니다"}) } for _, u := range req.Usernames { if len(u) == 0 || len(u) > 50 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효하지 않은 username입니다"}) } } room, tokens, err := h.svc.RequestEntryWithTokens(req.Usernames, req.BossID) if err != nil { return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "roomId": room.ID, "sessionName": room.SessionName, "bossId": room.BossID, "players": req.Usernames, "status": room.Status, "tokens": tokens, }) } // 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"` } 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은 필수입니다"}) } room, err := h.svc.StartRaid(req.SessionName) if err != nil { return bossError(c, fiber.StatusBadRequest, "레이드 시작에 실패했습니다", err) } return c.JSON(fiber.Map{ "roomId": room.ID, "sessionName": room.SessionName, "status": room.Status, }) } // 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"` Rewards []PlayerReward `json:"rewards"` } 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은 필수입니다"}) } room, results, err := h.svc.CompleteRaid(req.SessionName, req.Rewards) if err != nil { return bossError(c, fiber.StatusBadRequest, "레이드 완료 처리에 실패했습니다", err) } return c.JSON(fiber.Map{ "roomId": room.ID, "sessionName": room.SessionName, "status": room.Status, "rewardResults": results, }) } // 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"` } 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은 필수입니다"}) } room, err := h.svc.FailRaid(req.SessionName) if err != nil { return bossError(c, fiber.StatusBadRequest, "레이드 실패 처리에 실패했습니다", err) } return c.JSON(fiber.Map{ "roomId": room.ID, "sessionName": room.SessionName, "status": room.Status, }) } // 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"` BossID int `json:"bossId"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } // 인증된 유저의 username authUsername, _ := c.Locals("username").(string) if authUsername == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 없습니다"}) } // 빈 usernames이면 솔로 입장 — 본인만 포함 if len(req.Usernames) == 0 { req.Usernames = []string{authUsername} } if req.BossID <= 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bossId는 필수입니다"}) } // 인증된 유저가 요청 목록에 포함되어 있는지 검증 found := false for _, u := range req.Usernames { if u == authUsername { found = true break } } if !found { return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "본인이 입장 목록에 포함되어야 합니다"}) } for _, u := range req.Usernames { if len(u) == 0 || len(u) > 50 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "유효하지 않은 username입니다"}) } } room, tokens, err := h.svc.RequestEntryWithTokens(req.Usernames, req.BossID) if err != nil { return bossError(c, fiber.StatusConflict, "보스 레이드 입장에 실패했습니다", err) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "roomId": room.ID, "sessionName": room.SessionName, "bossId": room.BossID, "players": req.Usernames, "status": room.Status, "entryToken": tokens[authUsername], }) } // 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 == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증 정보가 없습니다"}) } sessionName, entryToken, err := h.svc.GetMyEntryToken(username) if err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "sessionName": sessionName, "entryToken": entryToken, }) } // 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"` } if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "잘못된 요청입니다"}) } if req.EntryToken == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "entryToken은 필수입니다"}) } username, sessionName, err := h.svc.ValidateEntryToken(req.EntryToken) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "valid": false, "error": err.Error(), }) } return c.JSON(fiber.Map{ "valid": true, "username": username, "sessionName": 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 == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "sessionName은 필수입니다"}) } room, err := h.svc.GetRoom(sessionName) if err != nil { return bossError(c, fiber.StatusNotFound, "방을 찾을 수 없습니다", err) } 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, }) }