-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add Valorant roles/description to contest application and member list (#73) #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE contests_members | ||
| DROP COLUMN valorant_roles, | ||
| DROP COLUMN description; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| ALTER TABLE contests_members | ||
| ADD COLUMN valorant_roles JSON DEFAULT NULL AFTER point, | ||
| ADD COLUMN description VARCHAR(64) DEFAULT '' AFTER valorant_roles; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -58,7 +58,20 @@ func (s *ContestApplicationService) SetScoreTablePort(scoreTableRepo pointPort.V | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // RequestParticipate - Contest 참가 신청 | ||||||||||||||||||||||||||
| func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64) (*dto.DiscordLinkRequiredResponse, error) { | ||||||||||||||||||||||||||
| func (s *ContestApplicationService) RequestParticipate(ctx context.Context, contestId, userId int64, req *dto.RequestParticipateRequest) (*dto.DiscordLinkRequiredResponse, error) { | ||||||||||||||||||||||||||
| // Validate request if provided | ||||||||||||||||||||||||||
| if req != nil { | ||||||||||||||||||||||||||
| if len(req.Description) > 64 { | ||||||||||||||||||||||||||
| return nil, exception.ErrDescriptionTooLong | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if len(req.ValorantRoles) > 0 { | ||||||||||||||||||||||||||
| roles := domain.ValorantRoles(req.ValorantRoles) | ||||||||||||||||||||||||||
| if !roles.AreValid() { | ||||||||||||||||||||||||||
| return nil, exception.ErrInvalidValorantRole | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+67
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This validation logic returns a generic
Suggested change
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Check if user has linked Discord account | ||||||||||||||||||||||||||
| _, err := s.oauth2Repository.FindDiscordAccountByUserId(userId) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
|
|
@@ -118,6 +131,12 @@ func (s *ContestApplicationService) RequestParticipate(ctx context.Context, cont | |||||||||||||||||||||||||
| PeakTier: user.GetPeakTierFullName(), | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Add valorant roles and description if provided | ||||||||||||||||||||||||||
| if req != nil { | ||||||||||||||||||||||||||
| senderSnapshot.ValorantRoles = req.ValorantRoles | ||||||||||||||||||||||||||
| senderSnapshot.Description = req.Description | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ttl := time.Until(contest.StartedAt) | ||||||||||||||||||||||||||
| if ttl < 0 { | ||||||||||||||||||||||||||
| ttl = 24 * time.Hour | ||||||||||||||||||||||||||
|
|
@@ -161,29 +180,22 @@ func (s *ContestApplicationService) AcceptApplication(ctx context.Context, conte | |||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Get the application to retrieve stored point before accepting | ||||||||||||||||||||||||||
| application, err := s.applicationRepo.GetApplication(ctx, contestId, userId) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if application == nil { | ||||||||||||||||||||||||||
| return exception.ErrApplicationNotFound | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| if application.Status != port.ApplicationStatusPending { | ||||||||||||||||||||||||||
| return exception.ErrApplicationNotPending | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // Get application data before accepting (to preserve valorant roles and description) | ||||||||||||||||||||||||||
| application, appErr := s.applicationRepo.GetApplication(ctx, contestId, userId) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| err = s.applicationRepo.AcceptRequest(ctx, contestId, userId, leaderUserId) | ||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| memberPoint := 0 | ||||||||||||||||||||||||||
| if application != nil && application.Sender != nil { | ||||||||||||||||||||||||||
| memberPoint = application.Sender.Point | ||||||||||||||||||||||||||
| member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Copy valorant roles and description from application sender snapshot | ||||||||||||||||||||||||||
| if appErr == nil && application != nil && application.Sender != nil { | ||||||||||||||||||||||||||
| member.ValorantRoles = application.Sender.ValorantRoles | ||||||||||||||||||||||||||
| member.Description = application.Sender.Description | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| member := domain.NewContestMember(userId, contestId, domain.MemberTypeNormal, domain.LeaderTypeMember, memberPoint) | ||||||||||||||||||||||||||
| if err := s.memberRepo.Save(member); err != nil { | ||||||||||||||||||||||||||
| return fmt.Errorf("[AcceptApplication] failed to save member (contestId=%d, userId=%d): %w", contestId, userId, err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,83 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package domain | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "database/sql/driver" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/FOR-GAMERS/GAMERS-BE/internal/global/exception" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ValorantRole represents a Valorant agent role type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ValorantRole string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ValorantRoleDuelist ValorantRole = "DUELIST" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ValorantRoleInitiator ValorantRole = "INITIATOR" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ValorantRoleController ValorantRole = "CONTROLLER" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ValorantRoleSentinel ValorantRole = "SENTINEL" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // IsValid checks if the ValorantRole is valid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (vr ValorantRole) IsValid() bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch vr { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case ValorantRoleDuelist, ValorantRoleInitiator, ValorantRoleController, ValorantRoleSentinel: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ValorantRoles is a custom type for storing []ValorantRole as JSON in the database | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ValorantRoles []ValorantRole | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scan implements the sql.Scanner interface for reading from DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (vr *ValorantRoles) Scan(value interface{}) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if value == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| *vr = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var bytes []byte | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch v := value.(type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case []byte: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes = v | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case string: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes = []byte(v) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("unsupported type for ValorantRoles: %T", value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return json.Unmarshal(bytes, vr) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Value implements the driver.Valuer interface for writing to DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (vr ValorantRoles) Value() (driver.Value, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if vr == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bytes, err := json.Marshal(vr) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return string(bytes), nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // AreValid checks if all roles in the slice are valid and there are no duplicates | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (vr ValorantRoles) AreValid() bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen := make(map[ValorantRole]bool) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for _, role := range vr { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if !role.IsValid() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if seen[role] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| seen[role] = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type MemberType string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,11 +103,13 @@ const ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type ContestMember struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UserID int64 `gorm:"column:user_id;primaryKey" json:"user_id"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ContestID int64 `gorm:"column:contest_id;primaryKey" json:"contest_id"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MemberType MemberType `gorm:"column:member_type;type:varchar(16);not null" json:"member_type"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LeaderType LeaderType `gorm:"column:leader_type;type:varchar(8);not null" json:"leader_type"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Point int `gorm:"column:point;type:int;default:0" json:"point"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UserID int64 `gorm:"column:user_id;primaryKey" json:"user_id"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ContestID int64 `gorm:"column:contest_id;primaryKey" json:"contest_id"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| MemberType MemberType `gorm:"column:member_type;type:varchar(16);not null" json:"member_type"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LeaderType LeaderType `gorm:"column:leader_type;type:varchar(8);not null" json:"leader_type"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Point int `gorm:"column:point;type:int;default:0" json:"point"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ValorantRoles ValorantRoles `gorm:"column:valorant_roles;type:json" json:"valorant_roles,omitempty"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Description string `gorm:"column:description;type:varchar(64)" json:"description,omitempty"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func NewContestMemberAsLeader(userID, contestID int64) *ContestMember { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -56,12 +56,13 @@ func (c *ContestApplicationController) RegisterRoute() { | |||||||||||
|
|
||||||||||||
| // RequestParticipate godoc | ||||||||||||
| // @Summary Request to participate in a contest | ||||||||||||
| // @Description Apply to participate in a contest | ||||||||||||
| // @Description Apply to participate in a contest with optional valorant roles and description | ||||||||||||
| // @Tags contest-applications | ||||||||||||
| // @Accept json | ||||||||||||
| // @Produce json | ||||||||||||
| // @Security BearerAuth | ||||||||||||
| // @Param contestId path int true "Contest ID" | ||||||||||||
| // @Param request body dto.RequestParticipateRequest false "Participation request with optional valorant roles and description" | ||||||||||||
| // @Success 201 {object} response.Response | ||||||||||||
| // @Failure 400 {object} response.Response | ||||||||||||
| // @Failure 401 {object} response.Response | ||||||||||||
|
|
@@ -81,7 +82,12 @@ func (c *ContestApplicationController) RequestParticipate(ctx *gin.Context) { | |||||||||||
| return | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| discordLinkRequired, err := c.service.RequestParticipate(ctx.Request.Context(), contestId, userId) | ||||||||||||
| // Parse optional request body | ||||||||||||
| var req dto.RequestParticipateRequest | ||||||||||||
| // ShouldBindJSON returns error if body exists but is invalid; nil body is OK | ||||||||||||
| _ = ctx.ShouldBindJSON(&req) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error returned by
Suggested change
|
||||||||||||
|
|
||||||||||||
| discordLinkRequired, err := c.service.RequestParticipate(ctx.Request.Context(), contestId, userId, &req) | ||||||||||||
|
|
||||||||||||
| // Handle Discord link required error | ||||||||||||
| if errors.Is(err, exception.ErrDiscordLinkRequired) { | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,5 +38,7 @@ var ( | |||||||||||||
| ErrContestNotActive = NewBadRequestError("contest is not in active status", "CT032") | ||||||||||||||
| ErrCannotChangeLeaderRole = NewBusinessError(http.StatusForbidden, "cannot change leader's role", "CT033") | ||||||||||||||
| ErrAlreadySameMemberType = NewBadRequestError("member already has the same role", "CT034") | ||||||||||||||
| ErrContestStartTimePassed = NewBadRequestError("contest start time has already passed", "CT035") | ||||||||||||||
| ErrInvalidValorantRole = NewBadRequestError("invalid valorant role", "CT035") | ||||||||||||||
| ErrDescriptionTooLong = NewBadRequestError("description must be at most 64 characters", "CT036") | ||||||||||||||
| ErrDuplicateValorantRole = NewBadRequestError("duplicate valorant role", "CT037") | ||||||||||||||
|
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is an inconsistency in the error codes between this file and the PR description. The description states
Suggested change
|
||||||||||||||
| ) | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value
64is a magic number representing the maximum length of the description. This value is also hardcoded in the database migration (VARCHAR(64)). To improve maintainability and ensure consistency, it's best practice to define this as a constant (e.g.,domain.MaxDescriptionLength) and reuse it across the application.