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 handles POST /api/internal/bossraid/entry // Called by MMO server when a party requests boss raid entry. 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, err := h.svc.RequestEntry(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, }) } // StartRaid handles POST /api/internal/bossraid/start // Called by dedicated server when the Fusion session begins. 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 handles POST /api/internal/bossraid/complete // Called by dedicated server when the boss is killed. Distributes rewards. 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 handles POST /api/internal/bossraid/fail // Called by dedicated server on timeout or party wipe. 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 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. 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 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. 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 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). 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 handles GET /api/internal/bossraid/room // Query param: sessionName 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) }