-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#37 조직 맴버 관리 - 조직 맴버 삭제 #41
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
687aed9
e53b393
726bccc
7282ff7
2312710
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 |
|---|---|---|
|
|
@@ -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<OrgMember> 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,72 +90,104 @@ 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); | ||
| } | ||
|
|
||
| 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<OrgMember> 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); | ||
| } | ||
|
Comment on lines
+174
to
+179
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. 요청자가 조직에 없을 때 현재 로직은:
두 경우 모두 "권한 없음" 상황인데, 전자는 404, 후자는 403이 반환됩니다. 조직에 없는 외부인이 멤버 추방을 시도할 때 "해당 멤버가 조직에 존재하지 않습니다"(404)를 받으면 조직 멤버십 여부가 노출되는 정보 노출 문제도 있습니다. 두 경우 모두 🛠️ 수정 제안- OrgMember requester = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
- .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));
+ OrgMember requester = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
+ .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN));
if (requester.getRole() != OrgRole.ADMIN) {
throw new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN);
}🤖 Prompt for AI Agents |
||
|
|
||
| // 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); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,12 +11,20 @@ | |
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface OrgMemberRepository extends JpaRepository<OrgMember, Long> { | ||
|
|
||
| //User 가 가진 OrgMember 모두 추출하는 메서드 | ||
| List<OrgMember> 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<OrgMember> findByUserIdAndOrgId(@Param("userId") Long userId, @Param("orgId") Long orgId); | ||
|
Comment on lines
+22
to
+26
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. 🧩 Analysis chain🏁 Script executed: # OrgMemberRepository 전체 내용 조회
find . -type f -name "OrgMemberRepository.java" | head -1 | xargs cat -nRepository: WhereYouAd/WhereYouAd-Backend Length of output: 2743 🏁 Script executed: # User 엔티티 찾기 및 status 필드 확인
find . -type f -name "User.java" | grep -E "domains|entity" | head -3Repository: WhereYouAd/WhereYouAd-Backend Length of output: 156 🏁 Script executed: # UserStatus enum 찾기
find . -type f -name "UserStatus.java" | head -1Repository: WhereYouAd/WhereYouAd-Backend Length of output: 159 🏁 Script executed: # User.java에서 status 필드 및 `@Enumerated` 설정 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/user/persistence/entity/User.java | grep -A 5 -B 5 "status"Repository: WhereYouAd/WhereYouAd-Backend Length of output: 610 🏁 Script executed: # UserStatus enum 전체 확인
cat -n ./src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/constant/UserStatus.javaRepository: WhereYouAd/WhereYouAd-Backend Length of output: 230 🏁 Script executed: # removeMemberFromOrg 메서드 찾기
rg "removeMemberFromOrg|findByUserIdAndOrgId" -A 5 -B 2Repository: WhereYouAd/WhereYouAd-Backend Length of output: 5882 일관성 있게 매개변수로 UserStatus를 전달하세요. 현재 User 엔티티의 status는 이 메서드도 동일한 패턴을 따르면:
호출부( - `@Query`("SELECT om FROM OrgMember om " +
- "WHERE om.user.id = :userId " +
- "AND om.organization.id = :orgId " +
- "AND om.user.status = 'ACTIVE'")
- Optional<OrgMember> findByUserIdAndOrgId(`@Param`("userId") Long userId, `@Param`("orgId") Long orgId);
+ `@Query`("SELECT om FROM OrgMember om " +
+ "WHERE om.user.id = :userId " +
+ "AND om.organization.id = :orgId " +
+ "AND om.user.status = :status")
+ Optional<OrgMember> findByUserIdAndOrgId(
+ `@Param`("userId") Long userId,
+ `@Param`("orgId") Long orgId,
+ `@Param`("status") UserStatus status);🤖 Prompt for AI Agents |
||
|
|
||
| //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<OrgMember> findOrgMemberByUserId(@Param("userId") Long userId); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -115,4 +115,14 @@ public ResponseEntity<DataResponse<OrgResponse.OrgMemberCountDTO>> getOrgMembers | |||||
| OrgResponse.OrgMemberCountDTO response = orgQueryService.getOrgMembersCount(orgId); | ||||||
| return ResponseEntity.ok(DataResponse.from(response)); | ||||||
| } | ||||||
|
|
||||||
| @DeleteMapping("{orgId}/members/{memberId}") | ||||||
| public ResponseEntity<DataResponse<String>> removeMember( | ||||||
| @AuthenticationPrincipal(expression = "userId") Long userId, // 관리자 ID | ||||||
| @PathVariable Long orgId, | ||||||
| @PathVariable Long memberId | ||||||
| ) { | ||||||
| orgService.removeMemberFromOrg(userId, orgId, memberId); | ||||||
| return ResponseEntity.ok(DataResponse.from("해당 맴버가 조직에서 제외되었습니다.")); | ||||||
|
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. 사용자 응답 메시지에 오타가 있습니다: "맴버" → "멤버"
✏️ 수정 제안- return ResponseEntity.ok(DataResponse.from("해당 맴버가 조직에서 제외되었습니다."));
+ return ResponseEntity.ok(DataResponse.from("해당 멤버가 조직에서 제외되었습니다."));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -119,4 +119,19 @@ ResponseEntity<DataResponse<OrgResponse.OrgMemberSliceDTO>> getOrgMembers( | |||||||||||||||||||||||
| ResponseEntity<DataResponse<OrgResponse.OrgMemberCountDTO>> 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") | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
|
Comment on lines
+127
to
+131
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. Swagger 문서에 400 오류 케이스가 누락되었습니다.
📄 수정 제안 `@ApiResponses`({
`@ApiResponse`(responseCode = "200", description = "성공"),
+ `@ApiResponse`(responseCode = "400_2", description = "자기 자신을 추방하려는 경우"),
+ `@ApiResponse`(responseCode = "400_3", description = "ADMIN 멤버를 추방하려는 경우"),
`@ApiResponse`(responseCode = "403", description = "권한이 부족한 경우(요청을 보낸 유저의 권한이 ADMIN이 아닌 경우)"),
`@ApiResponse`(responseCode = "404", description = "해당 id의 데이터 존재 X")
})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| public ResponseEntity<DataResponse<String>> removeMember( | ||||||||||||||||||||||||
| @AuthenticationPrincipal(expression = "userId") Long userId, | ||||||||||||||||||||||||
| @PathVariable Long orgId, | ||||||||||||||||||||||||
| @PathVariable Long memberId | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
# OrgMember 엔티티 파일 위치 찾기 fd OrgMember.java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 167
🏁 Script executed:
# OrgServiceImpl 파일 위치 찾기 및 해당 메서드 확인 fd OrgServiceImpl.java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 168
🏁 Script executed:
# OrgMemberRepository 파일 위치 fd OrgMemberRepository.java --type fRepository: WhereYouAd/WhereYouAd-Backend
Length of output: 181
🏁 Script executed:
Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 1342
🏁 Script executed:
Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 8207
🏁 Script executed:
Repository: WhereYouAd/WhereYouAd-Backend
Length of output: 2743
createOrganization에서 N+1 쿼리 문제가 실제로 발생합니다.OrgMember의organization필드가FetchType.LAZY로 설정되어 있고,findOrgMemberByUser(user)메서드가JOIN FETCH없이 단순 쿼리로 작동하기 때문입니다.현재 코드 흐름:
findOrgMemberByUser로 OrgMember 리스트 조회orgMember.getOrganization().getName()호출 시 각각 Organization 조회해결 방법:
Repository 메서드에
JOIN FETCH추가 (권장)이름 중복 검사 전용 쿼리 추가
중복 검사만 필요하다면 두 번째 방식이 더 효율적입니다.
🤖 Prompt for AI Agents