Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import com.assu.server.domain.chat.dto.ChatRequestDTO;
import com.assu.server.domain.chat.dto.ChatResponseDTO;
import com.assu.server.domain.chat.dto.ChatRoomUpdateDTO;
import com.assu.server.domain.chat.repository.MessageRepository;
import com.assu.server.domain.chat.service.ChatService;
import com.assu.server.global.apiPayload.code.status.SuccessStatus;
import com.assu.server.global.util.PresenceTracker;
import com.assu.server.global.util.PrincipalDetails;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
Expand All @@ -24,6 +27,8 @@
public class ChatController {
private final ChatService chatService;
private final SimpMessagingTemplate simpMessagingTemplate;
private final PresenceTracker presenceTracker;
private final MessageRepository messageRepository;

@Operation(
summary = "채팅방을 생성하는 API",
Expand Down Expand Up @@ -61,10 +66,33 @@ public BaseResponse<List<com.assu.server.domain.chat.dto.ChatRoomListResultDTO>>
)
@MessageMapping("/send")
public void handleMessage(@Payload ChatRequestDTO.ChatMessageRequestDTO request) {
log.info("[WS] handleMessage IN: {}", request); // ★ 호출 여부 확인
ChatResponseDTO.SendMessageResponseDTO response = chatService.handleMessage(request);
log.info("[WS] handleMessage SAVED id={}", response.messageId()); // 저장 확인용
simpMessagingTemplate.convertAndSend("/sub/chat/" + request.roomId(), response);
// 먼저 접속 여부 확인 후 unreadCount 계산
boolean receiverInRoom = presenceTracker.isInRoom(request.getReceiverId(), request.getRoomId());
int unreadForSender = receiverInRoom ? 0 : 1;
request.setUnreadCountForSender(unreadForSender);

ChatResponseDTO.SendMessageResponseDTO saved = chatService.handleMessage(request);
simpMessagingTemplate.convertAndSend("/sub/chat/" + request.getRoomId(), saved);

if (!receiverInRoom) {
Long totalUnreadCount = messageRepository.countUnreadMessagesByRoomAndReceiver(
request.getRoomId(),
request.getReceiverId()
);

ChatRoomUpdateDTO updateDTO = ChatRoomUpdateDTO.builder()
.roomId(request.getRoomId())
.lastMessage(saved.message())
.lastMessageTime(saved.sentAt())
.unreadCount(totalUnreadCount)
.build();

simpMessagingTemplate.convertAndSendToUser(
request.getReceiverId().toString(),
"/queue/updates",
updateDTO
);
}
}

@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public static Message toMessageEntity(ChatRequestDTO.ChatMessageRequestDTO reque
.chattingRoom(room)
.sender(sender)
.receiver(receiver)
.message(request.message())
.message(request.getMessage())
.unreadCount(request.getUnreadCountForSender())
.build();
}

Expand All @@ -70,6 +71,7 @@ public static ChatResponseDTO.SendMessageResponseDTO toSendMessageDTO(Message me
.message(message.getMessage())
.sentAt(message.getCreatedAt())
.messageType(message.getType())
.unreadCountForSender(message.getUnreadCount())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class ChatMessageDTO {
private String message;
private LocalDateTime sendTime;

@JsonProperty("unreadCountForSender")
private Integer unreadCount;

@JsonProperty("isRead")
private boolean isRead;

Expand Down
16 changes: 10 additions & 6 deletions src/main/java/com/assu/server/domain/chat/dto/ChatRequestDTO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.assu.server.domain.chat.dto;

import lombok.Getter;
import lombok.Setter;

public class ChatRequestDTO {
@Getter
Expand All @@ -9,10 +10,13 @@ public static class CreateChatRoomRequestDTO {
private Long partnerId;
}

public record ChatMessageRequestDTO(
Long roomId,
Long senderId,
Long receiverId,
String message
) {}
@Getter
@Setter
public static class ChatMessageRequestDTO {
private Long roomId;
private Long senderId;
private Long receiverId;
private String message;
private int unreadCountForSender;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ public record SendMessageResponseDTO(
String message,
MessageType messageType,
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime sentAt
) {}
LocalDateTime sentAt,
Integer unreadCountForSender
) {
public SendMessageResponseDTO withUnreadCountForSender(Integer count) {
return new SendMessageResponseDTO(
messageId, roomId, senderId, receiverId, message, messageType, sentAt, count
);
}
}

// 메시지 읽음 처리
public record ReadMessageResponseDTO(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.assu.server.domain.chat.dto;

import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class ChatRoomUpdateDTO {
private Long roomId;
private String lastMessage;
private LocalDateTime lastMessageTime;
private Long unreadCount; // 해당 채팅방의 총 안읽은 메시지 수
}
3 changes: 3 additions & 0 deletions src/main/java/com/assu/server/domain/chat/entity/Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class Message extends BaseEntity {

private String message;

private Integer unreadCount;

// private LocalDateTime sendTime;
// private LocalDateTime readTime;

Expand All @@ -47,5 +49,6 @@ public class Message extends BaseEntity {

public void markAsRead() {
this.isRead = true;
this.unreadCount = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface MessageRepository extends JpaRepository<Message, Long> {
@Query("""
Expand All @@ -18,13 +17,23 @@ public interface MessageRepository extends JpaRepository<Message, Long> {
""")
List<Message> findUnreadMessagesByRoomAndReceiver(Long roomId, Long receiverId);

@Query("""
SELECT COUNT(m)
FROM Message m
WHERE m.chattingRoom.id = :roomId
AND m.receiver.id = :receiverId
AND m.isRead = false
""")
Long countUnreadMessagesByRoomAndReceiver(@Param("roomId") Long roomId, @Param("receiverId") Long receiverId);


@Query("""
SELECT new com.assu.server.domain.chat.dto.ChatMessageDTO (
m.chattingRoom.id,
m.id,
m.message,
m.createdAt,
m.unreadCount,
m.isRead,
CASE WHEN m.sender.id = :memberId THEN true
ELSE false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,18 @@ public ChatResponseDTO.CreateChatRoomResponseDTO createChatRoom(ChatRequestDTO.C
@Transactional
public ChatResponseDTO.SendMessageResponseDTO handleMessage(ChatRequestDTO.ChatMessageRequestDTO request) {
// 유효성 검사
ChattingRoom room = chatRepository.findById(request.roomId())
ChattingRoom room = chatRepository.findById(request.getRoomId())
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_ROOM));
Member sender = memberRepository.findById(request.senderId())
Member sender = memberRepository.findById(request.getSenderId())
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));
Member receiver = memberRepository.findById(request.receiverId())
Member receiver = memberRepository.findById(request.getReceiverId())
.orElseThrow(() -> new DatabaseException(ErrorStatus.NO_SUCH_MEMBER));

Message message = ChatConverter.toMessageEntity(request, room, sender, receiver);
// messageRepository.save(message);
log.info("saved message start");
Message saved = messageRepository.saveAndFlush(message);
log.info("saved message middle");
log.info("saved message id={}, roomId={}, senderId={}, receiverId={}",
saved.getId(), room.getId(), sender.getId(), receiver.getId());

log.info("saved message end");
boolean exists = messageRepository.existsById(saved.getId());
log.info("Saved? {}", exists); // true 아니면 트랜잭션/DB 문제
return ChatConverter.toSendMessageDTO(saved);
Expand Down
90 changes: 90 additions & 0 deletions src/main/java/com/assu/server/global/util/PresenceTracker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.assu.server.global.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;

import java.security.Principal;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class PresenceTracker {
private final Map<Long, Set<Long>> roomSubscribers = new ConcurrentHashMap<>();
private final Map<String, Long> sessionToMember = new ConcurrentHashMap<>();
private final Map<String, Set<Long>> sessionToRooms = new ConcurrentHashMap<>();

private Long parseRoomId(String dest) { // "/sub/chat/26" -> 26
if (dest == null) return null;
String[] p = dest.split("/");
if (p.length >= 4 && "chat".equals(p[2])) return Long.valueOf(p[3]);
return null;
}

private Long memberIdFrom(Principal user) {
if (user == null) return null;
// StompAuthChannelInterceptor 에서 Principal.name을 memberId로 넣어두었다고 가정
return Long.valueOf(user.getName());
}

@EventListener
public void onSubscribe(SessionSubscribeEvent e) {
var acc = StompHeaderAccessor.wrap(e.getMessage());
Long roomId = parseRoomId(acc.getDestination());
Long memberId = memberIdFrom(e.getUser());
if (roomId == null || memberId == null) return;

String sessionId = acc.getSessionId();
sessionToMember.put(sessionId, memberId);
sessionToRooms.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(roomId);
roomSubscribers.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(memberId);

log.debug("SUB: member {} -> room {}", memberId, roomId);
}

@EventListener
public void onUnsubscribe(SessionUnsubscribeEvent e) {
var acc = StompHeaderAccessor.wrap(e.getMessage());
String sessionId = acc.getSessionId();
var rooms = sessionToRooms.getOrDefault(sessionId, Set.of());
Long memberId = sessionToMember.get(sessionId);
if (memberId != null) {
for (Long roomId : rooms) {
var set = roomSubscribers.get(roomId);
if (set != null) {
set.remove(memberId);
if (set.isEmpty()) roomSubscribers.remove(roomId);
}
}
}
sessionToRooms.remove(sessionId);
log.debug("UNSUB: session {}", sessionId);
}

@EventListener
public void onDisconnect(SessionDisconnectEvent e) {
String sessionId = e.getSessionId();
Long memberId = sessionToMember.remove(sessionId);
var rooms = sessionToRooms.remove(sessionId);
if (memberId != null && rooms != null) {
for (Long roomId : rooms) {
var set = roomSubscribers.get(roomId);
if (set != null) {
set.remove(memberId);
if (set.isEmpty()) roomSubscribers.remove(roomId);
}
}
}
log.debug("DISCONNECT: session {}", sessionId);
}

public boolean isInRoom(Long memberId, Long roomId) {
return roomSubscribers.getOrDefault(roomId, Set.of()).contains(memberId);
}
}