diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomCreationService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomCreationService.java new file mode 100644 index 00000000..8034a331 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomCreationService.java @@ -0,0 +1,121 @@ +package gg.agit.konect.domain.chat.service; + +import static gg.agit.konect.domain.chat.service.ChatRoomMembershipService.SYSTEM_ADMIN_ID; +import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_USER; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.user.enums.UserRole; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class ChatRoomCreationService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final UserRepository userRepository; + private final ChatRoomMembershipService chatRoomMembershipService; + + public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreateRequest request) { + User currentUser = userRepository.getById(currentUserId); + User targetUser = userRepository.getById(request.userId()); + + if (currentUser.getId().equals(targetUser.getId())) { + throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); + } + + if (currentUser.isAdmin() && !targetUser.isAdmin()) { + return getOrCreateSystemAdminChatRoomForUser(targetUser, currentUser); + } + + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers( + currentUser.getId(), + targetUser.getId(), + ChatType.DIRECT + ) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); + + LocalDateTime joinedAt = Objects.requireNonNull(chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null"); + chatRoomMembershipService.ensureDirectRoomRequester(chatRoom, currentUser, joinedAt); + chatRoomMembershipService.ensureMember(chatRoom, targetUser, joinedAt); + + return ChatRoomResponse.from(chatRoom); + } + + public ChatRoomResponse createOrGetAdminChatRoom(Integer currentUserId) { + User adminUser = userRepository.findFirstByRoleAndDeletedAtIsNullOrderByIdAsc(UserRole.ADMIN) + .orElseThrow(() -> CustomException.of(NOT_FOUND_USER)); + + return createOrGetChatRoom(currentUserId, new ChatRoomCreateRequest(adminUser.getId())); + } + + public ChatRoomResponse createGroupChatRoom(Integer currentUserId, ChatRoomCreateRequest.Group request) { + User creator = userRepository.getById(currentUserId); + + List distinctUserIds = request.userIds().stream() + .distinct() + .filter(id -> !id.equals(currentUserId)) + .toList(); + + if (distinctUserIds.isEmpty()) { + throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); + } + + List invitees = userRepository.findAllByIdIn(distinctUserIds); + if (invitees.size() != distinctUserIds.size()) { + throw CustomException.of(NOT_FOUND_USER); + } + + ChatRoom chatRoom = chatRoomRepository.save(ChatRoom.groupOf()); + LocalDateTime joinedAt = Objects.requireNonNull( + chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null" + ); + + List members = new ArrayList<>(); + members.add(ChatRoomMember.ofOwner(chatRoom, creator, joinedAt)); + invitees.forEach(user -> members.add(ChatRoomMember.of(chatRoom, user, joinedAt))); + chatRoomMemberRepository.saveAll(members); + + return ChatRoomResponse.from(chatRoom); + } + + private ChatRoomResponse getOrCreateSystemAdminChatRoomForUser(User targetUser, User adminUser) { + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(SYSTEM_ADMIN_ID, targetUser.getId(), ChatType.DIRECT) + .orElseGet(() -> { + ChatRoom newRoom = chatRoomRepository.save(ChatRoom.directOf()); + User systemAdmin = userRepository.getById(SYSTEM_ADMIN_ID); + LocalDateTime joinedAt = Objects.requireNonNull( + newRoom.getCreatedAt(), "chatRoom.createdAt must not be null" + ); + chatRoomMembershipService.ensureMember(newRoom, systemAdmin, joinedAt); + chatRoomMembershipService.ensureMember(newRoom, targetUser, joinedAt); + return newRoom; + }); + + LocalDateTime joinedAt = Objects.requireNonNull( + chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null" + ); + chatRoomMembershipService.ensureDirectRoomRequester(chatRoom, adminUser, joinedAt); + + return ChatRoomResponse.from(chatRoom); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java index ee6b3211..012e11df 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java @@ -148,7 +148,8 @@ private ChatRoom findOrCreateClubRoom(Club club) { }); } - private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { + @Transactional + public void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { LocalDateTime lastReadAt = member.getLastReadAt(); @@ -158,6 +159,26 @@ private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { }, () -> saveRoomMemberIgnoringDuplicate(room, user, baseline)); } + @Transactional + public void ensureDirectRoomRequester(ChatRoom room, User user, LocalDateTime joinedAt) { + if (shouldSkipSystemAdminMembership(room, user)) { + return; + } + + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + if (member.hasLeft()) { + member.reopenDirectRoom(LocalDateTime.now()); + return; + } + + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(joinedAt)) { + member.updateLastReadAt(joinedAt); + } + }, () -> saveRoomMemberIgnoringDuplicate(room, user, joinedAt)); + } + private void saveRoomMemberIgnoringDuplicate(ChatRoom room, User user, LocalDateTime baseline) { try { chatRoomMemberRepository.save(ChatRoomMember.of(room, user, baseline)); @@ -184,6 +205,12 @@ private void ensureDirectRoomMemberExists(ChatRoom room, User user, LocalDateTim throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); } + private boolean shouldSkipSystemAdminMembership(ChatRoom room, User user) { + // 문의방은 SYSTEM_ADMIN + 일반 사용자 2인 구조를 전제로 재사용(findByTwoUsers)되므로, + // 생성/재오픈 경로에서도 일반 ADMIN을 멤버로 추가하면 안 된다. + return user.isAdmin() && chatRoomSystemAdminService.isSystemAdminRoom(room.getId()); + } + private boolean isDuplicateKeyException(DataIntegrityViolationException e) { if (e instanceof DuplicateKeyException) { return true; diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 4d1b3b2b..6a35671a 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -9,7 +9,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -76,95 +75,24 @@ public class ChatService { private final ChatSearchService chatSearchService; private final ChatInviteService chatInviteService; private final ChatMessagePageResolver chatMessagePageResolver; + private final ChatRoomCreationService chatRoomCreationService; private final ChatRoomSystemAdminService chatRoomSystemAdminService; private final ChatDirectRoomAccessService chatDirectRoomAccessService; private final ChatMessageSendService chatMessageSendService; @Transactional public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreateRequest request) { - User currentUser = userRepository.getById(currentUserId); - User targetUser = userRepository.getById(request.userId()); - - if (currentUser.getId().equals(targetUser.getId())) { - throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); - } - - if (currentUser.isAdmin() && !targetUser.isAdmin()) { - return getOrCreateSystemAdminChatRoomForUser(targetUser, currentUser); - } - - ChatRoom chatRoom = chatRoomRepository.findByTwoUsers( - currentUser.getId(), - targetUser.getId(), - ChatType.DIRECT - ) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); - - LocalDateTime joinedAt = Objects.requireNonNull(chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null"); - ensureDirectRoomRequester(chatRoom, currentUser, joinedAt); - ensureRoomMember(chatRoom, targetUser, joinedAt); - - return ChatRoomResponse.from(chatRoom); - } - - private ChatRoomResponse getOrCreateSystemAdminChatRoomForUser(User targetUser, User adminUser) { - ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(SYSTEM_ADMIN_ID, targetUser.getId(), ChatType.DIRECT) - .orElseGet(() -> { - ChatRoom newRoom = chatRoomRepository.save(ChatRoom.directOf()); - User systemAdmin = userRepository.getById(SYSTEM_ADMIN_ID); - LocalDateTime joinedAt = Objects.requireNonNull( - newRoom.getCreatedAt(), "chatRoom.createdAt must not be null" - ); - ensureRoomMember(newRoom, systemAdmin, joinedAt); - ensureRoomMember(newRoom, targetUser, joinedAt); - return newRoom; - }); - - LocalDateTime joinedAt = Objects.requireNonNull( - chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null" - ); - ensureDirectRoomRequester(chatRoom, adminUser, joinedAt); - - return ChatRoomResponse.from(chatRoom); + return chatRoomCreationService.createOrGetChatRoom(currentUserId, request); } @Transactional public ChatRoomResponse createOrGetAdminChatRoom(Integer currentUserId) { - User adminUser = userRepository.findFirstByRoleAndDeletedAtIsNullOrderByIdAsc(UserRole.ADMIN) - .orElseThrow(() -> CustomException.of(NOT_FOUND_USER)); - - return createOrGetChatRoom(currentUserId, new ChatRoomCreateRequest(adminUser.getId())); + return chatRoomCreationService.createOrGetAdminChatRoom(currentUserId); } @Transactional public ChatRoomResponse createGroupChatRoom(Integer currentUserId, ChatRoomCreateRequest.Group request) { - User creator = userRepository.getById(currentUserId); - - List distinctUserIds = request.userIds().stream() - .distinct() - .filter(id -> !id.equals(currentUserId)) - .toList(); - - if (distinctUserIds.isEmpty()) { - throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); - } - - List invitees = userRepository.findAllByIdIn(distinctUserIds); - if (invitees.size() != distinctUserIds.size()) { - throw CustomException.of(NOT_FOUND_USER); - } - - ChatRoom chatRoom = chatRoomRepository.save(ChatRoom.groupOf()); - LocalDateTime joinedAt = Objects.requireNonNull( - chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null" - ); - - List members = new ArrayList<>(); - members.add(ChatRoomMember.ofOwner(chatRoom, creator, joinedAt)); - invitees.forEach(user -> members.add(ChatRoomMember.of(chatRoom, user, joinedAt))); - chatRoomMemberRepository.saveAll(members); - - return ChatRoomResponse.from(chatRoom); + return chatRoomCreationService.createGroupChatRoom(currentUserId, request); } @Transactional @@ -743,31 +671,6 @@ private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); } - private void ensureDirectRoomRequester(ChatRoom room, User user, LocalDateTime joinedAt) { - if (shouldSkipSystemAdminMembership(room, user)) { - return; - } - - chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) - .ifPresentOrElse(member -> { - if (member.hasLeft()) { - member.reopenDirectRoom(LocalDateTime.now()); - return; - } - - LocalDateTime lastReadAt = member.getLastReadAt(); - if (lastReadAt == null || lastReadAt.isBefore(joinedAt)) { - member.updateLastReadAt(joinedAt); - } - }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); - } - - private boolean shouldSkipSystemAdminMembership(ChatRoom room, User user) { - // 문의방은 SYSTEM_ADMIN + 일반 사용자 2인 구조를 전제로 재사용(findByTwoUsers)되므로, - // 생성/재오픈 경로에서도 일반 ADMIN을 멤버로 추가하면 안 된다. - return user.isAdmin() && chatRoomSystemAdminService.isSystemAdminRoom(room.getId()); - } - private String normalizeCustomRoomName(String roomName) { if (!StringUtils.hasText(roomName)) { return null; diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java index 9f7380bb..30747c3a 100644 --- a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java @@ -56,6 +56,7 @@ import gg.agit.konect.domain.chat.service.ChatMessageSendService; import gg.agit.konect.domain.chat.service.ChatMessagePageResolver; import gg.agit.konect.domain.chat.service.ChatPresenceService; +import gg.agit.konect.domain.chat.service.ChatRoomCreationService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; import gg.agit.konect.domain.chat.service.ChatSearchService; @@ -138,6 +139,19 @@ void setUp() { clubMemberRepository, chatRoomSystemAdminService ); + ChatRoomMembershipService chatRoomMembershipForCreation = new ChatRoomMembershipService( + chatRoomRepository, + chatRoomMemberRepository, + clubMemberRepository, + userRepository, + chatRoomSystemAdminService + ); + ChatRoomCreationService chatRoomCreationService = new ChatRoomCreationService( + chatRoomRepository, + chatRoomMemberRepository, + userRepository, + chatRoomMembershipForCreation + ); ChatMessageSendService chatMessageSendService = new ChatMessageSendService( chatRoomRepository, chatMessageRepository, @@ -163,6 +177,7 @@ void setUp() { chatSearchService, chatInviteService, chatMessagePageResolver, + chatRoomCreationService, chatRoomSystemAdminService, chatDirectRoomAccessService, chatMessageSendService