Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -66,29 +66,16 @@ public void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime readA
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateDirectRoomLastReadAt(Integer roomId, Integer userId, LocalDateTime readAt) {
User user = userRepository.getById(userId);
ChatRoom room = chatRoomRepository.findById(roomId)
.orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM));
public void updateDirectRoomLastReadAt(Integer roomId, User user, LocalDateTime readAt, ChatRoom room) {
// 어드민이 SYSTEM_ADMIN 방의 메시지를 읽으면 SYSTEM_ADMIN의 lastReadAt을 업데이트
if (user.getRole() == UserRole.ADMIN && isSystemAdminRoom(roomId)) {
chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, SYSTEM_ADMIN_ID, readAt);
return;
}

ensureDirectRoomMemberExists(room, user, readAt);

if (user.getRole() == UserRole.ADMIN) {
List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
boolean isSystemAdmin = members.stream()
.anyMatch(member -> Objects.equals(member.getUserId(), SYSTEM_ADMIN_ID));

if (isSystemAdmin) {
for (ChatRoomMember member : members) {
if (member.getUser().getRole() == UserRole.ADMIN) {
chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, member.getUserId(), readAt);
}
}
return;
}
}

chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, readAt);
chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, user.getId(), readAt);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
Expand Down Expand Up @@ -145,8 +132,9 @@ private void ensureDirectRoomMemberExists(ChatRoom room, User user, LocalDateTim
return;
}

// 어드민은 SYSTEM_ADMIN 방의 메시지를 조회할 수 있지만, 멤버로 추가되지는 않는다
// (멤버가 추가되면 findByTwoUsers에서 해당 방을 찾지 못해 채팅방이 중복 생성됨)
if (user.getRole() == UserRole.ADMIN && isSystemAdminRoom(room.getId())) {
saveRoomMemberIgnoringDuplicate(room, user, readAt);
return;
Comment thread
dh2906 marked this conversation as resolved.
}

Expand Down
145 changes: 126 additions & 19 deletions src/main/java/gg/agit/konect/domain/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -388,15 +387,23 @@ record SectionKey(Integer clubId, String clubName) {
return ChatInvitableUsersResponse.forClubSort(pagedInvitableUsers, sections);
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional(readOnly = true)
public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) {
ChatRoom room = chatRoomRepository.findById(roomId)
.orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM));
User user = userRepository.getById(userId);

LocalDateTime readAt = LocalDateTime.now();

if (room.isDirectRoom()) {
chatRoomMembershipService.updateDirectRoomLastReadAt(roomId, userId, readAt);
boolean isAdminViewingSystemRoom = user.getRole() == UserRole.ADMIN && isSystemAdminRoom(room);
if (isAdminViewingSystemRoom) {
chatRoomMembershipService.updateLastReadAt(roomId, SYSTEM_ADMIN_ID, readAt);
recordPresenceSafely(roomId, userId);
return getAdminSystemDirectChatRoomMessages(user, room, roomId, page, limit, readAt);
}

chatRoomMembershipService.updateDirectRoomLastReadAt(roomId, user, readAt, room);
recordPresenceSafely(roomId, userId);
return getDirectChatRoomMessages(userId, roomId, page, limit, readAt);
}
Expand Down Expand Up @@ -440,7 +447,12 @@ public ChatMuteResponse toggleMute(Integer userId, Integer roomId) {
ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId);
ensureRoomMember(room, member.getUser(), member.getCreatedAt());
} else if (room.isDirectRoom()) {
getAccessibleDirectRoomMember(room, user);
// 어드민이 SYSTEM_ADMIN 방에 접근하는 경우는 멤버십 체크를 건너뜀
boolean isAdminAccessingSystemAdminRoom = user.getRole() == UserRole.ADMIN
&& isSystemAdminRoom(room);
if (!isAdminAccessingSystemAdminRoom) {
getAccessibleDirectRoomMember(room, user);
}
} else {
getAccessibleRoomMember(room, userId);
}
Expand Down Expand Up @@ -617,24 +629,63 @@ private ChatMessagePageResponse getDirectChatRoomMessages(
) {
ChatRoom chatRoom = getDirectRoom(roomId);
User user = userRepository.getById(userId);
ChatRoomMember member = getOrCreateDirectRoomMember(chatRoom, user);
LocalDateTime visibleMessageFrom = prepareDirectRoomAccess(member, chatRoom);

boolean isAdminViewingSystemRoom = user.getRole() == UserRole.ADMIN && isSystemAdminRoom(chatRoom);
List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
LocalDateTime visibleMessageFrom = prepareDirectRoomAccess(getOrCreateDirectRoomMember(chatRoom, user),
chatRoom);

PageRequest pageable = PageRequest.of(page - 1, limit);
Page<ChatMessage> messages = chatMessageRepository.findByChatRoomId(roomId, visibleMessageFrom, pageable);

List<LocalDateTime> sortedReadBaselines = toSortedReadBaselines(members);

List<ChatMessageDetailResponse> responseMessages = messages.getContent().stream()
.map(message -> {
boolean isRead = message.isSentBy(userId) || !message.getCreatedAt().isAfter(readAt);
int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines);
return new ChatMessageDetailResponse(
message.getId(),
message.getSender().getId(),
null,
message.getContent(),
message.getCreatedAt(),
isRead,
unreadCount,
message.isSentBy(userId)
);
})
.toList();

