Skip to content
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
40 changes: 4 additions & 36 deletions src/main/java/gg/agit/konect/domain/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down Expand Up @@ -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()));
}

Expand Down Expand Up @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -142,6 +143,13 @@ void setUp() {
clubMemberRepository,
chatRoomSystemAdminService
);
ChatRoomAccessService chatRoomAccessService = new ChatRoomAccessService(
chatRoomMemberRepository,
userRepository,
chatRoomMembershipService,
chatRoomSystemAdminService,
chatDirectRoomAccessService
);
ChatRoomMemberCommandService chatRoomMemberCommandService = new ChatRoomMemberCommandService(
chatRoomRepository,
chatRoomMemberRepository
Expand Down Expand Up @@ -192,6 +200,7 @@ void setUp() {
chatInviteService,
chatMessageReadService,
chatMessagePageResolver,
chatRoomAccessService,
chatRoomCreationService,
chatRoomSystemAdminService,
chatDirectRoomAccessService,
Expand Down Expand Up @@ -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);
}

Expand Down
Loading