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 0a82d83..0ae762f 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 @@ -18,4 +18,7 @@ public interface OrgService { void removeOrganizationSoft(Long userId, Long orgId); OrgResponse.Delete restoreOrganization(Long userId, Long orgId); + + // orgId 조직에서 memberId에 해당하는 맴버 제거 + void removeMemberFromOrg(Long userId, Long orgId, Long memberId); } 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 c152534..7033239 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 @@ -4,6 +4,7 @@ import com.whereyouad.WhereYouAd.domains.organization.application.dto.response.OrgResponse; import com.whereyouad.WhereYouAd.domains.organization.application.mapper.OrgConverter; import com.whereyouad.WhereYouAd.domains.organization.application.mapper.OrgMemberConverter; +import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole; import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgStatus; import com.whereyouad.WhereYouAd.domains.organization.exception.code.OrgErrorCode; import com.whereyouad.WhereYouAd.domains.organization.exception.handler.OrgHandler; @@ -20,37 +21,38 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.Objects; @Service @Transactional @RequiredArgsConstructor -public class OrgServiceImpl implements OrgService{ +public class OrgServiceImpl implements OrgService { private final OrgRepository orgRepository; private final OrgMemberRepository orgMemberRepository; private final UserRepository userRepository; - //조직(워크스페이스) 생성 메서드 + // 조직(워크스페이스) 생성 메서드 public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request) { - //유저 정보 추출 + // 유저 정보 추출 User user = userRepository.findById(userId) .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND)); - //만약 해당 User 가 이미 같은 name 을 가진 Organization 에 속해있으면 예외처리 - //해당 User 의 OrgMember 를 모두 추출해서, + // 만약 해당 User 가 이미 같은 name 을 가진 Organization 에 속해있으면 예외처리 + // 해당 User 의 OrgMember 를 모두 추출해서, List orgMemberByUser = orgMemberRepository.findOrgMemberByUser(user); for (OrgMember orgMember : orgMemberByUser) { - //OrgMember 내부 Organization 의 name 이 생성하려는 request 의 name 과 같으면 + // OrgMember 내부 Organization 의 name 이 생성하려는 request 의 name 과 같으면 if (orgMember.getOrganization().getName().equals(request.name())) { - throw new OrgHandler(OrgErrorCode.ORG_NAME_DUPLICATE); //예외처리 + throw new OrgHandler(OrgErrorCode.ORG_NAME_DUPLICATE); // 예외처리 } } - //조직 생성 + // 조직 생성 Organization organization = OrgConverter.toOrganization(userId, request); - //OrgMember 생성 + // OrgMember 생성 OrgMember orgMember = OrgMemberConverter.toOrgMemberADMIN(user, organization); orgRepository.save(organization); @@ -88,20 +90,20 @@ public OrgResponse.OrgDetail getOrganizationDetail(Long orgId) { return OrgConverter.toOrgDetail(organization); } - //조직 정보 수정 메서드 + // 조직 정보 수정 메서드 public OrgResponse.Update modifyOrganization(Long userId, Long orgId, OrgRequest.Update request) { Organization organization = orgRepository.findById(orgId) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - //만약 조직 정보 수정을 요청한 회원이 해당 조직을 생성한 회원이 아니라면, + // 만약 조직 정보 수정을 요청한 회원이 해당 조직을 생성한 회원이 아니라면, if (!organization.getOwnerUserId().equals(userId)) { - throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 + throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 } - //조직 정보 수정 + // 조직 정보 수정 organization.modifyInfo(request); - //변환 된 필드값과 해당 조직의 Id, updatedAt 가 포함된 DTO 로 반환 + // 변환 된 필드값과 해당 조직의 Id, updatedAt 가 포함된 DTO 로 반환 return OrgConverter.toUpdatedResponse(organization); } @@ -109,51 +111,83 @@ public OrgResponse.Delete restoreOrganization(Long userId, Long orgId) { Organization organization = orgRepository.findById(orgId) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - //만약 조직 복구 요청한 회원이 해당 조직을 생성한 회원이 아니라면, + // 만약 조직 복구 요청한 회원이 해당 조직을 생성한 회원이 아니라면, if (!organization.getOwnerUserId().equals(userId)) { - throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 + throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 } - //조직이 이미 활성화 상태라면, + // 조직이 이미 활성화 상태라면, if (organization.getStatus() == OrgStatus.ACTIVE) { - throw new OrgHandler(OrgErrorCode.ORG_ALREADY_ACTIVE); //예외처리 + throw new OrgHandler(OrgErrorCode.ORG_ALREADY_ACTIVE); // 예외처리 } - organization.restoreDelete(); //조직 Soft Delete 복구 + organization.restoreDelete(); // 조직 Soft Delete 복구 return OrgConverter.toRestoredResponse(organization); } - //조직 삭제 메서드 -> Hard Delete (DB 에서 완전히 제거) + // 조직 삭제 메서드 -> Hard Delete (DB 에서 완전히 제거) public void removeOrganization(Long userId, Long orgId) { Organization organization = orgRepository.findById(orgId) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - //만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, + // 만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, if (!organization.getOwnerUserId().equals(userId)) { - throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); //예외처리 + throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); // 예외처리 } - //해당 조직에 가입된 모든 회원들의 가입 정보 삭제 + // 해당 조직에 가입된 모든 회원들의 가입 정보 삭제 List orgMembers = orgMemberRepository.findOrgMemberByOrg(organization); orgMemberRepository.deleteAll(orgMembers); - //조직 실제 삭제 + // 조직 실제 삭제 orgRepository.delete(organization); } - //조직 삭제 메서드 -> Soft Delete (status 만 DELETED 로 변경) + // 조직 삭제 메서드 -> Soft Delete (status 만 DELETED 로 변경) public void removeOrganizationSoft(Long userId, Long orgId) { Organization organization = orgRepository.findById(orgId) .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND)); - //만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, + // 만약 조직 삭제 요청한 회원이 해당 조직을 생성한 회원이 아니라면, if (!organization.getOwnerUserId().equals(userId)) { throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN); } - //조직 status 만 DELETED 로 변경 후 종료 + // 조직 status 만 DELETED 로 변경 후 종료 organization.softDelete(); } + + public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) { + + // 0. 본인은 삭제 불가 + if(Objects.equals(userId, memberId)){ + throw new OrgHandler(OrgErrorCode.ORG_CANNOT_KICK_SELF); + } + + // 1. 조직 존재 여부 확인 + 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 targetMember = orgMemberRepository.findByUserIdAndOrgId(memberId, orgId) + .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND)); + + // 4. 대상 맴버가 ADMIN이라면 추방 불가 + if (targetMember.getRole() == OrgRole.ADMIN) { + throw new OrgHandler(OrgErrorCode.ORG_CANNOT_KICK_ADMIN); + } + + // 5. 중간 테이블에서 해당 멤버 삭제 + orgMemberRepository.delete(targetMember); + } } 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 af64560..fca280f 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 @@ -8,14 +8,18 @@ @Getter @AllArgsConstructor public enum OrgErrorCode implements BaseErrorCode { - //400 + // 400 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은 추방할 수 없습니다."), - //403 + // 403 ORG_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_1", "해당 요청은 조직 생성자만 요청 가능합니다."), + ORG_MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_2", "해당 요청은 ADMIN 권한을 가진 멤버만 요청 가능합니다."), - //404 + // 404 ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."), + ORG_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_2", "해당 멤버가 조직에 존재하지 않습니다."), //409 ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), @@ -24,6 +28,7 @@ public enum OrgErrorCode implements BaseErrorCode { //410 ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"), ; + private final HttpStatus httpStatus; private final String code; private final String message; 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 a93ea9e..c3ac8f5 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 @@ -11,12 +11,20 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; 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); 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 89c940a..77b8ded 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 @@ -115,4 +115,14 @@ public ResponseEntity> getOrgMembers OrgResponse.OrgMemberCountDTO response = orgQueryService.getOrgMembersCount(orgId); return ResponseEntity.ok(DataResponse.from(response)); } + + @DeleteMapping("{orgId}/members/{memberId}") + public ResponseEntity> removeMember( + @AuthenticationPrincipal(expression = "userId") Long userId, // 관리자 ID + @PathVariable Long orgId, + @PathVariable Long memberId + ) { + orgService.removeMemberFromOrg(userId, orgId, memberId); + return ResponseEntity.ok(DataResponse.from("해당 맴버가 조직에서 제외되었습니다.")); + } } \ No newline at end of file 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 72c09a5..f6269a5 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 @@ -119,4 +119,19 @@ ResponseEntity> getOrgMembers( ResponseEntity> getOrgMembersCount( @PathVariable Long orgId ); + + @Operation( + summary = "조직 맴버 삭제 API", + description = "맴버 삭제를 요청한 유저의 권한이 ADMIN인 경우 실행이 가능합니다. memberId에 해당하는 맴버를 조직에서 제외시킵니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "403", description = "권한이 부족한 경우(요청을 보낸 유저의 권한이 ADMIN이 아닌 경우)"), + @ApiResponse(responseCode = "404", description = "해당 id의 데이터 존재 X") + }) + public ResponseEntity> removeMember( + @AuthenticationPrincipal(expression = "userId") Long userId, + @PathVariable Long orgId, + @PathVariable Long memberId + ); } \ No newline at end of file