return new ChatMessagePageResponse(
messages.getTotalElements(),
messages.getNumberOfElements(),
messages.getTotalPages(),
messages.getNumber() + 1,
null,
responseMessages
);
}

private ChatMessagePageResponse getAdminSystemDirectChatRoomMessages(
User user,
ChatRoom chatRoom,
Integer roomId,
Integer page,
Integer limit,
LocalDateTime readAt
) {
List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
LocalDateTime visibleMessageFrom = resolveAdminSystemRoomVisibleMessageFrom(members);

List<LocalDateTime> sortedReadBaselines = isAdminViewingSystemRoom
? toAdminChatReadBaselines(members)
: toSortedReadBaselines(members);
PageRequest pageable = PageRequest.of(page - 1, limit);
Page<ChatMessage> messages = chatMessageRepository.findByChatRoomId(roomId, visibleMessageFrom, pageable);

List<LocalDateTime> sortedReadBaselines = toAdminChatReadBaselines(members);

Integer maskedAdminId = getMaskedAdminId(user, chatRoom);
List<ChatMessageDetailResponse> responseMessages = messages.getContent().stream()
.map(message -> {
Integer senderId = resolveDirectSenderId(message, maskedAdminId);
boolean isMine = shouldDisplayAsOwnMessage(user, message, isAdminViewingSystemRoom);
boolean isMine = shouldDisplayAsOwnMessage(user, message, true);
boolean isRead = isMine || !message.getCreatedAt().isAfter(readAt);
int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines);
return new ChatMessageDetailResponse(
Expand Down Expand Up @@ -667,19 +718,37 @@ private ChatMessageDetailResponse sendDirectMessage(
) {
ChatRoom chatRoom = getDirectRoom(roomId);
User sender = userRepository.getById(userId);
ChatRoomMember senderMember = getAccessibleDirectRoomMember(chatRoom, sender);
boolean senderHadLeft = senderMember.hasLeft();

// 어드민이 SYSTEM_ADMIN 방에 메시지를 보내는 경우
boolean isAdminSendingToSystemAdminRoom = sender.getRole() == UserRole.ADMIN
&& isSystemAdminRoom(chatRoom);

ChatRoomMember senderMember = null;
boolean senderHadLeft = false;

if (!isAdminSendingToSystemAdminRoom) {
senderMember = getAccessibleDirectRoomMember(chatRoom, sender);
senderHadLeft = senderMember.hasLeft();
}

List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
User receiver = resolveDirectChatPartner(members, userId);
User receiver = resolveDirectMessageReceiver(members, sender);

ChatMessage chatMessage = chatMessageRepository.save(
ChatMessage.of(chatRoom, sender, request.content())
);
if (senderHadLeft) {

if (senderHadLeft && senderMember != null) {
senderMember.restoreDirectRoom();
}

chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt());
updateMemberLastReadAt(roomId, userId, chatMessage.getCreatedAt());

// 어드민이 보낸 경우는 lastReadAt 업데이트하지 않음 (멤버가 아니므로)
if (!isAdminSendingToSystemAdminRoom) {
updateMemberLastReadAt(roomId, userId, chatMessage.getCreatedAt());
}

List<LocalDateTime> sortedReadBaselines = toSortedReadBaselines(members);

notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content());
Comment thread
dh2906 marked this conversation as resolved.
Expand Down Expand Up @@ -1170,6 +1239,10 @@ private void ensureRoomMember(ChatRoom room, User user, LocalDateTime 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()) {
Expand All @@ -1184,6 +1257,12 @@ private void ensureDirectRoomRequester(ChatRoom room, User user, LocalDateTime j
}, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt)));
}

