Skip to content

Commit

Permalink
feat: ✨ Chat Room Delete API (#222)
Browse files Browse the repository at this point in the history
* feat: add chat-room-delete-command

* feat: chat-member delete by chat-room-id query

* feat: impl service logic

* style: using var key-word

* fix: add static factory method to the command

* rename: from find-by-chat-room-id to find-by-chat-member-id in chat-member-repository

* style: add chat-room-delete-status-log in the delete-service

* test: chat-room-delete-service-integration-test

* rename: from delete-chat-room to execute in the chat-room-delete-service

* feat: chat room delete usecase

* feat: chat-room-delete controller

* docs: chat-room delete api swagger

* chore: fix log setting simple
  • Loading branch information
psychology50 authored Jan 16, 2025
1 parent 0c3a98f commit da91ad0
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.co.pennyway.api.apis.chat.dto.ChatRoomReq;
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes;
import kr.co.pennyway.api.common.annotation.ApiExceptionExplanation;
import kr.co.pennyway.api.common.annotation.ApiResponseExplanations;
import kr.co.pennyway.api.common.response.SliceResponseTemplate;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorCode;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
Expand Down Expand Up @@ -50,4 +53,13 @@ public interface ChatRoomApi {
@Parameter(name = "chatRoomId", description = "μˆ˜μ •ν•  μ±„νŒ…λ°©μ˜ μ‹λ³„μž", example = "1", required = true)
@ApiResponse(responseCode = "200", description = "μ±„νŒ…λ°© μˆ˜μ • 성곡", content = @Content(schemaProperties = @SchemaProperty(name = "chatRoom", schema = @Schema(implementation = ChatRoomRes.Detail.class))))
ResponseEntity<?> updateChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @Validated @RequestBody ChatRoomReq.Update request);

@Operation(summary = "μ±„νŒ…λ°© μ‚­μ œ", method = "DELETE", description = "μ±„νŒ…λ°©μ„ μ‚­μ œν•œλ‹€. μ±„νŒ…λ°© λ°©μž₯만이 κ°€λŠ₯ν•˜λ©°, μ±„νŒ…λ°©μ„ μ‚­μ œν•˜λ©΄ μ±„νŒ…λ°©μ— μ°Έμ—¬ν•œ λͺ¨λ“  μ‚¬μš©μžκ°€ μ±„νŒ…λ°©μ—μ„œ λ‚˜κ°€κ²Œ λœλ‹€.")
@Parameter(name = "chatRoomId", description = "μ‚­μ œν•  μ±„νŒ…λ°©μ˜ μ‹λ³„μž", example = "1", required = true)
@ApiResponseExplanations(errors = {
@ApiExceptionExplanation(value = ChatMemberErrorCode.class, constant = "NOT_FOUND", summary = "μ±„νŒ…λ°© 멀버 정보λ₯Ό 찾을 수 μ—†μŒ"),
@ApiExceptionExplanation(value = ChatMemberErrorCode.class, constant = "NOT_ADMIN", summary = "κΆŒν•œ μ—†μŒ", description = "μ±„νŒ…λ°© λ°©μž₯이 μ•„λ‹ˆλΌ μ±„νŒ…λ°©μ„ μ‚­μ œν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
})
@ApiResponse(responseCode = "200", description = "μ±„νŒ…λ°© μ‚­μ œ 성곡")
ResponseEntity<?> deleteChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @AuthenticationPrincipal SecurityUserDetails user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,13 @@ public ResponseEntity<?> getChatRoom(@PathVariable("chatRoomId") Long chatRoomId
public ResponseEntity<?> updateChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @Validated @RequestBody ChatRoomReq.Update request) {
return ResponseEntity.ok(SuccessResponse.from(CHAT_ROOM, chatRoomUseCase.updateChatRoom(request)));
}

@Override
@DeleteMapping("/{chatRoomId}")
@PreAuthorize("isAuthenticated() and @chatRoomManager.hasAdminPermission(principal.userId, #chatRoomId)")
public ResponseEntity<?> deleteChatRoom(@PathVariable("chatRoomId") Long chatRoomId, @AuthenticationPrincipal SecurityUserDetails user) {
chatRoomUseCase.deleteChatRoom(user.getUserId(), chatRoomId);

return ResponseEntity.ok(SuccessResponse.noContent());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import kr.co.pennyway.api.apis.chat.service.*;
import kr.co.pennyway.api.common.response.SliceResponseTemplate;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.context.chat.dto.ChatRoomDeleteCommand;
import kr.co.pennyway.domain.context.chat.service.ChatRoomDeleteService;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import lombok.RequiredArgsConstructor;
Expand All @@ -22,6 +24,7 @@ public class ChatRoomUseCase {
private final ChatRoomSearchService chatRoomSearchService;
private final ChatRoomWithParticipantsSearchService chatRoomWithParticipantsSearchService;
private final ChatRoomPatchHelper chatRoomPatchHelper;
private final ChatRoomDeleteService chatRoomDeleteService;

private final ChatMemberSearchService chatMemberSearchService;

Expand Down Expand Up @@ -59,4 +62,8 @@ public ChatRoomRes.Detail updateChatRoom(ChatRoomReq.Update request) {

return ChatRoomMapper.toChatRoomResDetail(chatRoom, null, true, 1, 0);
}

public void deleteChatRoom(Long userId, Long chatRoomId) {
chatRoomDeleteService.execute(ChatRoomDeleteCommand.of(userId, chatRoomId));
}
}
10 changes: 4 additions & 6 deletions pennyway-app-external-api/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,12 @@

<!-- μ—λŸ¬ λ°œμƒμ‹œ λ¬΄μ‹œν•˜λ„λ‘ μ„€μ • -->
<prudent>true</prudent>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 둀링된 파일 λͺ…λͺ… κ·œμΉ™ -->
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${LOG_FILE_NAME}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- νŒŒμΌλ‹Ή μ΅œλŒ€ 크기 -->
<maxFileSize>${LOG_MAX_FILE_SIZE}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- νŒŒμΌλ‹Ή μ΅œλŒ€ 크기 -->
<maxFileSize>${LOG_MAX_FILE_SIZE}</maxFileSize>
<!-- 보관 μ£ΌκΈ° -->
<maxHistory>${LOG_MAX_HISTORY}</maxHistory>
<!-- 총 파일 크기 μ œν•œ -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import kr.co.pennyway.domain.common.repository.ExtendedRepository;
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import kr.co.pennyway.domain.domains.member.type.ChatMemberRole;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -23,7 +24,7 @@ public interface ChatMemberRepository extends ExtendedRepository<ChatMember, Lon

@Transactional(readOnly = true)
@Query("SELECT cm FROM ChatMember cm WHERE cm.id = :chatMemberId")
Optional<ChatMember> findByChatRoom_Id(Long chatMemberId);
Optional<ChatMember> findByChatMember_Id(Long chatMemberId);

@Transactional(readOnly = true)
@Query("SELECT cm FROM ChatMember cm WHERE cm.chatRoom.id = :chatRoomId AND cm.user.id = :userId AND cm.deletedAt IS NULL")
Expand All @@ -40,4 +41,9 @@ public interface ChatMemberRepository extends ExtendedRepository<ChatMember, Lon
@Transactional(readOnly = true)
@Query("SELECT cm.user.id FROM ChatMember cm WHERE cm.chatRoom.id = :chatRoomId AND cm.deletedAt IS NULL")
Set<Long> findUserIdsByChatRoomId(Long chatRoomId);

@Transactional
@Modifying(clearAutomatically = true)
@Query("UPDATE ChatMember cm SET cm.deletedAt = NOW() WHERE cm.chatRoom.id = :chatRoomId")
void deleteAllByChatRoomId(Long chatRoomId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public ChatMember createMember(User user, ChatRoom chatRoom) {

@Transactional(readOnly = true)
public Optional<ChatMember> readChatMemberByChatMemberId(Long chatMemberId) {
return chatMemberRepository.findByChatRoom_Id(chatMemberId);
return chatMemberRepository.findByChatMember_Id(chatMemberId);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -155,4 +155,9 @@ public long countActiveMembers(Long chatRoomId) {
public void update(ChatMember chatMember) {
chatMemberRepository.save(chatMember);
}

@Transactional
public void deleteAllByChatRoomId(Long chatRoomId) {
chatMemberRepository.deleteAllByChatRoomId(chatRoomId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.co.pennyway.domain.context.chat.dto;

public record ChatRoomDeleteCommand(
Long userId,
Long chatRoomId
) {
public ChatRoomDeleteCommand {
if (userId == null) {
throw new IllegalArgumentException("userId must not be null");
}
if (chatRoomId == null) {
throw new IllegalArgumentException("chatRoomId must not be null");
}
}

public static ChatRoomDeleteCommand of(Long userId, Long chatRoomId) {
return new ChatRoomDeleteCommand(userId, chatRoomId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package kr.co.pennyway.domain.context.chat.service;

import kr.co.pennyway.common.annotation.DomainService;
import kr.co.pennyway.domain.context.chat.dto.ChatRoomDeleteCommand;
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomRdbService;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorCode;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorException;
import kr.co.pennyway.domain.domains.member.service.ChatMemberRdbService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@DomainService
@RequiredArgsConstructor
public class ChatRoomDeleteService {
private final ChatMemberRdbService chatMemberRdbService;
private final ChatRoomRdbService chatRoomRdbService;

@Transactional
public void execute(ChatRoomDeleteCommand command) {
var admin = chatMemberRdbService.readChatMember(command.userId(), command.chatRoomId())
.orElseThrow(() -> new ChatMemberErrorException(ChatMemberErrorCode.NOT_FOUND));

if (!admin.isAdmin()) throw new ChatMemberErrorException(ChatMemberErrorCode.NOT_ADMIN);

var chatRoom = admin.getChatRoom();

chatMemberRdbService.deleteAllByChatRoomId(chatRoom.getId());
chatRoomRdbService.delete(chatRoom);

log.info("μ±„νŒ…λ°©μ΄ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. chatRoom: {}", chatRoom);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package kr.co.pennyway.domain.context.chat.integration;

import kr.co.pennyway.domain.common.repository.ExtendedRepositoryFactory;
import kr.co.pennyway.domain.config.DomainServiceIntegrationProfileResolver;
import kr.co.pennyway.domain.config.DomainServiceTestInfraConfig;
import kr.co.pennyway.domain.config.JpaTestConfig;
import kr.co.pennyway.domain.context.chat.dto.ChatRoomDeleteCommand;
import kr.co.pennyway.domain.context.chat.service.ChatRoomDeleteService;
import kr.co.pennyway.domain.context.common.fixture.ChatRoomFixture;
import kr.co.pennyway.domain.context.common.fixture.UserFixture;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import kr.co.pennyway.domain.domains.chatroom.repository.ChatRoomRepository;
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomRdbService;
import kr.co.pennyway.domain.domains.member.domain.ChatMember;
import kr.co.pennyway.domain.domains.member.exception.ChatMemberErrorException;
import kr.co.pennyway.domain.domains.member.repository.ChatMemberRepository;
import kr.co.pennyway.domain.domains.member.service.ChatMemberRdbService;
import kr.co.pennyway.domain.domains.member.type.ChatMemberRole;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.context.ActiveProfiles;

import java.util.List;
import java.util.stream.IntStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Slf4j
@EnableAutoConfiguration
@SpringBootTest(classes = {ChatRoomDeleteService.class, ChatMemberRdbService.class, ChatRoomRdbService.class})
@EntityScan(basePackageClasses = {User.class, ChatRoom.class, ChatMember.class})
@EnableJpaRepositories(
basePackageClasses = {UserRepository.class, ChatRoomRepository.class, ChatMemberRepository.class},
repositoryFactoryBeanClass = ExtendedRepositoryFactory.class
)
@ActiveProfiles(resolver = DomainServiceIntegrationProfileResolver.class)
@Import(value = {JpaTestConfig.class})
public class ChatRoomDeleteServiceIntegrationTest extends DomainServiceTestInfraConfig {
@Autowired
private ChatRoomDeleteService chatRoomDeleteService;

@Autowired
private UserRepository userRepository;

@Autowired
private ChatMemberRepository chatMemberRepository;

@Autowired
private ChatRoomRepository chatRoomRepository;

@AfterEach
void tearDown() {
chatMemberRepository.deleteAll();
chatRoomRepository.deleteAll();
userRepository.deleteAll();
}

@Test
@DisplayName("κ΄€λ¦¬μžλŠ” μ±„νŒ…λ°©μ„ μ‚­μ œν•  수 μžˆλ‹€.")
void shouldChatRoomDeletedWhenAdminExecute() {
// given
var user = userRepository.save(UserFixture.GENERAL_USER.toUser());
var chatRoom = chatRoomRepository.save(ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntityWithId(1L));
var admin = chatMemberRepository.save(ChatMember.of(user, chatRoom, ChatMemberRole.ADMIN));

ChatRoomDeleteCommand command = ChatRoomDeleteCommand.of(user.getId(), chatRoom.getId());

// when
chatRoomDeleteService.execute(command);

// then
var chatMembers = chatMemberRepository.findAll();
assertThat(chatMembers).hasSize(1);
assertTrue(chatMembers.stream().noneMatch(ChatMember::isActive));
assertThat(chatRoomRepository.findById(chatRoom.getId())).isEmpty();
}

@Test
@DisplayName("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” 멀버가 μ±„νŒ…λ°© μ‚­μ œλ₯Ό μ‹œλ„ν•˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.")
void shouldThrowExceptionWhenNonExistMemberExecute() {
// given
var user = userRepository.save(UserFixture.GENERAL_USER.toUser());
var chatRoom = chatRoomRepository.save(ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntityWithId(2L));

ChatRoomDeleteCommand command = ChatRoomDeleteCommand.of(user.getId(), chatRoom.getId());

// when & then
assertThatThrownBy(() -> chatRoomDeleteService.execute(command))
.isInstanceOf(ChatMemberErrorException.class);
}

@Test
@DisplayName("일반 μ‚¬μš©μžλŠ” μ±„νŒ…λ°©μ„ μ‚­μ œν•  수 μ—†λ‹€.")
void shouldThrowExceptionWhenGeneralUserExecute() {
// given
var user = userRepository.save(UserFixture.GENERAL_USER.toUser());
var chatRoom = chatRoomRepository.save(ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntityWithId(3L));
chatMemberRepository.save(ChatMember.of(user, chatRoom, ChatMemberRole.MEMBER));

ChatRoomDeleteCommand command = ChatRoomDeleteCommand.of(user.getId(), chatRoom.getId());

// when & then
assertThatThrownBy(() -> chatRoomDeleteService.execute(command))
.isInstanceOf(ChatMemberErrorException.class);
}

@Test
@DisplayName("μ±„νŒ…λ°©μ— μ†ν•œ λͺ¨λ“  멀버가 μ‚­μ œλœλ‹€.")
void shouldAllChatMembersDeletedWhenExecute() {
// given
var chatRoom = chatRoomRepository.save(ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntityWithId(4L));
var users = createUsers(10);
var admin = chatMemberRepository.save(ChatMember.of(users.get(0), chatRoom, ChatMemberRole.ADMIN));
var members = createGeneralChatMembers(users.subList(1, users.size()), chatRoom);

ChatRoomDeleteCommand command = ChatRoomDeleteCommand.of(admin.getUser().getId(), chatRoom.getId());

// when
chatRoomDeleteService.execute(command);

// then
var chatMembers = chatMemberRepository.findAll();
assertThat(chatMembers).hasSize(10);
assertTrue(chatMembers.stream().noneMatch(ChatMember::isActive));
assertThat(chatRoomRepository.findById(chatRoom.getId())).isEmpty();
}

private List<User> createUsers(int count) {
return IntStream.range(0, count)
.mapToObj(i -> userRepository.save(UserFixture.GENERAL_USER.toUser()))
.toList();
}

private List<ChatMember> createGeneralChatMembers(List<User> users, ChatRoom chatRoom) {
return users.stream()
.map(user -> chatMemberRepository.save(ChatMember.of(user, chatRoom, ChatMemberRole.MEMBER)))
.toList();
}
}

0 comments on commit da91ad0

Please sign in to comment.