diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomAccessService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomAccessService.java new file mode 100644 index 00000000..d7dc78b0 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomAccessService.java @@ -0,0 +1,69 @@ +package gg.agit.konect.domain.chat.service; + +import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +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.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomAccessService { + + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final UserRepository userRepository; + private final ChatRoomMembershipService chatRoomMembershipService; + private final ChatRoomSystemAdminService chatRoomSystemAdminService; + private final ChatDirectRoomAccessService chatDirectRoomAccessService; + + public ChatRoomMember getAccessibleMember(ChatRoom room, Integer userId) { + if (room.isDirectRoom()) { + User user = userRepository.getById(userId); + return chatDirectRoomAccessService.getAccessibleMember(room, user); + } + + return getAccessibleNonDirectMember(room, userId); + } + + public ChatRoomMember getAccessibleMember(ChatRoom room, User user) { + if (room.isDirectRoom()) { + return chatDirectRoomAccessService.getAccessibleMember(room, user); + } + + return getAccessibleNonDirectMember(room, user.getId()); + } + + private ChatRoomMember getAccessibleNonDirectMember(ChatRoom room, Integer userId) { + if (room.isClubGroupRoom()) { + chatRoomMembershipService.ensureClubRoomMember(room, userId); + return getRoomMember(room.getId(), userId); + } + + ChatRoomMember member = getRoomMember(room.getId(), userId); + if (member.hasLeft()) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + return member; + } + + public void ensureMuteAccess(ChatRoom room, User user) { + if (room.isDirectRoom() && user.isAdmin() && chatRoomSystemAdminService.isSystemAdminRoom(room.getId())) { + return; + } + + getAccessibleMember(room, user); + } + + private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { + return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) + .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); + } +} 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 012e11df..987f1386 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 @@ -125,6 +125,11 @@ public void updateDirectRoomLastReadAt(Integer roomId, User user, LocalDateTime public void ensureClubRoomMember(Integer roomId, Integer userId) { ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + ensureClubRoomMember(room, userId); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void ensureClubRoomMember(ChatRoom room, Integer userId) { if (!room.isGroupRoom() || room.getClub() == null) { throw CustomException.of(NOT_FOUND_CHAT_ROOM); } 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 67c2d27b..f647730a 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 @@ -74,6 +74,7 @@ public class ChatService { private final ChatInviteService chatInviteService; private final ChatMessageReadService chatMessageReadService; private final ChatMessagePageResolver chatMessagePageResolver; + private final ChatRoomAccessService chatRoomAccessService; private final ChatRoomCreationService chatRoomCreationService; private final ChatRoomSystemAdminService chatRoomSystemAdminService; private final ChatDirectRoomAccessService chatDirectRoomAccessService; @@ -175,7 +176,7 @@ public ChatMessagePageResponse getMessages( return chatMessageReadService.getClubMessagesByRoom(room, userId, page, limit); } - getAccessibleRoomMember(room, userId); + chatRoomAccessService.getAccessibleMember(room, userId); chatRoomMembershipService.updateLastReadAt(roomId, userId, readAt); recordPresenceSafely(roomId, userId); return chatMessageReadService.getGroupMessagesByRoom(roomId, userId, page, limit); @@ -192,19 +193,7 @@ public ChatMuteResponse toggleMute(Integer userId, Integer roomId) { .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); User user = userRepository.getById(userId); - if (room.isClubGroupRoom()) { - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - } else if (room.isDirectRoom()) { - // 어드민이 SYSTEM_ADMIN 방에 접근하는 경우는 멤버십 체크를 건너뜀 - boolean isAdminAccessingSystemAdminRoom = user.isAdmin() - && chatRoomSystemAdminService.isSystemAdminRoom(room.getId()); - if (!isAdminAccessingSystemAdminRoom) { - chatDirectRoomAccessService.getAccessibleMember(room, user); - } - } else { - getAccessibleRoomMember(room, userId); - } + chatRoomAccessService.ensureMuteAccess(room, user); Boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( NotificationTargetType.CHAT_ROOM, roomId, @@ -233,7 +222,7 @@ public void updateChatRoomName(Integer userId, Integer roomId, ChatRoomNameUpdat ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - ChatRoomMember roomMember = getAccessibleRoomMember(room, userId); + ChatRoomMember roomMember = chatRoomAccessService.getAccessibleMember(room, userId); roomMember.updateCustomRoomName(normalizeCustomRoomName(request.roomName())); } @@ -404,27 +393,6 @@ private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { return ChatRoomMemberLookup.getRoomMember(chatRoomMemberRepository, roomId, userId); } - private ChatRoomMember getAccessibleRoomMember(ChatRoom room, Integer userId) { - if (room.isClubGroupRoom()) { - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - return getRoomMember(room.getId(), userId); - } - - if (room.isDirectRoom()) { - User user = userRepository.getById(userId); - return chatDirectRoomAccessService.getAccessibleMember(room, user); - } - - ChatRoomMember member = getRoomMember(room.getId(), userId); - - if (member.hasLeft()) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - - return member; - } - private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomMembershipServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomMembershipServiceTest.java index c8eed343..41afc706 100644 --- a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomMembershipServiceTest.java +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomMembershipServiceTest.java @@ -418,6 +418,28 @@ void ensureClubRoomMemberCreatesOrUpdatesMemberFromClubMemberBaseline() { verify(chatRoomMemberRepository).save(any(ChatRoomMember.class)); } + @Test + @DisplayName("ensureClubRoomMember는 이미 조회한 club group room을 재조회하지 않고 멤버를 보장한다") + void ensureClubRoomMemberUsesProvidedRoomWithoutRefetch() { + // given + Club club = createClub(10); + ChatRoom room = createRoom(30, ChatType.CLUB_GROUP, LocalDateTime.of(2026, 4, 11, 9, 0)); + ReflectionTestUtils.setField(room, "club", club); + User user = createUser(20, "동아리원", UserRole.USER); + ClubMember clubMember = createClubMember(club, user, LocalDateTime.of(2026, 4, 11, 10, 0)); + + given(clubMemberRepository.getByClubIdAndUserId(club.getId(), user.getId())).willReturn(clubMember); + given(chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId())) + .willReturn(Optional.empty()); + + // when + chatRoomMembershipService.ensureClubRoomMember(room, user.getId()); + + // then + verify(chatRoomRepository, never()).findById(any()); + verify(chatRoomMemberRepository).save(any(ChatRoomMember.class)); + } + @Test @DisplayName("updateLastReadAt는 저장된 값이 더 오래된 경우에만 갱신 쿼리를 위임한다") void updateLastReadAtDelegatesConditionalUpdate() { 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 beb6c962..f8c7e6a6 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 @@ -55,15 +55,16 @@ import gg.agit.konect.domain.chat.service.ChatDirectRoomAccessService; import gg.agit.konect.domain.chat.service.ChatInviteService; import gg.agit.konect.domain.chat.service.ChatMessageReadService; -import gg.agit.konect.domain.chat.service.ChatMessageSendService; import gg.agit.konect.domain.chat.service.ChatMessagePageResolver; +import gg.agit.konect.domain.chat.service.ChatMessageSendService; import gg.agit.konect.domain.chat.service.ChatPresenceService; +import gg.agit.konect.domain.chat.service.ChatRoomAccessService; import gg.agit.konect.domain.chat.service.ChatRoomCreationService; import gg.agit.konect.domain.chat.service.ChatRoomMemberCommandService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; -import gg.agit.konect.domain.chat.service.ChatSearchService; import gg.agit.konect.domain.chat.service.ChatRoomSystemAdminService; +import gg.agit.konect.domain.chat.service.ChatSearchService; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; @@ -142,6 +143,13 @@ void setUp() { clubMemberRepository, chatRoomSystemAdminService ); + ChatRoomAccessService chatRoomAccessService = new ChatRoomAccessService( + chatRoomMemberRepository, + userRepository, + chatRoomMembershipService, + chatRoomSystemAdminService, + chatDirectRoomAccessService + ); ChatRoomMemberCommandService chatRoomMemberCommandService = new ChatRoomMemberCommandService( chatRoomRepository, chatRoomMemberRepository @@ -192,6 +200,7 @@ void setUp() { chatInviteService, chatMessageReadService, chatMessagePageResolver, + chatRoomAccessService, chatRoomCreationService, chatRoomSystemAdminService, chatDirectRoomAccessService, @@ -502,6 +511,7 @@ void toggleMuteTogglesFromUnmutedToMuted() { // then assertThat(response.isMuted()).isTrue(); assertThat(setting.getIsMuted()).isTrue(); + verify(userRepository, times(1)).getById(userId); verify(notificationMuteSettingRepository).save(setting); }