diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java index e3166dc..ffc23d4 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java @@ -1,7 +1,10 @@ package com.whereyouad.WhereYouAd.domains.organization.application.dto.request; +import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public class OrgRequest { @@ -23,6 +26,11 @@ public record Update ( String logoUrl ) {} + public record UpdateRole ( + @Schema(description = "조직 내 역할(ADMIN / MEMBER)", example = "ADMIN", allowableValues = {"ADMIN", "MEMBER"}) + @NotNull(message = "역할은 필수입니다.") + OrgRole orgRole + ) {} public record Invite( @NotBlank(message = "이메일은 필수입니다.") @Email(message = "이메일 형식이 올바르지 않습니다.") diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java index 4b47751..88c918b 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java @@ -66,6 +66,16 @@ public static Organization toOrganization(Long userId, OrgRequest.Create request .build(); } + // 단일 OrgMember -> OrgMemberDTO 변환 + public static OrgResponse.OrgMemberDTO toOrgMemberDTO(OrgMember orgMember) { + return new OrgResponse.OrgMemberDTO( + orgMember.getUser().getName(), + orgMember.getUser().getEmail(), + orgMember.getUser().getProfileImageUrl(), + orgMember.getRole().name() + ); + } + // 조직 멤버 Slice DTO 변환 (무한 스크롤) public static OrgResponse.OrgMemberSliceDTO toOrgMemberSliceDTO( boolean hasNext, @@ -73,12 +83,7 @@ public static OrgResponse.OrgMemberSliceDTO toOrgMemberSliceDTO( List orgMembers ) { List memberDTOs = orgMembers.stream() - .map(m -> new OrgResponse.OrgMemberDTO( - m.getUser().getName(), - m.getUser().getEmail(), - m.getUser().getProfileImageUrl(), - m.getRole().name() - )) + .map(OrgConverter::toOrgMemberDTO) .toList(); return new OrgResponse.OrgMemberSliceDTO( @@ -87,4 +92,4 @@ public static OrgResponse.OrgMemberSliceDTO toOrgMemberSliceDTO( memberDTOs ); } -} +} \ No newline at end of file diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java index 504257f..3f290fb 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java @@ -22,6 +22,9 @@ public interface OrgService { // orgId 조직에서 memberId에 해당하는 맴버 제거 void removeMemberFromOrg(Long userId, Long orgId, Long memberId); + // 조직 내 멤버 권한 변경 메서드 + OrgResponse.OrgMemberDTO updateOrgMembersRole(Long userId, Long orgId, Long memberId, OrgRequest.UpdateRole dto); + OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email); OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token); diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java index ba4c3db..b211711 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java @@ -197,6 +197,47 @@ public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) { orgMemberRepository.delete(targetMember); } + public OrgResponse.OrgMemberDTO updateOrgMembersRole(Long userId, Long orgId, Long memberId, + OrgRequest.UpdateRole dto) { + + // 0. 본인 권한 변경 불가 + if (Objects.equals(userId, memberId)) { + throw new OrgHandler(OrgErrorCode.ORG_CANNOT_ROLE_CHANGE_SELF); + } + + // 1. 조직 존재 여부 확인 + Organization organization = orgRepository.findById(orgId) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); + + // 2. 요청자가 해당 조직의 ADMIN인지 확인 + OrgMember requester = orgMemberRepository.findByUserIdAndOrgId(userId, orgId) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND)); + + if (requester.getRole() != OrgRole.ADMIN) { + throw new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN); + } + + // 3. 권한 변경 대상 멤버 조회 + OrgMember orgMember = orgMemberRepository.findByUserIdAndOrgId(memberId, orgId) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND)); + + // 4. ADMIN -> MEMBER 강등 시: 조직 내 ADMIN이 2명 이상이어야만 허용 + // 해당 멤버 ADMIN, 요청 역할 MEMBER인 경우 + boolean isDemoting = orgMember.getRole() == OrgRole.ADMIN && dto.orgRole() == OrgRole.MEMBER; + if (isDemoting) { + long adminCount = orgMemberRepository.countByOrganizationIdAndRole(orgId, OrgRole.ADMIN); + if (adminCount < 2) { + throw new OrgHandler(OrgErrorCode.ORG_LAST_ADMIN); + } + } + + // 역할 변경 (더티체킹) + orgMember.updateRole(dto.orgRole()); + + // 변경된 멤버 정보를 DTO 로 반환 + return OrgConverter.toOrgMemberDTO(orgMember); + } + @Override // 조직 초대 이메일 보내기 public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email) { diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java index c08f747..f9d4078 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java @@ -12,10 +12,13 @@ public enum OrgErrorCode implements BaseErrorCode { ORG_NAME_DUPLICATE(HttpStatus.BAD_REQUEST, "ORG_400_1", "사용자가 이미 속해있는 조직의 이름입니다."), ORG_CANNOT_KICK_SELF(HttpStatus.BAD_REQUEST, "ORG_400_2", "자기 자신을 추방할 수 없습니다."), ORG_CANNOT_KICK_ADMIN(HttpStatus.BAD_REQUEST, "ORG_400_3", "ADMIN은 추방할 수 없습니다."), + ORG_CANNOT_ROLE_CHANGE_SELF(HttpStatus.BAD_REQUEST, "ORG_400_4", "본인의 역할은 변경할 수 없습니다."), + ORG_LAST_ADMIN(HttpStatus.BAD_REQUEST, "ORG_400_5", "마지막 ADMIN은 강등할 수 없습니다."), // 403 ORG_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_1", "해당 요청은 조직 생성자만 요청 가능합니다."), ORG_MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_2", "해당 요청은 ADMIN 권한을 가진 멤버만 요청 가능합니다."), + ORG_INVITATION_FORBIDDEN_USER(HttpStatus.FORBIDDEN, "ORG_INVITATION_403_1", "초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하지 않습니다."), // 404 ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."), @@ -23,18 +26,11 @@ public enum OrgErrorCode implements BaseErrorCode { //409 ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), + ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."), //410 ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"), - - // 409 - ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."), - - // 400 ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400", "조직 초대 토큰이 만료되었거나 유효하지 않습니다."), - - // 403 - ORG_INVITATION_FORBIDDEN_USER(HttpStatus.FORBIDDEN, "ORG_INVITATION_403_1", "초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하지 않습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgMember.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgMember.java index cee7b65..b023a18 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgMember.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgMember.java @@ -34,4 +34,8 @@ public class OrgMember { //중간 테이블이므로 BaseEntity 미적용 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "org_id") private Organization organization; + + public void updateRole(OrgRole role) { + this.role = role; + } } diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java index 4197524..6cad323 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java @@ -1,5 +1,6 @@ package com.whereyouad.WhereYouAd.domains.organization.persistence.repository; +import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole; import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember; import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization; import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus; @@ -17,14 +18,7 @@ public interface OrgMemberRepository extends JpaRepository { //User 가 가진 OrgMember 모두 추출하는 메서드 List findOrgMemberByUser(User user); - - // userId 와 orgId 로 특정 OrgMember 조회 - @Query("SELECT om FROM OrgMember om " + - "WHERE om.user.id = :userId " + - "AND om.organization.id = :orgId " + - "AND om.user.status = 'ACTIVE'") - Optional findByUserIdAndOrgId(@Param("userId") Long userId, @Param("orgId") Long orgId); - + //userId 를 통해 OrgMember 추출 -> Organization 의 status 가 ACTIVE 인 경우에만 조회 @Query(value = "select om from OrgMember om join fetch om.organization o where om.user.id = :userId and o.status = 'ACTIVE'") List findOrgMemberByUserId(@Param("userId") Long userId); @@ -47,6 +41,13 @@ Slice findByOrganizationIdWithCursor( Pageable pageable ); + // userId 와 orgId 로 특정 OrgMember 조회 + @Query("SELECT om FROM OrgMember om " + + "WHERE om.user.id = :userId " + + "AND om.organization.id = :orgId " + + "AND om.user.status = 'ACTIVE'") + Optional findByUserIdAndOrgId(@Param("userId") Long userId, @Param("orgId") Long orgId); + // 조직의 전체 멤버 수 조회 @Query("SELECT COUNT(m) FROM OrgMember m " + "JOIN m.user u " + @@ -58,4 +59,7 @@ int countByOrganizationIdAndUserStatus( ); Boolean existsByUserAndOrganization(User user, Organization organization); + + // 조직 id에 해당하는 역할 인원 수 조회 + long countByOrganizationIdAndRole(Long orgId, OrgRole orgRole); } \ No newline at end of file diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java index 7072404..70488d8 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java @@ -53,6 +53,7 @@ public ResponseEntity> getOrganizationDetail ); } + @PatchMapping("/{orgId}") public ResponseEntity> modifyOrganization( @AuthenticationPrincipal(expression = "userId") Long userId, @@ -125,6 +126,17 @@ public ResponseEntity> removeMember( return ResponseEntity.ok(DataResponse.from("해당 맴버가 조직에서 제외되었습니다.")); } + @PatchMapping("/members/{orgId}/{memberId}") + public ResponseEntity> updateOrgMembersRole( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable Long orgId, + @PathVariable Long memberId, + @RequestBody @Valid OrgRequest.UpdateRole dto + ) { + OrgResponse.OrgMemberDTO response = orgService.updateOrgMembersRole(userId, orgId, memberId, dto); + return ResponseEntity.ok(DataResponse.from(response)); + } + @PostMapping("/members/{orgId}/invitation") public ResponseEntity> sendOrgInvitation( @AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId, diff --git a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java index ac356c4..0544e50 100644 --- a/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java +++ b/src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java @@ -135,6 +135,23 @@ public ResponseEntity> removeMember( @PathVariable Long memberId ); + @Operation( + summary = "조직 맴버 권한 변경 API", + description = "맴버 권한 변경을 요청한 유저의 권한이 ADMIN인 경우 실행이 가능합니다. memberId에 해당하는 맴버의 권한을 변경시킵니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공 (totalCount: 전체 멤버 수)"), + @ApiResponse(responseCode = "401", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"), + @ApiResponse(responseCode = "403", description = "권한이 부족한 경우(요청을 보낸 유저의 권한이 ADMIN이 아닌 경우)"), + @ApiResponse(responseCode = "404", description = "해당 id의 데이터 존재 X") + }) + ResponseEntity> updateOrgMembersRole( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable Long orgId, + @PathVariable Long memberId, + @RequestBody OrgRequest.UpdateRole dto + ); + @Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"),