From af58ae70411203dc6970d6ea56865533d4a568a0 Mon Sep 17 00:00:00 2001 From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:26:46 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E2=9C=8F=EF=B8=8F=20Users=20are=20unabl?= =?UTF-8?q?e=20to=20delete=20their=20accounts=20while=20they=20hold=20owne?= =?UTF-8?q?rship=20of=20any=20chatrooms=20(#198)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add method of find existence that user has chatroom's ownership anyone on repository * feat: add method in domain service * feat: add 409 error for user's signout blocking because user has chat room ownership * feat: resolve todo annotation * test: add delete test about has ownership chatroom user can't delete account --- .../apis/users/service/UserDeleteService.java | 8 +++-- .../users/usecase/UserDeleteServiceTest.java | 31 ++++++++++++++++--- .../CustomChatMemberRepository.java | 5 +++ .../CustomChatMemberRepositoryImpl.java | 10 ++++++ .../member/service/ChatMemberService.java | 5 +++ .../domains/user/exception/UserErrorCode.java | 1 + 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/UserDeleteService.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/UserDeleteService.java index 3f99e7c5e..55494224d 100644 --- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/UserDeleteService.java +++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/users/service/UserDeleteService.java @@ -1,6 +1,7 @@ package kr.co.pennyway.api.apis.users.service; import kr.co.pennyway.domain.domains.device.service.DeviceTokenService; +import kr.co.pennyway.domain.domains.member.service.ChatMemberService; import kr.co.pennyway.domain.domains.oauth.service.OauthService; import kr.co.pennyway.domain.domains.spending.service.SpendingCustomCategoryService; import kr.co.pennyway.domain.domains.spending.service.SpendingService; @@ -27,6 +28,8 @@ public class UserDeleteService { private final OauthService oauthService; private final DeviceTokenService deviceTokenService; + private final ChatMemberService chatMemberService; + private final SpendingService spendingService; private final SpendingCustomCategoryService spendingCustomCategoryService; @@ -36,13 +39,14 @@ public class UserDeleteService { * hard delete가 수행되어야 할 데이터는 삭제하지 않으며, 사용자 데이터 유지 기간이 만료될 때 DBA가 수행한다. * * @param userId - * @todo [2024-05-03] 채팅 기능이 추가되는 경우 채팅방장 탈퇴를 제한해야 하며, 추가로 삭제될 엔티티 삭제 로직을 추가해야 한다. */ @Transactional public void execute(Long userId) { if (!userService.isExistUser(userId)) throw new UserErrorException(UserErrorCode.NOT_FOUND); - // TODO: [2024-05-03] 하나라도 채팅방의 방장으로 참여하는 경우 삭제 불가능 처리 + if (chatMemberService.hasUserChatRoomOwnership(userId)) { + throw new UserErrorException(UserErrorCode.HAS_OWNERSHIP_CHAT_ROOM); + } oauthService.deleteOauthsByUserIdInQuery(userId); deviceTokenService.deleteDevicesByUserIdInQuery(userId); diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/UserDeleteServiceTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/UserDeleteServiceTest.java index a1c5bef36..55476d524 100644 --- a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/UserDeleteServiceTest.java +++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/users/usecase/UserDeleteServiceTest.java @@ -3,13 +3,14 @@ import kr.co.pennyway.api.apis.users.service.UserDeleteService; import kr.co.pennyway.api.config.ExternalApiDBTestConfig; import kr.co.pennyway.api.config.TestJpaConfig; -import kr.co.pennyway.api.config.fixture.DeviceTokenFixture; -import kr.co.pennyway.api.config.fixture.SpendingCustomCategoryFixture; -import kr.co.pennyway.api.config.fixture.SpendingFixture; -import kr.co.pennyway.api.config.fixture.UserFixture; +import kr.co.pennyway.api.config.fixture.*; import kr.co.pennyway.domain.config.JpaConfig; +import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom; +import kr.co.pennyway.domain.domains.chatroom.repository.ChatRoomRepository; import kr.co.pennyway.domain.domains.device.domain.DeviceToken; import kr.co.pennyway.domain.domains.device.service.DeviceTokenService; +import kr.co.pennyway.domain.domains.member.repository.ChatMemberRepository; +import kr.co.pennyway.domain.domains.member.service.ChatMemberService; import kr.co.pennyway.domain.domains.oauth.domain.Oauth; import kr.co.pennyway.domain.domains.oauth.service.OauthService; import kr.co.pennyway.domain.domains.oauth.type.Provider; @@ -40,7 +41,7 @@ @Slf4j @ExtendWith(MockitoExtension.class) @DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create") -@ContextConfiguration(classes = {JpaConfig.class, UserDeleteService.class, UserService.class, OauthService.class, DeviceTokenService.class, SpendingService.class, SpendingCustomCategoryService.class}) +@ContextConfiguration(classes = {JpaConfig.class, UserDeleteService.class, UserService.class, OauthService.class, DeviceTokenService.class, SpendingService.class, SpendingCustomCategoryService.class, ChatMemberService.class}) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Import(TestJpaConfig.class) public class UserDeleteServiceTest extends ExternalApiDBTestConfig { @@ -62,6 +63,12 @@ public class UserDeleteServiceTest extends ExternalApiDBTestConfig { @Autowired private SpendingCustomCategoryService spendingCustomCategoryService; + @Autowired + private ChatRoomRepository chatRoomRepository; + + @Autowired + private ChatMemberRepository chatMemberRepository; + @Test @Transactional @DisplayName("사용자가 삭제된 유저를 조회하려는 경우 NOT_FOUND 에러를 반환한다.") @@ -144,6 +151,20 @@ void deleteAccountWithSpending() { assertTrue("지출 카테고리가 삭제되어 있어야 한다.", spendingCustomCategoryService.readSpendingCustomCategory(category.getId()).isEmpty()); } + @Test + @Transactional + @DisplayName("사용자가 채팅방장으로 등록된 채팅방이 하나 이상 존재하는 경우, 삭제할 수 없다.") + void deleteAccountWithOwnershipChatRoom() { + // given + User user = userService.createUser(UserFixture.GENERAL_USER.toUser()); + ChatRoom chatRoom = chatRoomRepository.save(ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntity(1L)); + chatMemberRepository.save(ChatMemberFixture.ADMIN.toEntity(user, chatRoom)); + + // when - then + UserErrorException ex = assertThrows(UserErrorException.class, () -> userDeleteService.execute(user.getId())); + assertEquals("채팅방장으로 등록된 채팅방이 하나 이상 존재하는 경우, 삭제할 수 없다.", UserErrorCode.HAS_OWNERSHIP_CHAT_ROOM, ex.getBaseErrorCode()); + } + private Oauth createOauth(Provider provider, String providerId, User user) { Oauth oauth = Oauth.of(provider, providerId, user); return oauthService.createOauth(oauth); diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepository.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepository.java index f4519e35e..433a28758 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepository.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepository.java @@ -11,6 +11,11 @@ public interface CustomChatMemberRepository { */ boolean existsByChatRoomIdAndUserId(Long chatRoomId, Long userId); + /** + * 해당 유저가 채팅방장으로 가입한 채팅방이 존재하는지 확인한다. + */ + boolean existsOwnershipChatRoomByUserId(Long userId); + /** * 채팅방의 관리자 정보를 조회한다. */ diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepositoryImpl.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepositoryImpl.java index ebf33d58e..e33d012c3 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepositoryImpl.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/repository/CustomChatMemberRepositoryImpl.java @@ -28,6 +28,16 @@ public boolean existsByChatRoomIdAndUserId(Long chatRoomId, Long userId) { .fetchFirst() != null; } + @Override + public boolean existsOwnershipChatRoomByUserId(Long userId) { + return queryFactory.select(ConstantImpl.create(1)) + .from(chatMember) + .where(chatMember.user.id.eq(userId) + .and(chatMember.role.eq(ChatMemberRole.ADMIN)) + .and(chatMember.deletedAt.isNull())) + .fetchFirst() != null; + } + @Override public Optional findAdminByChatRoomId(Long chatRoomId) { ChatMemberResult.Detail result = diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java index 3c4b57468..ce81bbdc5 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/member/service/ChatMemberService.java @@ -123,6 +123,11 @@ public boolean isExists(Long chatRoomId, Long userId) { return chatMemberRepository.existsByChatRoomIdAndUserId(chatRoomId, userId); } + @Transactional(readOnly = true) + public boolean hasUserChatRoomOwnership(Long userId) { + return chatMemberRepository.existsOwnershipChatRoomByUserId(userId); + } + @Transactional(readOnly = true) public long countActiveMembers(Long chatRoomId) { return chatMemberRepository.countByChatRoomIdAndActive(chatRoomId); diff --git a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/user/exception/UserErrorCode.java b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/user/exception/UserErrorCode.java index a44a5b225..cacb5f055 100644 --- a/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/user/exception/UserErrorCode.java +++ b/pennyway-domain/src/main/java/kr/co/pennyway/domain/domains/user/exception/UserErrorCode.java @@ -26,6 +26,7 @@ public enum UserErrorCode implements BaseErrorCode { ALREADY_SIGNUP(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "이미 회원가입한 유저입니다."), ALREADY_EXIST_USERNAME(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "이미 존재하는 아이디입니다."), ALREADY_EXIST_PHONE(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "이미 존재하는 휴대폰 번호입니다."), + HAS_OWNERSHIP_CHAT_ROOM(StatusCode.CONFLICT, ReasonCode.RESOURCE_ALREADY_EXISTS, "채팅방의 방장으로 참여하고 있는 경우 삭제할 수 없습니다."), /* 404 NOT_FOUND */ NOT_FOUND(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "유저를 찾을 수 없습니다."),