private boolean shouldSkipSystemAdminMembership(ChatRoom room, User user) {
// 문의방은 SYSTEM_ADMIN + 일반 사용자 2인 구조를 전제로 재사용(findByTwoUsers)되므로,
// 생성/재오픈 경로에서도 일반 ADMIN을 멤버로 추가하면 안 된다.
return user.getRole() == UserRole.ADMIN && isSystemAdminRoom(room);
}

private String normalizeCustomRoomName(String roomName) {
if (!StringUtils.hasText(roomName)) {
return null;
Expand Down Expand Up @@ -1303,9 +1382,9 @@ private Map<Integer, Integer> getRoomUnreadCountMap(List<Integer> roomIds, Integ
private ChatRoomMember getOrCreateDirectRoomMember(ChatRoom chatRoom, User user) {
return chatRoomMemberRepository.findByChatRoomIdAndUserId(chatRoom.getId(), user.getId())
.orElseGet(() -> {
// 어드민은 SYSTEM_ADMIN 방에 멤버로 추가되지 않음
if (user.getRole() == UserRole.ADMIN && isSystemAdminRoom(chatRoom)) {
LocalDateTime joinedAt = LocalDateTime.now();
return chatRoomMemberRepository.save(ChatRoomMember.of(chatRoom, user, joinedAt));
throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS);
}
throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS);
});
Comment thread
dh2906 marked this conversation as resolved.
Comment thread
dh2906 marked this conversation as resolved.
Expand All @@ -1323,6 +1402,11 @@ private LocalDateTime prepareDirectRoomAccess(ChatRoomMember member, ChatRoom ch
return visibleMessageFrom;
}

private LocalDateTime resolveAdminSystemRoomVisibleMessageFrom(List<ChatRoomMember> members) {
ChatRoomMember systemAdminMember = findRoomMember(members, SYSTEM_ADMIN_ID);
return systemAdminMember != null ? systemAdminMember.getVisibleMessageFrom() : null;
}

/**
* direct 채팅방에서 나간 사용자가 다시 볼 수 있는 상태인지 확인하고,
* 새 메시지가 이미 존재하면 나간 상태를 해제한다.
Expand Down Expand Up @@ -1446,6 +1530,29 @@ private User resolveDirectChatPartner(List<ChatRoomMember> members, Integer user
return findDirectPartner(members, userId);
}

private User findNonAdminUser(List<ChatRoomMember> members) {
return members.stream()
.map(ChatRoomMember::getUser)
.filter(memberUser -> memberUser.getRole() != UserRole.ADMIN)
.findFirst()
.orElse(null);
}

private User resolveDirectMessageReceiver(List<ChatRoomMember> members, User sender) {
if (sender.getRole() == UserRole.ADMIN) {
User nonAdminUser = findNonAdminUser(members);
if (nonAdminUser != null) {
return nonAdminUser;
}
}

User partner = resolveDirectChatPartner(members, sender.getId());
if (partner == null) {
throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS);
}
return partner;
}

private User findDirectPartnerFromMemberInfo(
List<MemberInfo> memberInfos,
Integer userId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- SYSTEM_ADMIN(1번)이 있는 DIRECT 채팅방에서 다른 어드민 멤버십 제거
-- 이유: 어드민이 멤버로 추가되면 findByTwoUsers에서 해당 방을 찾지 못해 중복 생성됨
-- 참고: https://github.com/BCSDLab/KONECT_BACK_END/issues/503

DELETE FROM chat_room_member
WHERE user_id IN (
SELECT u.id
FROM users u
WHERE u.role = 'ADMIN'
AND u.id != 1 -- SYSTEM_ADMIN(1번)은 제외
)
AND chat_room_id IN (
SELECT DISTINCT crm.chat_room_id
FROM chat_room_member crm
JOIN chat_room cr ON crm.chat_room_id = cr.id
WHERE crm.user_id = 1 -- SYSTEM_ADMIN(1번)이 있는 방
AND cr.room_type = 'DIRECT' -- DIRECT 타입 방만
);
Loading
Loading