diff --git a/src/main/java/com/shinhan/klljs/domain/team/controller/TeamController.java b/src/main/java/com/shinhan/klljs/domain/team/controller/TeamController.java index 2d4ad3b..ba7c165 100644 --- a/src/main/java/com/shinhan/klljs/domain/team/controller/TeamController.java +++ b/src/main/java/com/shinhan/klljs/domain/team/controller/TeamController.java @@ -2,27 +2,33 @@ import com.shinhan.klljs.domain.team.dto.TeamCreateRequest; import com.shinhan.klljs.domain.team.dto.TeamCreateResponse; +import com.shinhan.klljs.domain.team.dto.TeamRenameRequest; +import com.shinhan.klljs.domain.team.dto.TeamRenameResponse; import com.shinhan.klljs.domain.team.service.TeamCreateService; +import com.shinhan.klljs.domain.team.service.TeamMemberManagementService; import com.shinhan.klljs.global.apiPayload.ApiResponse; import com.shinhan.klljs.global.apiPayload.code.GeneralSuccessCode; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** - * 팀 생성 API (docs/team-creation-api-spec.md 5절). + * 팀 생성·수정 API (docs/team-creation-api-spec.md 5절). * - * 아직 팀이 없는 사용자가 호출하므로 "인증된 사용자인가"만 확인한다 - 팀 소속 검사가 없다. + * 팀 생성은 아직 팀이 없는 사용자가 호출하므로 "인증된 사용자인가"만 확인한다 - 팀 소속 검사가 없다. */ @RestController @RequiredArgsConstructor public class TeamController implements TeamControllerDocs { private final TeamCreateService teamCreateService; + private final TeamMemberManagementService teamMemberManagementService; @Override @PostMapping("/api/v1/teams") @@ -33,4 +39,16 @@ public ApiResponse createTeam( Long userId = Long.valueOf(jwt.getSubject()); return ApiResponse.onSuccess(GeneralSuccessCode.OK, teamCreateService.create(userId, request)); } + + @Override + @PatchMapping("/api/v1/teams/{teamId}") + public ApiResponse renameTeam( + @AuthenticationPrincipal Jwt jwt, + @PathVariable Long teamId, + @Valid @RequestBody TeamRenameRequest request + ) { + Long userId = Long.valueOf(jwt.getSubject()); + TeamRenameResponse response = teamMemberManagementService.renameTeam(userId, teamId, request.teamName()); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, response); + } } diff --git a/src/main/java/com/shinhan/klljs/domain/team/controller/TeamControllerDocs.java b/src/main/java/com/shinhan/klljs/domain/team/controller/TeamControllerDocs.java index 3f17081..a2011cf 100644 --- a/src/main/java/com/shinhan/klljs/domain/team/controller/TeamControllerDocs.java +++ b/src/main/java/com/shinhan/klljs/domain/team/controller/TeamControllerDocs.java @@ -2,6 +2,8 @@ import com.shinhan.klljs.domain.team.dto.TeamCreateRequest; import com.shinhan.klljs.domain.team.dto.TeamCreateResponse; +import com.shinhan.klljs.domain.team.dto.TeamRenameRequest; +import com.shinhan.klljs.domain.team.dto.TeamRenameResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -14,7 +16,7 @@ /** * {@link TeamController}의 Swagger 문서 전용 인터페이스. */ -@Tag(name = "팀", description = "팀 생성 API") +@Tag(name = "팀", description = "팀 생성·수정 API") public interface TeamControllerDocs { @Operation( @@ -70,4 +72,40 @@ com.shinhan.klljs.global.apiPayload.ApiResponse createTeam( @Parameter(hidden = true) Jwt jwt, TeamCreateRequest request ); + + @Operation( + summary = "팀명 수정", + description = """ + 팀명만 바꾼다. 팀의 `OWNER`/`ADMIN`만 호출할 수 있고, `MEMBER`는 403이다. + """ + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "수정 성공", + content = @Content(mediaType = "application/json", examples = @ExampleObject(value = """ + { + "isSuccess": true, + "code": "COMMON_200_001", + "message": "성공적으로 요청을 처리했습니다.", + "result": { + "teamId": 12, + "teamName": "신한 KLLJS 딥비전스 옥외 광고 3팀" + } + } + """)) + ), + @ApiResponse(responseCode = "400", description = "`COMMON_400_002`: 팀명 누락 또는 200자 초과"), + @ApiResponse(responseCode = "403", description = """ + - `TEAM_403_001`: 요청자가 이 팀 소속이 아님 + - `TEAM_403_004`: 팀 소속이지만 `MEMBER`라 팀 설정을 바꿀 권한이 없음"""), + @ApiResponse(responseCode = "404", description = "`TEAM_404_001`: 팀이 없음"), + @ApiResponse(responseCode = "409", description = "`TEAM_409_001`: 팀이 `ACTIVE` 상태가 아님") + }) + com.shinhan.klljs.global.apiPayload.ApiResponse renameTeam( + @Parameter(hidden = true) Jwt jwt, + @Parameter(description = "팀 ID", example = "12") + Long teamId, + TeamRenameRequest request + ); } diff --git a/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameRequest.java b/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameRequest.java new file mode 100644 index 0000000..87cf2e2 --- /dev/null +++ b/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameRequest.java @@ -0,0 +1,11 @@ +package com.shinhan.klljs.domain.team.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record TeamRenameRequest( + @NotBlank(message = "팀명을 입력해 주세요.") + @Size(max = 200, message = "팀명은 200자를 넘을 수 없습니다.") + String teamName +) { +} diff --git a/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameResponse.java b/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameResponse.java new file mode 100644 index 0000000..9508738 --- /dev/null +++ b/src/main/java/com/shinhan/klljs/domain/team/dto/TeamRenameResponse.java @@ -0,0 +1,13 @@ +package com.shinhan.klljs.domain.team.dto; + +import com.shinhan.klljs.domain.team.entity.Team; + +public record TeamRenameResponse( + Long teamId, + String teamName +) { + + public static TeamRenameResponse of(Team team) { + return new TeamRenameResponse(team.getId(), team.getTeamName()); + } +} diff --git a/src/main/java/com/shinhan/klljs/domain/team/exception/TeamErrorCode.java b/src/main/java/com/shinhan/klljs/domain/team/exception/TeamErrorCode.java index c94bff7..026e77c 100644 --- a/src/main/java/com/shinhan/klljs/domain/team/exception/TeamErrorCode.java +++ b/src/main/java/com/shinhan/klljs/domain/team/exception/TeamErrorCode.java @@ -18,6 +18,7 @@ public enum TeamErrorCode implements BaseErrorCode { "TEAM_403_003", "캠페인을 등록하거나 삭제할 권한이 없습니다." ), + TEAM_SETTINGS_FORBIDDEN(HttpStatus.FORBIDDEN, "TEAM_403_004", "팀 설정을 변경할 권한이 없습니다."), TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "TEAM_404_001", "팀을 찾을 수 없습니다."), TEAM_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "TEAM_404_002", "활성 팀원을 찾을 수 없습니다."), diff --git a/src/main/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementService.java b/src/main/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementService.java index 64d008e..25b8b4e 100644 --- a/src/main/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementService.java +++ b/src/main/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementService.java @@ -3,6 +3,7 @@ import com.shinhan.klljs.domain.team.dto.TeamMemberListResponse; import com.shinhan.klljs.domain.team.dto.TeamMemberRoleChangeResponse; import com.shinhan.klljs.domain.team.dto.TeamMemberSummary; +import com.shinhan.klljs.domain.team.dto.TeamRenameResponse; import com.shinhan.klljs.domain.team.dto.UpdatedTeamMember; import com.shinhan.klljs.domain.team.entity.Team; import com.shinhan.klljs.domain.team.entity.TeamMember; @@ -60,6 +61,21 @@ public TeamMemberListResponse getMembers(Long requesterId, Long teamId, String k return new TeamMemberListResponse(team.getId(), team.getTeamName(), summaries); } + /** OWNER/ADMIN만 팀명을 바꿀 수 있다 - MEMBER는 조회만 가능하고 팀 설정은 못 건드린다. */ + @Transactional + public TeamRenameResponse renameTeam(Long requesterId, Long teamId, String teamName) { + Team team = lockTeam(teamId); + TeamMember requester = requireActiveMember(requesterId, teamId); + requireActiveTeam(team); + + if (requester.getRole() == TeamMemberRole.MEMBER) { + throw new GeneralException(TeamErrorCode.TEAM_SETTINGS_FORBIDDEN); + } + + team.rename(teamName); + return TeamRenameResponse.of(team); + } + @Transactional public TeamMemberRoleChangeResponse changeRole( Long requesterId, diff --git a/src/test/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementServiceTest.java b/src/test/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementServiceTest.java index 439dbd3..5273072 100644 --- a/src/test/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementServiceTest.java +++ b/src/test/java/com/shinhan/klljs/domain/team/service/TeamMemberManagementServiceTest.java @@ -2,6 +2,7 @@ import com.shinhan.klljs.domain.team.dto.TeamMemberListResponse; import com.shinhan.klljs.domain.team.dto.TeamMemberRoleChangeResponse; +import com.shinhan.klljs.domain.team.dto.TeamRenameResponse; import com.shinhan.klljs.domain.team.entity.Team; import com.shinhan.klljs.domain.team.entity.TeamMember; import com.shinhan.klljs.domain.team.entity.TeamMemberRole; @@ -96,6 +97,31 @@ void changeRole_returnsCurrentRoleForIdempotentRequest() { assertThat(response.updatedMembers().getFirst().role()).isEqualTo(TeamMemberRole.ADMIN); } + @Test + void renameTeam_allowsOwnerAndAdminButRejectsMember() { + Team team = persistTeam(TeamStatus.ACTIVE); + TeamMember owner = persistMember(team, "오너", "owner@example.com", TeamMemberRole.OWNER, TeamMemberStatus.ACTIVE); + TeamMember admin = persistMember(team, "관리자", "admin@example.com", TeamMemberRole.ADMIN, TeamMemberStatus.ACTIVE); + TeamMember member = persistMember(team, "멤버", "member@example.com", TeamMemberRole.MEMBER, TeamMemberStatus.ACTIVE); + entityManager.flush(); + + TeamRenameResponse response = service.renameTeam(owner.getUser().getId(), team.getId(), "새 팀명"); + entityManager.flush(); + + assertThat(response.teamId()).isEqualTo(team.getId()); + assertThat(response.teamName()).isEqualTo("새 팀명"); + assertThat(team.getTeamName()).isEqualTo("새 팀명"); + + service.renameTeam(admin.getUser().getId(), team.getId(), "관리자가 바꾼 팀명"); + assertThat(team.getTeamName()).isEqualTo("관리자가 바꾼 팀명"); + + assertTeamError( + () -> service.renameTeam(member.getUser().getId(), team.getId(), "멤버가 시도한 팀명"), + TeamErrorCode.TEAM_SETTINGS_FORBIDDEN + ); + assertThat(team.getTeamName()).isEqualTo("관리자가 바꾼 팀명"); + } + @Test void removeAndLeave_changeMembershipStatuses() { Team team = persistTeam(TeamStatus.ACTIVE); @@ -155,6 +181,10 @@ void mutationsRejectInactiveTeam() { () -> service.removeMember(owner.getUser().getId(), team.getId(), target.getUser().getId()), TeamErrorCode.TEAM_NOT_ACTIVE ); + assertTeamError( + () -> service.renameTeam(owner.getUser().getId(), team.getId(), "정지된 팀명"), + TeamErrorCode.TEAM_NOT_ACTIVE + ); } private Team persistTeam(TeamStatus status) {