From 687aed98c006ea51c7c3e6000451ba8f10108c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EA=B7=9C?= Date: Wed, 18 Feb 2026 22:05:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A1=B0=EC=A7=81=20?= =?UTF-8?q?=EB=A7=B4=EB=B2=84=20=EC=A0=9C=EA=B1=B0=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organization/exception/code/OrgErrorCode.java | 15 ++++++++++----- .../repository/OrgMemberRepository.java | 8 ++++++++ .../organization/presentation/OrgController.java | 10 ++++++++++ 3 files changed, 28 insertions(+), 5 deletions(-) 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 1204834..9044825 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,18 +8,23 @@ @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", "해당 조직은 이미 활성화 상태 입니다.") + // 409 + ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."), ; + 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 5324f89..9b74375 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); + //특정 Organization 에 속한 OrgMember 모두 추출하는 메서드 @Query("select om from OrgMember om where om.organization = :organization") List findOrgMemberByOrg(@Param(value = "organization") Organization organization); 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 eeeec47..7987e1e 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 @@ -105,4 +105,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 From e53b39334790d483ff99db2dc69c9dbb73d65fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EA=B7=9C?= Date: Wed, 18 Feb 2026 22:05:41 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20feat:=20=EC=A1=B0=EC=A7=81=20?= =?UTF-8?q?=EB=A7=B4=EB=B2=84=20=EC=A0=9C=EA=B1=B0=20API=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/service/OrgService.java | 3 + .../domain/service/OrgServiceImpl.java | 90 +++++++++++++------ 2 files changed, 65 insertions(+), 28 deletions(-) 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 052c1d1..194f2d1 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 @@ -16,4 +16,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 e6db79b..a730cdc 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); @@ -60,24 +62,24 @@ public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create requ } public OrgResponse.Read getOrganization(Long userId) { - //TODO + // TODO return null; } - //조직 정보 수정 메서드 + // 조직 정보 수정 메서드 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); } @@ -85,51 +87,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_MEMBER_FORBIDDEN); + } + + // 5. 중간 테이블에서 해당 멤버 삭제 + orgMemberRepository.delete(targetMember); + } } From 726bccc77e85ecd800b0690656f551ee095a9df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EA=B7=9C?= Date: Wed, 18 Feb 2026 22:06:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9D=20docs:=20OrgControllerDocs=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/docs/OrgControllerDocs.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 1e7fca9..4eab62f 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 @@ -96,4 +96,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 From 7282ff7e82f2fb3d79fc8a8675e6f44c34441019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=AF=BC=EA=B7=9C?= Date: Wed, 18 Feb 2026 22:25:02 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20bug:=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?ADMIN=EC=9D=80=20=EC=82=AD=EC=A0=9C=20=EB=B6=88=EA=B0=80=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=20=EC=9E=88=EB=8D=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domains/organization/domain/service/OrgServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a730cdc..8a42b6b 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 @@ -159,8 +159,8 @@ public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) { .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND)); // 4. 대상 맴버가 ADMIN이라면 추방 불가 - if (targetMember.getRole() != OrgRole.ADMIN) { - throw new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN); + if (targetMember.getRole() == OrgRole.ADMIN) { + throw new OrgHandler(OrgErrorCode.ORG_CANNOT_KICK_ADMIN); } // 5. 중간 테이블에서 해당 멤버 삭제