From efc8282f184a7753522f3d806f5460425644eadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:45:28 +0900 Subject: [PATCH 01/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V26__add_unified_chat_room_table.sql | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/main/resources/db/migration/V26__add_unified_chat_room_table.sql diff --git a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql new file mode 100644 index 000000000..4f73712ed --- /dev/null +++ b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql @@ -0,0 +1,192 @@ +ALTER TABLE chat_room + ADD COLUMN club_id INT NULL; + +ALTER TABLE chat_room + ADD CONSTRAINT fk_chat_room_club_id FOREIGN KEY (club_id) REFERENCES club (id) ON DELETE CASCADE; + +CREATE UNIQUE INDEX uq_chat_room_group_club_id + ON chat_room (club_id); + +SET @receiver_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_message' + AND kcu.COLUMN_NAME = 'receiver_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_receiver_fk_sql = IF( + @receiver_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_message DROP FOREIGN KEY `', @receiver_fk_name, '`') +); + +PREPARE drop_receiver_fk_stmt FROM @drop_receiver_fk_sql; +EXECUTE drop_receiver_fk_stmt; +DEALLOCATE PREPARE drop_receiver_fk_stmt; + +ALTER TABLE chat_message + DROP COLUMN receiver_id; + +ALTER TABLE chat_message + DROP COLUMN is_read; + +CREATE TABLE IF NOT EXISTS chat_room_member +( + chat_room_id INT NOT NULL, + user_id INT NOT NULL, + last_read_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (chat_room_id, user_id), + FOREIGN KEY (chat_room_id) REFERENCES chat_room (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE INDEX idx_chat_room_member_user_id + ON chat_room_member (user_id, chat_room_id); + +CREATE INDEX idx_chat_room_member_room_last_read + ON chat_room_member (chat_room_id, last_read_at); + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT cr.id, + cr.sender_id, + COALESCE(cr.last_message_sent_at, cr.created_at), + cr.created_at, + NOW() +FROM chat_room cr +WHERE cr.sender_id IS NOT NULL +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT cr.id, + cr.receiver_id, + COALESCE(cr.last_message_sent_at, cr.created_at), + cr.created_at, + NOW() +FROM chat_room cr +WHERE cr.receiver_id IS NOT NULL +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + +INSERT INTO chat_room ( + sender_id, + receiver_id, + last_message_content, + last_message_sent_at, + club_id, + created_at, + updated_at +) +SELECT NULL, + NULL, + gm.content, + gm.created_at, + gcr.club_id, + gcr.created_at, + gcr.updated_at +FROM group_chat_room gcr + LEFT JOIN ( + SELECT m.room_id, + m.content, + m.created_at + FROM group_chat_message m + JOIN ( + SELECT room_id, MAX(id) AS max_id + FROM group_chat_message + GROUP BY room_id + ) lm ON lm.max_id = m.id +) gm ON gm.room_id = gcr.id + LEFT JOIN chat_room cr ON cr.club_id = gcr.club_id +WHERE cr.id IS NULL; + +INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) +SELECT cr.id, + cm.user_id, + COALESCE(grs.last_read_at, cm.created_at), + cm.created_at, + NOW() +FROM chat_room cr + JOIN club_member cm ON cm.club_id = cr.club_id + LEFT JOIN group_chat_room gcr ON gcr.club_id = cr.club_id + LEFT JOIN group_chat_read_status grs ON grs.room_id = gcr.id + AND grs.user_id = cm.user_id +WHERE cr.club_id IS NOT NULL +ON DUPLICATE KEY UPDATE + last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + +INSERT INTO chat_message ( + chat_room_id, + sender_id, + content, + created_at, + updated_at +) +SELECT cr.id, + gm.sender_id, + gm.content, + gm.created_at, + gm.updated_at +FROM group_chat_message gm + JOIN group_chat_room gcr ON gcr.id = gm.room_id + JOIN chat_room cr ON cr.club_id = gcr.club_id + LEFT JOIN chat_message cm + ON cm.chat_room_id = cr.id + AND cm.sender_id = gm.sender_id + AND cm.content = gm.content + AND cm.created_at = gm.created_at +WHERE cm.id IS NULL; + +UPDATE chat_room cr + JOIN ( + SELECT cm.chat_room_id, + cm.content, + cm.created_at + FROM chat_message cm + JOIN ( + SELECT chat_room_id, MAX(id) AS max_id + FROM chat_message + GROUP BY chat_room_id + ) lm ON lm.max_id = cm.id +) lm ON lm.chat_room_id = cr.id +SET cr.last_message_content = lm.content, + cr.last_message_sent_at = lm.created_at, + cr.updated_at = GREATEST(cr.updated_at, lm.created_at) +WHERE cr.club_id IS NOT NULL; + +INSERT INTO notification_mute_setting (user_id, target_type, target_id, is_muted, created_at, updated_at) +SELECT nms.user_id, + 'CHAT_ROOM', + nms.target_id, + nms.is_muted, + nms.created_at, + nms.updated_at +FROM notification_mute_setting nms + LEFT JOIN notification_mute_setting existed + ON existed.user_id = nms.user_id + AND existed.target_type = 'CHAT_ROOM' + AND existed.target_id = nms.target_id +WHERE nms.target_type = 'DIRECT_CHAT_ROOM' + AND existed.id IS NULL; + +INSERT INTO notification_mute_setting (user_id, target_type, target_id, is_muted, created_at, updated_at) +SELECT nms.user_id, + 'CHAT_ROOM', + cr.id, + nms.is_muted, + nms.created_at, + nms.updated_at +FROM notification_mute_setting nms + JOIN group_chat_room gcr ON gcr.id = nms.target_id + JOIN chat_room cr ON cr.club_id = gcr.club_id + LEFT JOIN notification_mute_setting existed + ON existed.user_id = nms.user_id + AND existed.target_type = 'CHAT_ROOM' + AND existed.target_id = cr.id +WHERE nms.target_type = 'GROUP_CHAT_ROOM' + AND existed.id IS NULL; From ca39fed7e6cfa2d5b3ccb87f7915710a5fe10f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:51:34 +0900 Subject: [PATCH 02/26] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=B0=B8?= =?UTF-8?q?=EA=B0=80=EC=9E=90=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/unified/model/ChatRoomMember.java | 79 +++++++++++++++++ .../chat/unified/model/ChatRoomMemberId.java | 28 +++++++ .../repository/ChatRoomMemberRepository.java | 84 +++++++++++++++++++ .../repository/RoomUnreadCountProjection.java | 8 ++ 4 files changed, 199 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java create mode 100644 src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java create mode 100644 src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java b/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java new file mode 100644 index 000000000..f9fe86bbb --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java @@ -0,0 +1,79 @@ +package gg.agit.konect.domain.chat.unified.model; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +import java.time.LocalDateTime; + +import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.global.model.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "chat_room_member") +@NoArgsConstructor(access = PROTECTED) +public class ChatRoomMember extends BaseEntity { + + @EmbeddedId + private ChatRoomMemberId id; + + @ManyToOne(fetch = LAZY) + @MapsId("chatRoomId") + @JoinColumn(name = "chat_room_id", nullable = false) + private ChatRoom chatRoom; + + @ManyToOne(fetch = LAZY) + @MapsId("userId") + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "last_read_at", nullable = false) + private LocalDateTime lastReadAt; + + @Builder + private ChatRoomMember( + ChatRoomMemberId id, + ChatRoom chatRoom, + User user, + LocalDateTime lastReadAt + ) { + this.id = id; + this.chatRoom = chatRoom; + this.user = user; + this.lastReadAt = lastReadAt; + } + + public static ChatRoomMember of(ChatRoom chatRoom, User user, LocalDateTime lastReadAt) { + return ChatRoomMember.builder() + .id(new ChatRoomMemberId(chatRoom.getId(), user.getId())) + .chatRoom(chatRoom) + .user(user) + .lastReadAt(lastReadAt) + .build(); + } + + public Integer getChatRoomId() { + return id.getChatRoomId(); + } + + public Integer getUserId() { + return id.getUserId(); + } + + public void updateLastReadAt(LocalDateTime lastReadAt) { + if (this.lastReadAt.isBefore(lastReadAt)) { + this.lastReadAt = lastReadAt; + } + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java b/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java new file mode 100644 index 000000000..b3df34693 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java @@ -0,0 +1,28 @@ +package gg.agit.konect.domain.chat.unified.model; + +import java.io.Serial; +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Embeddable +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class ChatRoomMemberId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Column(name = "chat_room_id", nullable = false, updatable = false) + private Integer chatRoomId; + + @Column(name = "user_id", nullable = false, updatable = false) + private Integer userId; +} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java b/src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java new file mode 100644 index 000000000..d597de63c --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java @@ -0,0 +1,84 @@ +package gg.agit.konect.domain.chat.unified.repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; +import gg.agit.konect.domain.chat.unified.model.ChatRoomMemberId; + +public interface ChatRoomMemberRepository extends Repository { + + ChatRoomMember save(ChatRoomMember chatRoomMember); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + Optional findByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + + @Query(""" + SELECT COUNT(crm) > 0 + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + boolean existsByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + """) + List findByChatRoomId(@Param("chatRoomId") Integer chatRoomId); + + @Query(""" + SELECT crm + FROM ChatRoomMember crm + WHERE crm.id.userId = :userId + """) + List findByUserId(@Param("userId") Integer userId); + + @Modifying(clearAutomatically = true) + @Query(""" + UPDATE ChatRoomMember crm + SET crm.lastReadAt = :lastReadAt + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + AND crm.lastReadAt < :lastReadAt + """) + int updateLastReadAtIfOlder( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId, + @Param("lastReadAt") LocalDateTime lastReadAt + ); + + @Query(value = """ + SELECT crm.chat_room_id AS roomId, COUNT(cm.id) AS unreadCount + FROM chat_room_member crm + LEFT JOIN chat_message cm + ON cm.chat_room_id = crm.chat_room_id + AND cm.sender_id <> :userId + AND cm.created_at > crm.last_read_at + WHERE crm.user_id = :userId + AND crm.chat_room_id IN (:roomIds) + GROUP BY crm.chat_room_id + """, nativeQuery = true) + List countUnreadByRoomIdsAndUserId( + @Param("roomIds") List roomIds, + @Param("userId") Integer userId + ); +} diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java b/src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java new file mode 100644 index 000000000..3965c58d2 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java @@ -0,0 +1,8 @@ +package gg.agit.konect.domain.chat.unified.repository; + +public interface RoomUnreadCountProjection { + + Integer getRoomId(); + + Long getUnreadCount(); +} From 95d367350a51303d259a0fc721d6715299ae8825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:52:31 +0900 Subject: [PATCH 03/26] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9/?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20roo?= =?UTF-8?q?m-member=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/direct/model/ChatMessage.java | 21 +----- .../domain/chat/direct/model/ChatRoom.java | 25 ++++++- .../repository/ChatMessageRepository.java | 73 ++++++++++--------- .../direct/repository/ChatRoomRepository.java | 53 +++++++++++--- 4 files changed, 106 insertions(+), 66 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java index d6bf656fb..e31503916 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java @@ -2,7 +2,6 @@ import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; -import static java.lang.Boolean.FALSE; import static lombok.AccessLevel.PROTECTED; import gg.agit.konect.domain.user.model.User; @@ -34,9 +33,6 @@ public class ChatMessage extends BaseEntity { @Column(name = "content", nullable = false, length = 1000) private String content; - @Column(name = "is_read", nullable = false) - private Boolean isRead = FALSE; - @ManyToOne(fetch = LAZY) @JoinColumn(name = "chat_room_id", nullable = false) private ChatRoom chatRoom; @@ -45,34 +41,24 @@ public class ChatMessage extends BaseEntity { @JoinColumn(name = "sender_id", nullable = false) private User sender; - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "receiver_id", nullable = false) - private User receiver; - @Builder private ChatMessage( Integer id, String content, - Boolean isRead, ChatRoom chatRoom, - User sender, - User receiver + User sender ) { this.id = id; this.content = content; - this.isRead = isRead; this.chatRoom = chatRoom; this.sender = sender; - this.receiver = receiver; } - public static ChatMessage of(ChatRoom chatRoom, User sender, User receiver, String content) { + public static ChatMessage of(ChatRoom chatRoom, User sender, String content) { return ChatMessage.builder() .content(content) .chatRoom(chatRoom) .sender(sender) - .receiver(receiver) - .isRead(FALSE) .build(); } @@ -80,7 +66,4 @@ public boolean isSentBy(Integer userId) { return sender.getId().equals(userId); } - public void markAsRead() { - this.isRead = true; - } } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java index 43e295f16..71238e4b8 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java @@ -8,6 +8,7 @@ import java.time.LocalDateTime; +import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.user.enums.UserRole; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.global.exception.CustomException; @@ -48,11 +49,16 @@ public class ChatRoom extends BaseEntity { @JoinColumn(name = "receiver_id") private User receiver; + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "club_id") + private Club club; + @Builder - private ChatRoom(Integer id, User sender, User receiver) { + private ChatRoom(Integer id, User sender, User receiver, Club club) { this.id = id; this.sender = sender; this.receiver = receiver; + this.club = club; } public static ChatRoom of(User sender, User receiver) { @@ -63,6 +69,12 @@ public static ChatRoom of(User sender, User receiver) { .build(); } + public static ChatRoom groupOf(Club club) { + return ChatRoom.builder() + .club(club) + .build(); + } + public static void validateIsNotSameParticipant(User sender, User receiver) { if (sender.getId().equals(receiver.getId())) { throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); @@ -76,6 +88,9 @@ public void validateIsParticipant(Integer userId) { } public boolean isParticipant(Integer userId) { + if (sender == null || receiver == null) { + return false; + } return sender.getId().equals(userId) || receiver.getId().equals(userId); } @@ -83,6 +98,14 @@ public User getChatPartner(User currentUser) { return sender.getId().equals(currentUser.getId()) ? receiver : sender; } + public boolean isDirectRoom() { + return club == null; + } + + public boolean isGroupRoom() { + return club != null; + } + public User getNonAdminUser() { if (sender.getRole() != UserRole.ADMIN) { return sender; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java index 4bb7f84c4..faaf5fe93 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java @@ -18,14 +18,16 @@ public interface ChatMessageRepository extends Repository @Query(""" SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - cm.chatRoom.id, + crm.chatRoomId, COUNT(cm) ) - FROM ChatMessage cm - WHERE cm.chatRoom.id IN :chatRoomIds - AND cm.receiver.id = :receiverId - AND cm.isRead = false - GROUP BY cm.chatRoom.id + FROM ChatRoomMember crm + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.chatRoomId + AND cm.sender.id <> :receiverId + AND cm.createdAt > crm.lastReadAt + WHERE crm.chatRoomId IN :chatRoomIds + AND crm.userId = :receiverId + GROUP BY crm.chatRoomId """) List countUnreadMessagesByChatRoomIdsAndUserId( @Param("chatRoomIds") List chatRoomIds, @@ -41,43 +43,42 @@ List countUnreadMessagesByChatRoomIdsAndUserId( """) Page findByChatRoomId(@Param("chatRoomId") Integer chatRoomId, Pageable pageable); - @Query(""" - SELECT cm - FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - AND cm.receiver.id = :receiverId - AND cm.isRead = false - """) - List findUnreadMessagesByChatRoomIdAndUserId( - @Param("chatRoomId") Integer chatRoomId, - @Param("receiverId") Integer receiverId - ); - - @Query(""" - SELECT cm - FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - AND cm.receiver.role = :adminRole - AND cm.isRead = false - """) - List findUnreadMessagesForAdmin( - @Param("chatRoomId") Integer chatRoomId, - @Param("adminRole") UserRole adminRole - ); - @Query(""" SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - cm.chatRoom.id, + crm.chatRoomId, COUNT(cm) ) - FROM ChatMessage cm - WHERE cm.chatRoom.id IN :chatRoomIds - AND cm.receiver.role = :adminRole - AND cm.isRead = false - GROUP BY cm.chatRoom.id + FROM ChatRoomMember crm + JOIN crm.user u + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.chatRoomId + AND cm.sender.role != :adminRole + AND cm.createdAt > crm.lastReadAt + WHERE crm.chatRoomId IN :chatRoomIds + AND u.role = :adminRole + GROUP BY crm.chatRoomId """) List countUnreadMessagesForAdmin( @Param("chatRoomIds") List chatRoomIds, @Param("adminRole") UserRole adminRole ); + + @Query(""" + SELECT m + FROM ChatMessage m + JOIN FETCH m.sender + WHERE m.id IN ( + SELECT MAX(m2.id) + FROM ChatMessage m2 + WHERE m2.chatRoom.id IN :roomIds + GROUP BY m2.chatRoom.id + ) + """) + List findLatestMessagesByRoomIds(@Param("roomIds") List roomIds); + + @Query(""" + SELECT COUNT(m) + FROM ChatMessage m + WHERE m.chatRoom.id = :chatRoomId + """) + long countByChatRoomId(@Param("chatRoomId") Integer chatRoomId); } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java index 0c7683e6d..85ee7b39e 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java @@ -17,9 +17,12 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT DISTINCT cr FROM ChatRoom cr - JOIN FETCH cr.sender - JOIN FETCH cr.receiver - WHERE cr.sender.id = :userId OR cr.receiver.id = :userId + JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + LEFT JOIN FETCH cr.sender + LEFT JOIN FETCH cr.receiver + LEFT JOIN FETCH cr.club + WHERE crm.userId = :userId + AND cr.club IS NULL ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id """) List findByUserId(@Param("userId") Integer userId); @@ -27,8 +30,9 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT cr FROM ChatRoom cr - JOIN FETCH cr.sender - JOIN FETCH cr.receiver + LEFT JOIN FETCH cr.sender + LEFT JOIN FETCH cr.receiver + LEFT JOIN FETCH cr.club WHERE cr.id = :chatRoomId """) Optional findById(@Param("chatRoomId") Integer chatRoomId); @@ -36,17 +40,20 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT cr FROM ChatRoom cr - WHERE (cr.sender.id = :userId1 AND cr.receiver.id = :userId2) - OR (cr.sender.id = :userId2 AND cr.receiver.id = :userId1) + WHERE cr.club IS NULL + AND ((cr.sender.id = :userId1 AND cr.receiver.id = :userId2) + OR (cr.sender.id = :userId2 AND cr.receiver.id = :userId1)) """) Optional findByTwoUsers(@Param("userId1") Integer userId1, @Param("userId2") Integer userId2); @Query(""" SELECT DISTINCT cr FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r - WHERE s.role = :adminRole OR r.role = :adminRole + WHERE cr.club IS NULL + AND (s.role = :adminRole OR r.role = :adminRole) ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id """) List findAllAdminChatRooms(@Param("adminRole") UserRole adminRole); @@ -54,9 +61,11 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT DISTINCT cr FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r - WHERE (s.role = :adminRole OR r.role = :adminRole) + WHERE cr.club IS NULL + AND (s.role = :adminRole OR r.role = :adminRole) AND EXISTS ( SELECT 1 FROM ChatMessage cm WHERE cm.chatRoom = cr @@ -69,13 +78,37 @@ AND EXISTS ( @Query(""" SELECT cr FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r - WHERE (s.id = :userId AND r.role = :adminRole) + WHERE cr.club IS NULL + AND ((s.id = :userId AND r.role = :adminRole) OR (s.role = :adminRole AND r.id = :userId) + ) """) Optional findByUserIdAndAdminRole( @Param("userId") Integer userId, @Param("adminRole") UserRole adminRole ); + + @Query(""" + SELECT cr + FROM ChatRoom cr + LEFT JOIN FETCH cr.club c + WHERE c.id = :clubId + """) + Optional findByClubId(@Param("clubId") Integer clubId); + + @Query(""" + SELECT DISTINCT cr + FROM ChatRoom cr + JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + LEFT JOIN FETCH cr.club + LEFT JOIN FETCH cr.sender + LEFT JOIN FETCH cr.receiver + WHERE crm.userId = :userId + AND cr.club IS NOT NULL + ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id + """) + List findGroupRoomsByUserId(@Param("userId") Integer userId); } From c14b62ff56a9cc471a64906894cd98606fa8a82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:52:58 +0900 Subject: [PATCH 04/26] =?UTF-8?q?refactor:=201=EB=8C=801=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=80=20lastReadAt?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/direct/dto/ChatMessageResponse.java | 12 +- .../chat/direct/dto/ChatMessagesResponse.java | 26 ++- .../chat/direct/service/ChatService.java | 174 ++++++++++-------- .../domain/user/service/UserService.java | 2 +- 4 files changed, 133 insertions(+), 81 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java index 26c9e3b74..cd8bfec87 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java @@ -30,12 +30,22 @@ public record ChatMessageResponse( Boolean isMine ) { public static ChatMessageResponse from(ChatMessage message, Integer currentUserId) { + return from(message, currentUserId, null); + } + + public static ChatMessageResponse from( + ChatMessage message, + Integer currentUserId, + LocalDateTime lastReadAt + ) { + boolean isRead = message.isSentBy(currentUserId) + || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); return new ChatMessageResponse( message.getId(), message.getSender().getId(), message.getContent(), message.getCreatedAt(), - message.getIsRead(), + isRead, message.isSentBy(currentUserId) ); } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java index 77cfe27d5..1d84009a1 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java @@ -52,16 +52,19 @@ public record InnerChatMessageResponse( public static InnerChatMessageResponse from( ChatMessage message, Integer currentUserId, - Integer maskedAdminId + Integer maskedAdminId, + LocalDateTime lastReadAt ) { Integer senderId = resolveSenderId(message, maskedAdminId); + boolean isMine = message.isSentBy(currentUserId); + boolean isRead = isMine || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); return new InnerChatMessageResponse( message.getId(), senderId, message.getContent(), message.getCreatedAt(), - message.getIsRead(), - message.isSentBy(currentUserId) + isRead, + isMine ); } @@ -76,7 +79,8 @@ private static Integer resolveSenderId(ChatMessage message, Integer maskedAdminI public static ChatMessagesResponse from( Page page, Integer currentUserId, - Integer maskedAdminId + Integer maskedAdminId, + LocalDateTime lastReadAt ) { return new ChatMessagesResponse( page.getTotalElements(), @@ -84,7 +88,19 @@ public static ChatMessagesResponse from( page.getTotalPages(), page.getNumber() + 1, page.stream() - .map(message -> InnerChatMessageResponse.from(message, currentUserId, maskedAdminId)) + .map(message -> { + Integer senderId = InnerChatMessageResponse.resolveSenderId(message, maskedAdminId); + boolean isMine = message.isSentBy(currentUserId); + boolean isRead = isMine || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); + return new InnerChatMessageResponse( + message.getId(), + senderId, + message.getContent(), + message.getCreatedAt(), + isRead, + isMine + ); + }) .toList() ); } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java index ad4125cfc..e4881937f 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java @@ -1,8 +1,10 @@ package gg.agit.konect.domain.chat.direct.service; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; +import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashSet; @@ -30,6 +32,8 @@ import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; +import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; +import gg.agit.konect.domain.chat.unified.repository.ChatRoomMemberRepository; import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; import gg.agit.konect.domain.notification.service.NotificationService; import gg.agit.konect.domain.user.enums.UserRole; @@ -45,6 +49,7 @@ public class ChatService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final NotificationService notificationService; @@ -60,10 +65,11 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat } ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), targetUser.getId()) - .orElseGet(() -> { - ChatRoom newChatRoom = ChatRoom.of(currentUser, targetUser); - return chatRoomRepository.save(newChatRoom); - }); + .orElseGet(() -> chatRoomRepository.save(ChatRoom.of(currentUser, targetUser))); + + LocalDateTime joinedAt = chatRoom.getCreatedAt() != null ? chatRoom.getCreatedAt() : LocalDateTime.now(); + ensureRoomMember(chatRoom, currentUser, joinedAt); + ensureRoomMember(chatRoom, targetUser, joinedAt); return ChatRoomResponse.from(chatRoom); } @@ -73,22 +79,17 @@ public ChatRoomsResponse getChatRooms(Integer userId) { List chatRoomResponses = new ArrayList<>(); List personalChatRooms = chatRoomRepository.findByUserId(userId); - Map personalUnreadCountMap = getUnreadCountMap( - extractChatRoomIds(personalChatRooms), userId - ); + Map personalUnreadCountMap = getUnreadCountMap(extractChatRoomIds(personalChatRooms), userId); Set addedChatRoomIds = new HashSet<>(); if (user.getRole() == UserRole.ADMIN) { List adminChatRooms = chatRoomRepository.findAdminChatRoomsWithUserReply(UserRole.ADMIN); - Map adminUnreadCountMap = getAdminUnreadCountMap( - extractChatRoomIds(adminChatRooms) - ); + Map adminUnreadCountMap = getAdminUnreadCountMap(extractChatRoomIds(adminChatRooms)); for (ChatRoom chatRoom : adminChatRooms) { - chatRoomResponses.add(ChatRoomsResponse.InnerChatRoomResponse.fromForAdmin( - chatRoom, adminUnreadCountMap - )); - + chatRoomResponses.add( + ChatRoomsResponse.InnerChatRoomResponse.fromForAdmin(chatRoom, adminUnreadCountMap) + ); addedChatRoomIds.add(chatRoom.getId()); } } @@ -96,20 +97,71 @@ public ChatRoomsResponse getChatRooms(Integer userId) { for (ChatRoom chatRoom : personalChatRooms) { if (!addedChatRoomIds.contains(chatRoom.getId())) { ChatRoomType type = isAdminChatRoom(chatRoom) ? ChatRoomType.ADMIN : ChatRoomType.NORMAL; - chatRoomResponses.add(ChatRoomsResponse.InnerChatRoomResponse.from( - chatRoom, user, personalUnreadCountMap, type - )); + chatRoomResponses.add( + ChatRoomsResponse.InnerChatRoomResponse.from(chatRoom, user, personalUnreadCountMap, type) + ); } } chatRoomResponses.sort(Comparator - .comparing(ChatRoomsResponse.InnerChatRoomResponse::lastSentTime, - Comparator.nullsLast(Comparator.reverseOrder())) + .comparing( + ChatRoomsResponse.InnerChatRoomResponse::lastSentTime, + Comparator.nullsLast(Comparator.reverseOrder()) + ) .thenComparing(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId)); return ChatRoomsResponse.of(chatRoomResponses); } + @Transactional + public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + ChatRoom chatRoom = getDirectRoom(roomId); + User user = userRepository.getById(userId); + validateChatRoomAccess(userId, chatRoom); + + ChatRoomMember member = getRoomMember(roomId, userId); + LocalDateTime readAt = LocalDateTime.now(); + chatPresenceService.recordPresence(roomId, userId); + member.updateLastReadAt(readAt); + + PageRequest pageable = PageRequest.of(page - 1, limit); + Page messages = chatMessageRepository.findByChatRoomId(roomId, pageable); + + Integer maskedAdminId = getMaskedAdminId(user, chatRoom); + return ChatMessagesResponse.from(messages, userId, maskedAdminId, readAt); + } + + @Transactional + public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + ChatRoom chatRoom = getDirectRoom(roomId); + User sender = userRepository.getById(userId); + validateChatRoomAccess(userId, chatRoom); + + User receiver = getMessageReceiver(sender, chatRoom); + + ChatMessage chatMessage = chatMessageRepository.save( + ChatMessage.of(chatRoom, sender, request.content()) + ); + chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); + updateMemberLastReadAt(roomId, userId, chatMessage.getCreatedAt()); + + notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); + publishAdminChatEventIfNeeded(receiver, sender, request.content()); + + return ChatMessageResponse.from(chatMessage, userId); + } + + private ChatRoom getDirectRoom(Integer roomId) { + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (!chatRoom.isDirectRoom()) { + throw CustomException.of(NOT_FOUND_CHAT_ROOM); + } + + return chatRoom; + } + private List extractChatRoomIds(List chatRooms) { return chatRooms.stream() .map(ChatRoom::getId) @@ -122,7 +174,8 @@ private Map getUnreadCountMap(List chatRoomIds, Integ } List unreadMessageCounts = chatMessageRepository.countUnreadMessagesByChatRoomIdsAndUserId( - chatRoomIds, userId + chatRoomIds, + userId ); return unreadMessageCounts.stream() @@ -138,7 +191,8 @@ private Map getAdminUnreadCountMap(List chatRoomIds) } List unreadMessageCounts = chatMessageRepository.countUnreadMessagesForAdmin( - chatRoomIds, UserRole.ADMIN + chatRoomIds, + UserRole.ADMIN ); return unreadMessageCounts.stream() @@ -148,23 +202,6 @@ private Map getAdminUnreadCountMap(List chatRoomIds) )); } - @Transactional - public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, Integer page, Integer limit) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User user = userRepository.getById(userId); - validateChatRoomAccess(user, chatRoom); - - chatPresenceService.recordPresence(roomId, userId); - markUnreadMessagesAsRead(user, roomId, chatRoom); - - PageRequest pageable = PageRequest.of(page - 1, limit); - Page messages = chatMessageRepository.findByChatRoomId(roomId, pageable); - - Integer maskedAdminId = getMaskedAdminId(user, chatRoom); - return ChatMessagesResponse.from(messages, userId, maskedAdminId); - } - private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { if (user.getRole() == UserRole.ADMIN || !isAdminChatRoom(chatRoom)) { return null; @@ -179,48 +216,15 @@ private User getAdminFromChatRoom(ChatRoom chatRoom) { return chatRoom.getReceiver(); } - private void markUnreadMessagesAsRead(User user, Integer roomId, ChatRoom chatRoom) { - List unreadMessages; - - if (user.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - unreadMessages = chatMessageRepository.findUnreadMessagesForAdmin(roomId, UserRole.ADMIN); - } else { - unreadMessages = chatMessageRepository.findUnreadMessagesByChatRoomIdAndUserId(roomId, user.getId()); - } - - unreadMessages.forEach(ChatMessage::markAsRead); - } - private boolean isAdminChatRoom(ChatRoom chatRoom) { return chatRoom.getSender().getRole() == UserRole.ADMIN || chatRoom.getReceiver().getRole() == UserRole.ADMIN; } - private void validateChatRoomAccess(User user, ChatRoom chatRoom) { - if (user.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - return; + private void validateChatRoomAccess(Integer userId, ChatRoom chatRoom) { + if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(chatRoom.getId(), userId)) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); } - chatRoom.validateIsParticipant(user.getId()); - } - - @Transactional - public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User sender = userRepository.getById(userId); - validateChatRoomAccess(sender, chatRoom); - - User receiver = getMessageReceiver(sender, chatRoom); - - ChatMessage chatMessage = chatMessageRepository.save( - ChatMessage.of(chatRoom, sender, receiver, request.content()) - ); - chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); - - notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); - publishAdminChatEventIfNeeded(receiver, sender, request.content()); - - return ChatMessageResponse.from(chatMessage, userId); } private User getMessageReceiver(User sender, ChatRoom chatRoom) { @@ -235,4 +239,26 @@ private void publishAdminChatEventIfNeeded(User receiver, User sender, String co eventPublisher.publishEvent(AdminChatReceivedEvent.of(sender.getId(), sender.getName(), content)); } } + + private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { + return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) + .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); + } + + private void ensureRoomMember(ChatRoom room, User user, LocalDateTime lastReadAt) { + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> member.updateLastReadAt(lastReadAt), () -> { + chatRoomMemberRepository.save(ChatRoomMember.of(room, user, lastReadAt)); + }); + } + + private void updateMemberLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + User user = userRepository.getById(userId); + ensureRoomMember(room, user, lastReadAt); + } + } } diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index 6fcdaaa5e..fac25d75b 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -119,7 +119,7 @@ private void sendWelcomeMessage(User newUser) { .orElseGet(() -> chatRoomRepository.save(ChatRoom.of(operator, newUser))); ChatMessage chatMessage = chatMessageRepository.save( - ChatMessage.of(chatRoom, operator, newUser, DEFAULT_WELCOME_MESSAGE) + ChatMessage.of(chatRoom, operator, DEFAULT_WELCOME_MESSAGE) ); chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt()); From 10b4ec957ce4f1fa904c8ed3743e2c3b724eaff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:53:24 +0900 Subject: [PATCH 05/26] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=99=80=20DTO?= =?UTF-8?q?=EB=A5=BC=20chat=5Froom=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/dto/GroupChatRoomsResponse.java | 12 +- .../chat/group/service/GroupChatService.java | 304 +++++++++--------- .../domain/club/service/ClubService.java | 8 +- 3 files changed, 158 insertions(+), 166 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java index e308d2334..bad414b61 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java @@ -9,8 +9,8 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; +import gg.agit.konect.domain.chat.direct.model.ChatMessage; +import gg.agit.konect.domain.chat.direct.model.ChatRoom; import io.swagger.v3.oas.annotations.media.Schema; public record GroupChatRoomsResponse( @@ -38,8 +38,8 @@ public record InnerGroupChatRoomResponse( Integer unreadMessageCount ) { public static InnerGroupChatRoomResponse from( - GroupChatRoom room, - GroupChatMessage lastMessage, + ChatRoom room, + ChatMessage lastMessage, Map unreadCountMap ) { return new InnerGroupChatRoomResponse( @@ -54,8 +54,8 @@ public static InnerGroupChatRoomResponse from( } public static GroupChatRoomsResponse from( - List rooms, - Map lastMessageMap, + List rooms, + Map lastMessageMap, Map unreadCountMap ) { return new GroupChatRoomsResponse( diff --git a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java b/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java index 85f4f0252..38362f26d 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java @@ -1,7 +1,7 @@ package gg.agit.konect.domain.chat.group.service; import java.time.LocalDateTime; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -12,27 +12,25 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.chat.direct.model.ChatMessage; import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; import gg.agit.konect.domain.chat.group.dto.GroupChatRoomResponse; import gg.agit.konect.domain.chat.group.dto.GroupChatRoomsResponse; -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatus; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.domain.chat.group.repository.GroupChatMessageRepository; -import gg.agit.konect.domain.chat.group.repository.GroupChatReadStatusRepository; -import gg.agit.konect.domain.chat.group.repository.GroupChatRoomRepository; -import gg.agit.konect.domain.chat.group.repository.GroupRoomUnreadCountProjection; -import gg.agit.konect.domain.chat.unified.enums.ChatType; +import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; +import gg.agit.konect.domain.chat.unified.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.unified.repository.RoomUnreadCountProjection; import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; +import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.repository.ClubMemberRepository; -import gg.agit.konect.domain.notification.service.NotificationService; import gg.agit.konect.domain.notification.enums.NotificationTargetType; import gg.agit.konect.domain.notification.model.NotificationMuteSetting; import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; +import gg.agit.konect.domain.notification.service.NotificationService; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.domain.user.repository.UserRepository; import gg.agit.konect.global.code.ApiResponseCode; @@ -44,30 +42,47 @@ @Transactional(readOnly = true) public class GroupChatService { - private final GroupChatRoomRepository groupChatRoomRepository; - private final GroupChatMessageRepository groupChatMessageRepository; - private final GroupChatReadStatusRepository groupChatReadStatusRepository; - private final NotificationMuteSettingRepository notificationMuteSettingRepository; private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final NotificationMuteSettingRepository notificationMuteSettingRepository; private final ClubMemberRepository clubMemberRepository; private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final NotificationService notificationService; + @Transactional public GroupChatRoomResponse getGroupChatRoom(Integer clubId, Integer userId) { - validateClubMember(clubId, userId); - - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(clubId, userId); + ChatRoom room = resolveOrCreateGroupRoom(member.getClub()); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); return new GroupChatRoomResponse(room.getId()); } + @Transactional public GroupChatRoomsResponse getChatRooms(Integer userId) { - List rooms = groupChatRoomRepository.findAllByUserId(userId); - List roomIds = rooms.stream() - .map(GroupChatRoom::getId) + List memberships = clubMemberRepository.findAllByUserId(userId); + if (memberships.isEmpty()) { + return GroupChatRoomsResponse.from(List.of(), Map.of(), Map.of()); + } + + Map membershipByClubId = memberships.stream() + .collect(Collectors.toMap(cm -> cm.getClub().getId(), cm -> cm, (a, b) -> a)); + + List rooms = memberships.stream() + .map(ClubMember::getClub) + .map(this::resolveOrCreateGroupRoom) .toList(); - Map lastMessageMap = getLastMessageMap(roomIds); + for (ChatRoom room : rooms) { + ClubMember member = membershipByClubId.get(room.getClub().getId()); + if (member != null) { + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } + } + + List roomIds = rooms.stream().map(ChatRoom::getId).toList(); + Map lastMessageMap = getLastMessageMap(roomIds); Map unreadCountMap = getUnreadCountMap(roomIds, userId); return GroupChatRoomsResponse.from(rooms, lastMessageMap, unreadCountMap); @@ -75,140 +90,97 @@ public GroupChatRoomsResponse getChatRooms(Integer userId) { @Transactional public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer userId, Integer page, Integer limit) { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - return getMessages(room.getClub().getId(), userId, page, limit); - } - - @Transactional - public GroupChatMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - return sendMessage(room.getClub().getId(), userId, content); - } - - @Transactional - public GroupChatMessagesResponse getMessages(Integer clubId, Integer userId, Integer page, Integer limit) { - validateClubMember(clubId, userId); - - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); - Integer roomId = room.getId(); + ChatRoom room = getGroupRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + syncGroupMembers(room); chatPresenceService.recordPresence(roomId, userId); - updateLastReadAt(roomId, userId, LocalDateTime.now()); PageRequest pageable = PageRequest.of(page - 1, limit); - long totalCount = groupChatMessageRepository.countByRoomId(roomId); - Page messagePage = groupChatMessageRepository - .findByRoomIdOrderByCreatedAtDesc(roomId, pageable); - List messages = messagePage.getContent(); - - List members = clubMemberRepository.findAllByClubId(clubId); - Map lastReadAtMap = groupChatReadStatusRepository.findByRoomId(roomId).stream() - .collect(java.util.stream.Collectors.toMap( - GroupChatReadStatus::getUserId, - GroupChatReadStatus::getLastReadAt - )); + long totalCount = chatMessageRepository.countByChatRoomId(roomId); + Page messagePage = chatMessageRepository.findByChatRoomId(roomId, pageable); + List messages = messagePage.getContent(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); List responseMessages = messages.stream() .map(message -> { - Integer messageId = message.getId(); - Integer senderId = message.getSender().getId(); - String senderName = message.getSender().getName(); - String messageContent = message.getContent(); - LocalDateTime createdAt = message.getCreatedAt(); - - int unreadCount = getUnreadCount(message, members, lastReadAtMap); - boolean isMine = senderId.equals(userId); - + int unreadCount = getUnreadCount(message, members); return new GroupChatMessageResponse( - messageId, - senderId, - senderName, - messageContent, - createdAt, + message.getId(), + message.getSender().getId(), + message.getSender().getName(), + message.getContent(), + message.getCreatedAt(), unreadCount, - isMine + message.isSentBy(userId) ); }) .toList(); int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new GroupChatMessagesResponse( totalCount, responseMessages.size(), totalPage, page, - clubId, + room.getClub().getId(), responseMessages ); } @Transactional - public GroupChatMessageResponse sendMessage(Integer clubId, Integer userId, String content) { - validateClubMember(clubId, userId); + public GroupChatMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { + ChatRoom room = getGroupRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + User sender = member.getUser(); - GroupChatRoom room = groupChatRoomRepository.getByClubId(clubId); - Integer roomId = room.getId(); - User sender = userRepository.getById(userId); + ensureRoomMember(room, sender, member.getCreatedAt()); + syncGroupMembers(room); - GroupChatMessage message = groupChatMessageRepository.save(GroupChatMessage.of(room, sender, content)); + ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); + room.updateLastMessage(message.getContent(), message.getCreatedAt()); updateLastReadAt(roomId, userId, message.getCreatedAt()); - Integer messageId = message.getId(); - Integer senderId = sender.getId(); - String senderName = sender.getName(); - String clubName = room.getClub().getName(); - String messageContent = message.getContent(); - LocalDateTime createdAt = message.getCreatedAt(); - - List recipientUserIds = clubMemberRepository.findUserIdsByClubId(clubId); - Set mutedUserIds = getMutedUserIds(roomId); - List filteredRecipients = recipientUserIds.stream() - .filter(recipientId -> !mutedUserIds.contains(recipientId)) - .toList(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); notificationService.sendGroupChatNotification( roomId, - senderId, - clubName, - senderName, - messageContent, - filteredRecipients + sender.getId(), + room.getClub().getName(), + sender.getName(), + message.getContent(), + recipientUserIds ); - int memberCount = recipientUserIds.size(); - int unreadCount = memberCount - 1; return new GroupChatMessageResponse( - messageId, - senderId, - senderName, - messageContent, - createdAt, - unreadCount, + message.getId(), + sender.getId(), + sender.getName(), + message.getContent(), + message.getCreatedAt(), + getUnreadCount(message, members), true ); } @Transactional - public Boolean toggleMute(Integer userId, ChatType type, Integer chatRoomId) { - NotificationTargetType targetType; - - if (type == ChatType.DIRECT) { - ChatRoom directRoom = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); - directRoom.validateIsParticipant(userId); - targetType = NotificationTargetType.DIRECT_CHAT_ROOM; + public Boolean toggleMute(Integer userId, Integer chatRoomId) { + ChatRoom room = chatRoomRepository.findById(chatRoomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + + if (room.isGroupRoom()) { + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); } else { - GroupChatRoom groupRoom = groupChatRoomRepository.getById(chatRoomId); - validateClubMember(groupRoom.getClub().getId(), userId); - targetType = NotificationTargetType.GROUP_CHAT_ROOM; + validateDirectAccess(room, userId); } User user = userRepository.getById(userId); - return notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( - targetType, + NotificationTargetType.CHAT_ROOM, chatRoomId, userId ) @@ -219,7 +191,7 @@ public Boolean toggleMute(Integer userId, ChatType type, Integer chatRoomId) { }) .orElseGet(() -> { notificationMuteSettingRepository.save(NotificationMuteSetting.of( - targetType, + NotificationTargetType.CHAT_ROOM, chatRoomId, user, true @@ -228,67 +200,75 @@ public Boolean toggleMute(Integer userId, ChatType type, Integer chatRoomId) { }); } - private void validateClubMember(Integer clubId, Integer userId) { - if (!clubMemberRepository.existsByClubIdAndUserId(clubId, userId)) { - throw CustomException.of(ApiResponseCode.FORBIDDEN_GROUP_CHAT_ACCESS); + private ChatRoom getGroupRoom(Integer roomId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + if (!room.isGroupRoom() || room.getClub() == null) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM); } + return room; } - private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - groupChatReadStatusRepository.findByRoomIdAndUserId(roomId, userId) - .ifPresentOrElse(status -> { - status.updateLastReadAt(lastReadAt); - }, () -> { - GroupChatRoom room = groupChatRoomRepository.getById(roomId); - User user = userRepository.getById(userId); - groupChatReadStatusRepository.save(GroupChatReadStatus.of(room, user, lastReadAt)); - }); + private ChatRoom resolveOrCreateGroupRoom(Club club) { + return chatRoomRepository.findByClubId(club.getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); } - private Set getMutedUserIds(Integer roomId) { - List mutedUserIds = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndIsMutedTrue( - NotificationTargetType.GROUP_CHAT_ROOM, - roomId - ) - .stream() - .map(setting -> setting.getUser().getId()) - .toList(); + private void syncGroupMembers(ChatRoom room) { + List clubMembers = clubMemberRepository.findAllByClubId(room.getClub().getId()); + for (ClubMember clubMember : clubMembers) { + ensureRoomMember(room, clubMember.getUser(), clubMember.getCreatedAt()); + } + } + + private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + if (member.getLastReadAt().isBefore(joinedAt)) { + member.updateLastReadAt(joinedAt); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); + } - return new HashSet<>(mutedUserIds); + private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = getGroupRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } } - private int getUnreadCount( - GroupChatMessage message, - List members, - Map lastReadAtMap - ) { - LocalDateTime messageCreatedAt = message.getCreatedAt(); + private void validateDirectAccess(ChatRoom room, Integer userId) { + if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(room.getId(), userId)) { + throw CustomException.of(ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS); + } + } + + private int getUnreadCount(ChatMessage message, List members) { + LocalDateTime createdAt = message.getCreatedAt(); int unreadCount = 0; - for (ClubMember member : members) { - if (member.getCreatedAt().isAfter(messageCreatedAt)) { - continue; + for (ChatRoomMember member : members) { + LocalDateTime baseline = member.getLastReadAt(); + if (baseline == null || baseline.isBefore(member.getCreatedAt())) { + baseline = member.getCreatedAt(); } - - LocalDateTime lastReadAt = lastReadAtMap.getOrDefault(member.getUser().getId(), member.getCreatedAt()); - if (lastReadAt.isBefore(messageCreatedAt)) { - unreadCount++; + if (baseline.isBefore(createdAt)) { + unreadCount += 1; } } return unreadCount; } - private Map getLastMessageMap(List roomIds) { + private Map getLastMessageMap(List roomIds) { if (roomIds.isEmpty()) { return Map.of(); } - return groupChatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() - .collect(Collectors.toMap( - message -> message.getRoom().getId(), - message -> message - )); + return chatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() + .collect(Collectors.toMap(message -> message.getChatRoom().getId(), message -> message)); } private Map getUnreadCountMap(List roomIds, Integer userId) { @@ -296,10 +276,22 @@ private Map getUnreadCountMap(List roomIds, Integer u return Map.of(); } - return groupChatMessageRepository.countUnreadByRoomIdsAndUserId(roomIds, userId).stream() - .collect(Collectors.toMap( - GroupRoomUnreadCountProjection::getRoomId, - unreadCount -> unreadCount.getUnreadCount().intValue() - )); + Map unreadCountMap = new HashMap<>(); + List projections = chatRoomMemberRepository.countUnreadByRoomIdsAndUserId( + roomIds, + userId + ); + for (RoomUnreadCountProjection projection : projections) { + unreadCountMap.put(projection.getRoomId(), projection.getUnreadCount().intValue()); + } + + Set existingRoomIds = unreadCountMap.keySet(); + for (Integer roomId : roomIds) { + if (!existingRoomIds.contains(roomId)) { + unreadCountMap.put(roomId, 0); + } + } + + return unreadCountMap; } } diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index b9c47a1aa..91055ec85 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -14,8 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.domain.chat.group.repository.GroupChatRoomRepository; +import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; import gg.agit.konect.domain.club.dto.ClubBasicInfoUpdateRequest; import gg.agit.konect.domain.club.dto.ClubCondition; import gg.agit.konect.domain.club.dto.ClubCreateRequest; @@ -51,7 +51,7 @@ public class ClubService { private final ClubApplyRepository clubApplyRepository; private final UserRepository userRepository; private final ClubPermissionValidator clubPermissionValidator; - private final GroupChatRoomRepository groupChatRoomRepository; + private final ChatRoomRepository chatRoomRepository; public ClubsResponse getClubs(ClubCondition condition, Integer userId) { User user = userRepository.getById(userId); @@ -107,7 +107,7 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) Club savedClub = clubRepository.save(club); - groupChatRoomRepository.save(GroupChatRoom.of(savedClub)); + chatRoomRepository.save(ChatRoom.groupOf(savedClub)); ClubMember president = ClubMember.builder() .club(savedClub) From 2f34c7bd5b6853fd49409e29a6186cabe553ea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:53:41 +0900 Subject: [PATCH 06/26] =?UTF-8?q?refactor:=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20API=20=ED=9D=90=EB=A6=84=EC=9D=84=20?= =?UTF-8?q?=EB=8B=A8=EC=9D=BC=20roomId=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/unified/controller/ChatApi.java | 4 - .../unified/controller/ChatController.java | 16 +-- .../unified/service/UnifiedChatService.java | 116 ++++++++++-------- 3 files changed, 67 insertions(+), 69 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java b/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java index 478eff8d5..13fd5a8a6 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java +++ b/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java @@ -15,7 +15,6 @@ import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -75,7 +74,6 @@ ResponseEntity getChatRooms( """) @GetMapping("/rooms/{chatRoomId}") ResponseEntity getChatRoomMessages( - @RequestParam(name = "type") ChatType type, @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, @PathVariable(value = "chatRoomId") Integer chatRoomId, @@ -98,7 +96,6 @@ ResponseEntity getChatRoomMessages( """) @PostMapping("/rooms/{chatRoomId}/messages") ResponseEntity sendMessage( - @RequestParam(name = "type") ChatType type, @PathVariable(value = "chatRoomId") Integer chatRoomId, @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId @@ -107,7 +104,6 @@ ResponseEntity sendMessage( @Operation(summary = "채팅 알림 기능 유무를 토글한다.") @PostMapping("/rooms/{chatRoomId}/mute") ResponseEntity toggleGroupChatMute( - @RequestParam(name = "type") ChatType type, @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java index 4be8177c8..4879e0df2 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java +++ b/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java @@ -10,13 +10,11 @@ import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.direct.service.ChatService; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMuteResponse; import gg.agit.konect.domain.chat.group.service.GroupChatService; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; import gg.agit.konect.domain.chat.unified.service.UnifiedChatService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; @@ -27,7 +25,6 @@ @RequestMapping("/chats") public class ChatController implements ChatApi { - private final ChatService chatService; private final GroupChatService groupChatService; private final UnifiedChatService unifiedChatService; @@ -36,7 +33,7 @@ public ResponseEntity createOrGetChatRoom( @Valid @RequestBody ChatRoomCreateRequest request, @UserId Integer userId ) { - ChatRoomResponse response = chatService.createOrGetChatRoom(userId, request); + ChatRoomResponse response = unifiedChatService.createOrGetChatRoom(userId, request); return ResponseEntity.ok(response); } @@ -50,34 +47,31 @@ public ResponseEntity getChatRooms( @Override public ResponseEntity getChatRoomMessages( - @RequestParam(name = "type") ChatType type, @RequestParam(name = "page", defaultValue = "1") Integer page, - @RequestParam(name = "limit", defaultValue = "30", required = false) Integer limit, + @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - UnifiedChatMessagesResponse response = unifiedChatService.getMessages(userId, type, chatRoomId, page, limit); + UnifiedChatMessagesResponse response = unifiedChatService.getMessages(userId, chatRoomId, page, limit); return ResponseEntity.ok(response); } @Override public ResponseEntity sendMessage( - @RequestParam(name = "type") ChatType type, @PathVariable(value = "chatRoomId") Integer chatRoomId, @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId ) { - UnifiedChatMessageResponse response = unifiedChatService.sendMessage(userId, type, chatRoomId, request); + UnifiedChatMessageResponse response = unifiedChatService.sendMessage(userId, chatRoomId, request); return ResponseEntity.ok(response); } @Override public ResponseEntity toggleGroupChatMute( - @RequestParam(name = "type") ChatType type, @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - Boolean isMuted = groupChatService.toggleMute(userId, type, chatRoomId); + Boolean isMuted = groupChatService.toggleMute(userId, chatRoomId); return ResponseEntity.ok(new UnifiedChatMuteResponse(isMuted)); } } diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java b/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java index ddbf577bb..2c74ffe70 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java @@ -1,5 +1,7 @@ package gg.agit.konect.domain.chat.unified.service; +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; + import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -11,7 +13,10 @@ import gg.agit.konect.domain.chat.direct.dto.ChatMessageResponse; import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.direct.dto.ChatMessagesResponse; +import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.direct.dto.ChatRoomsResponse; +import gg.agit.konect.domain.chat.direct.model.ChatRoom; import gg.agit.konect.domain.chat.direct.service.ChatService; import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; @@ -22,37 +27,51 @@ import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomResponse; import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; import gg.agit.konect.domain.chat.unified.enums.ChatType; +import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; import gg.agit.konect.domain.notification.enums.NotificationTargetType; import gg.agit.konect.domain.notification.model.NotificationMuteSetting; import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; -import lombok.RequiredArgsConstructor; +import gg.agit.konect.global.exception.CustomException; @Service -@RequiredArgsConstructor public class UnifiedChatService { private final ChatService chatService; private final GroupChatService groupChatService; + private final ChatRoomRepository chatRoomRepository; private final NotificationMuteSettingRepository notificationMuteSettingRepository; + public UnifiedChatService( + ChatService chatService, + GroupChatService groupChatService, + ChatRoomRepository chatRoomRepository, + NotificationMuteSettingRepository notificationMuteSettingRepository + ) { + this.chatService = chatService; + this.groupChatService = groupChatService; + this.chatRoomRepository = chatRoomRepository; + this.notificationMuteSettingRepository = notificationMuteSettingRepository; + } + + public ChatRoomResponse createOrGetChatRoom(Integer userId, ChatRoomCreateRequest request) { + return chatService.createOrGetChatRoom(userId, request); + } + public UnifiedChatRoomsResponse getChatRooms(Integer userId) { ChatRoomsResponse directRooms = chatService.getChatRooms(userId); GroupChatRoomsResponse groupRooms = groupChatService.getChatRooms(userId); - Map directMuteMap = getMuteMap( - NotificationTargetType.DIRECT_CHAT_ROOM, - directRooms.chatRooms().stream() - .map(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId) - .toList(), - userId + + List roomIds = new ArrayList<>(); + roomIds.addAll( + directRooms.chatRooms().stream().map(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId).toList() ); - Map groupMuteMap = getMuteMap( - NotificationTargetType.GROUP_CHAT_ROOM, + roomIds.addAll( groupRooms.groupChatRooms().stream() .map(GroupChatRoomsResponse.InnerGroupChatRoomResponse::groupChatRoomId) - .toList(), - userId + .toList() ); + Map muteMap = getMuteMap(roomIds, userId); List rooms = new ArrayList<>(); directRooms.chatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( @@ -63,7 +82,7 @@ public UnifiedChatRoomsResponse getChatRooms(Integer userId) { room.lastMessage(), room.lastSentTime(), room.unreadCount(), - directMuteMap.getOrDefault(room.chatRoomId(), false) + muteMap.getOrDefault(room.chatRoomId(), false) ))); groupRooms.groupChatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( @@ -74,7 +93,7 @@ public UnifiedChatRoomsResponse getChatRooms(Integer userId) { room.lastMessage(), room.lastSentAt(), room.unreadMessageCount(), - groupMuteMap.getOrDefault(room.groupChatRoomId(), false) + muteMap.getOrDefault(room.groupChatRoomId(), false) ))); rooms.sort( @@ -85,52 +104,22 @@ public UnifiedChatRoomsResponse getChatRooms(Integer userId) { return new UnifiedChatRoomsResponse(rooms); } - private Map getMuteMap( - NotificationTargetType targetType, - List roomIds, - Integer userId - ) { - if (roomIds.isEmpty()) { - return Map.of(); - } + public UnifiedChatMessagesResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - List settings = notificationMuteSettingRepository - .findByTargetTypeAndTargetIdsAndUserId(targetType, roomIds, userId); - - Map muteMap = new HashMap<>(); - for (NotificationMuteSetting setting : settings) { - Integer targetId = setting.getTargetId(); - if (targetId != null) { - muteMap.put(targetId, setting.getIsMuted()); - } + if (room.isDirectRoom()) { + return toUnifiedResponse(chatService.getChatRoomMessages(userId, roomId, page, limit)); } - return muteMap; + return toUnifiedResponse(groupChatService.getMessagesByRoomId(roomId, userId, page, limit)); } - public UnifiedChatMessagesResponse getMessages( - Integer userId, - ChatType chatType, - Integer roomId, - Integer page, - Integer limit - ) { - if (chatType == ChatType.DIRECT) { - ChatMessagesResponse response = chatService.getChatRoomMessages(userId, roomId, page, limit); - return toUnifiedResponse(response); - } - - GroupChatMessagesResponse response = groupChatService.getMessagesByRoomId(roomId, userId, page, limit); - return toUnifiedResponse(response); - } + public UnifiedChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - public UnifiedChatMessageResponse sendMessage( - Integer userId, - ChatType chatType, - Integer roomId, - ChatMessageSendRequest request - ) { - if (chatType == ChatType.DIRECT) { + if (room.isDirectRoom()) { ChatMessageResponse response = chatService.sendMessage(userId, roomId, request); return new UnifiedChatMessageResponse( response.messageId(), @@ -157,6 +146,25 @@ public UnifiedChatMessageResponse sendMessage( ); } + private Map getMuteMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + List settings = notificationMuteSettingRepository + .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); + + Map muteMap = new HashMap<>(); + for (NotificationMuteSetting setting : settings) { + Integer targetId = setting.getTargetId(); + if (targetId != null) { + muteMap.put(targetId, setting.getIsMuted()); + } + } + + return muteMap; + } + private UnifiedChatMessagesResponse toUnifiedResponse(ChatMessagesResponse response) { List messages = response.messages().stream() .map(message -> new UnifiedChatMessageResponse( From c119c046472c9edf9521816fbd3e04ac14575b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 21:53:48 +0900 Subject: [PATCH 07/26] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=AE=A4=ED=8A=B8=20=ED=83=80=EA=B2=9F?= =?UTF-8?q?=EC=9D=84=20CHAT=5FROOM=20=EB=8B=A8=EC=9D=BC=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../enums/NotificationTargetType.java | 3 +-- .../model/NotificationMuteSetting.java | 9 +-------- .../service/NotificationService.java | 19 ++++++++++++++++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java b/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java index abe1ef11f..8e7efa152 100644 --- a/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java +++ b/src/main/java/gg/agit/konect/domain/notification/enums/NotificationTargetType.java @@ -1,6 +1,5 @@ package gg.agit.konect.domain.notification.enums; public enum NotificationTargetType { - DIRECT_CHAT_ROOM, - GROUP_CHAT_ROOM + CHAT_ROOM } diff --git a/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java b/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java index 6007cfb9b..d5ebab0e3 100644 --- a/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java +++ b/src/main/java/gg/agit/konect/domain/notification/model/NotificationMuteSetting.java @@ -17,7 +17,6 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -51,7 +50,6 @@ public class NotificationMuteSetting extends BaseEntity { @Column(name = "is_muted", nullable = false) private Boolean isMuted; - @Builder private NotificationMuteSetting( Integer id, NotificationTargetType targetType, @@ -72,12 +70,7 @@ public static NotificationMuteSetting of( User user, Boolean isMuted ) { - return NotificationMuteSetting.builder() - .targetType(targetType) - .targetId(targetId) - .user(user) - .isMuted(isMuted) - .build(); + return new NotificationMuteSetting(null, targetType, targetId, user, isMuted); } public void toggleMute() { diff --git a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java index b5eec80c1..07d15fe0b 100644 --- a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java +++ b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java @@ -111,7 +111,7 @@ public void sendChatNotification(Integer receiverId, Integer roomId, String send } boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( - NotificationTargetType.DIRECT_CHAT_ROOM, + NotificationTargetType.CHAT_ROOM, roomId, receiverId ) @@ -230,6 +230,23 @@ public void sendGroupChatNotification( continue; } + boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( + NotificationTargetType.CHAT_ROOM, + roomId, + recipientId + ) + .map(setting -> Boolean.TRUE.equals(setting.getIsMuted())) + .orElse(false); + + if (isMuted) { + log.debug( + "Group chat muted, skipping notification: roomId={}, recipientId={}", + roomId, + recipientId + ); + continue; + } + List tokens = notificationDeviceTokenRepository.findTokensByUserId(recipientId); if (tokens.isEmpty()) { log.debug("No device tokens found for user: recipientId={}", recipientId); From b327250601a60bea6d7b43dff092bc6015b99ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:04:28 +0900 Subject: [PATCH 08/26] =?UTF-8?q?refactor:=20=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=20=EA=B4=80=EB=A0=A8=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/group/model/GroupChatMessage.java | 66 ---------------- .../chat/group/model/GroupChatReadStatus.java | 76 ------------------- .../group/model/GroupChatReadStatusId.java | 16 ---- .../chat/group/model/GroupChatRoom.java | 46 ----------- .../GroupChatMessageRepository.java | 72 ------------------ .../GroupChatReadStatusRepository.java | 18 ----- .../repository/GroupChatRoomRepository.java | 42 ---------- .../GroupRoomUnreadCountProjection.java | 8 -- 8 files changed, 344 deletions(-) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java deleted file mode 100644 index a0180c3e0..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatMessage.java +++ /dev/null @@ -1,66 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import gg.agit.konect.domain.user.model.User; -import gg.agit.konect.global.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_message") -@NoArgsConstructor(access = PROTECTED) -public class GroupChatMessage extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id", nullable = false, updatable = false, unique = true) - private Integer id; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "room_id", nullable = false) - private GroupChatRoom room; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "sender_id", nullable = false) - private User sender; - - @Column(name = "content", nullable = false, length = 1000) - private String content; - - @Builder - private GroupChatMessage( - Integer id, - GroupChatRoom room, - User sender, - String content - ) { - this.id = id; - this.room = room; - this.sender = sender; - this.content = content; - } - - public static GroupChatMessage of(GroupChatRoom room, User sender, String content) { - return GroupChatMessage.builder() - .room(room) - .sender(sender) - .content(content) - .build(); - } - - public boolean isSentBy(Integer userId) { - return sender.getId().equals(userId); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java deleted file mode 100644 index daeb2d1e2..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatus.java +++ /dev/null @@ -1,76 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static lombok.AccessLevel.PROTECTED; - -import java.time.LocalDateTime; - -import gg.agit.konect.domain.user.model.User; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_read_status") -@IdClass(GroupChatReadStatusId.class) -@NoArgsConstructor(access = PROTECTED) -public class GroupChatReadStatus { - - @Id - @Column(name = "room_id", nullable = false, updatable = false) - private Integer roomId; - - @Id - @Column(name = "user_id", nullable = false, updatable = false) - private Integer userId; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "room_id", nullable = false, insertable = false, updatable = false) - private GroupChatRoom room; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "user_id", nullable = false, insertable = false, updatable = false) - private User user; - - @Column(name = "last_read_at", nullable = false) - private LocalDateTime lastReadAt; - - @Builder - private GroupChatReadStatus( - Integer roomId, - Integer userId, - GroupChatRoom room, - User user, - LocalDateTime lastReadAt - ) { - this.roomId = roomId; - this.userId = userId; - this.room = room; - this.user = user; - this.lastReadAt = lastReadAt; - } - - public static GroupChatReadStatus of(GroupChatRoom room, User user, LocalDateTime lastReadAt) { - return GroupChatReadStatus.builder() - .roomId(room.getId()) - .userId(user.getId()) - .room(room) - .user(user) - .lastReadAt(lastReadAt) - .build(); - } - - public void updateLastReadAt(LocalDateTime lastReadAt) { - if (this.lastReadAt.isBefore(lastReadAt)) { - this.lastReadAt = lastReadAt; - } - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java deleted file mode 100644 index cdf1abc8f..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatReadStatusId.java +++ /dev/null @@ -1,16 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import java.io.Serializable; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -@EqualsAndHashCode -@NoArgsConstructor -@AllArgsConstructor -public class GroupChatReadStatusId implements Serializable { - - private Integer roomId; - private Integer userId; -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java deleted file mode 100644 index e93c3de13..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/model/GroupChatRoom.java +++ /dev/null @@ -1,46 +0,0 @@ -package gg.agit.konect.domain.chat.group.model; - -import static jakarta.persistence.FetchType.LAZY; -import static jakarta.persistence.GenerationType.IDENTITY; -import static lombok.AccessLevel.PROTECTED; - -import gg.agit.konect.domain.club.model.Club; -import gg.agit.konect.global.model.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Entity -@Table(name = "group_chat_room") -@NoArgsConstructor(access = PROTECTED) -public class GroupChatRoom extends BaseEntity { - - @Id - @GeneratedValue(strategy = IDENTITY) - @Column(name = "id", nullable = false, updatable = false, unique = true) - private Integer id; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "club_id", nullable = false, unique = true) - private Club club; - - @Builder - private GroupChatRoom(Integer id, Club club) { - this.id = id; - this.club = club; - } - - public static GroupChatRoom of(Club club) { - return GroupChatRoom.builder() - .club(club) - .build(); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java deleted file mode 100644 index 9ab29aa1c..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatMessageRepository.java +++ /dev/null @@ -1,72 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.group.model.GroupChatMessage; - -public interface GroupChatMessageRepository extends Repository { - - GroupChatMessage save(GroupChatMessage message); - - Optional findById(Integer messageId); - - @Query(""" - SELECT m - FROM GroupChatMessage m - JOIN FETCH m.sender - WHERE m.room.id = :roomId - ORDER BY m.createdAt DESC - """) - Page findByRoomIdOrderByCreatedAtDesc( - @Param("roomId") Integer roomId, - Pageable pageable - ); - - @Query(""" - SELECT COUNT(m) - FROM GroupChatMessage m - WHERE m.room.id = :roomId - """) - long countByRoomId(@Param("roomId") Integer roomId); - - @Query(""" - SELECT m - FROM GroupChatMessage m - JOIN FETCH m.sender - WHERE m.id IN ( - SELECT MAX(m2.id) - FROM GroupChatMessage m2 - WHERE m2.room.id IN :roomIds - GROUP BY m2.room.id - ) - """) - List findLatestMessagesByRoomIds(@Param("roomIds") List roomIds); - - @Query(value = """ - SELECT r.id AS roomId, COUNT(m.id) AS unreadCount - FROM group_chat_room r - JOIN club_member cm - ON cm.club_id = r.club_id - AND cm.user_id = :userId - LEFT JOIN group_chat_read_status grs - ON grs.room_id = r.id - AND grs.user_id = :userId - LEFT JOIN group_chat_message m - ON m.room_id = r.id - AND m.sender_id <> :userId - AND m.created_at > COALESCE(grs.last_read_at, cm.created_at) - WHERE r.id IN (:roomIds) - GROUP BY r.id - """, nativeQuery = true) - List countUnreadByRoomIdsAndUserId( - @Param("roomIds") List roomIds, - @Param("userId") Integer userId - ); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java deleted file mode 100644 index d11e27c46..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatReadStatusRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.repository.Repository; - -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatus; -import gg.agit.konect.domain.chat.group.model.GroupChatReadStatusId; - -public interface GroupChatReadStatusRepository extends Repository { - - GroupChatReadStatus save(GroupChatReadStatus status); - - Optional findByRoomIdAndUserId(Integer roomId, Integer userId); - - List findByRoomId(Integer roomId); -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java deleted file mode 100644 index a2324c8c8..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupChatRoomRepository.java +++ /dev/null @@ -1,42 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.Param; - -import gg.agit.konect.domain.chat.group.model.GroupChatRoom; -import gg.agit.konect.global.code.ApiResponseCode; -import gg.agit.konect.global.exception.CustomException; - -public interface GroupChatRoomRepository extends Repository { - - GroupChatRoom save(GroupChatRoom room); - - Optional findById(Integer roomId); - - @Query(""" - SELECT r - FROM GroupChatRoom r - JOIN FETCH r.club c - JOIN ClubMember cm ON cm.club.id = c.id - WHERE cm.user.id = :userId - ORDER BY r.updatedAt DESC - """) - List findAllByUserId(@Param("userId") Integer userId); - - Optional findByClubId(Integer clubId); - - default GroupChatRoom getById(Integer roomId) { - return findById(roomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM)); - } - - default GroupChatRoom getByClubId(Integer clubId) { - return findByClubId(clubId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM)); - } - -} diff --git a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java b/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java deleted file mode 100644 index 1fb947c6f..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/group/repository/GroupRoomUnreadCountProjection.java +++ /dev/null @@ -1,8 +0,0 @@ -package gg.agit.konect.domain.chat.group.repository; - -public interface GroupRoomUnreadCountProjection { - - Integer getRoomId(); - - Long getUnreadCount(); -} From 9cdb8a677aeb394b7e8fa3f209e4a94a96141bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:05:09 +0900 Subject: [PATCH 09/26] =?UTF-8?q?refactor:=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{unified => }/controller/ChatApi.java | 24 ++-- .../controller/ChatController.java | 41 ++++--- .../ChatMessageDetailResponse.java} | 4 +- .../ChatMessagePageResponse.java} | 6 +- .../{direct => }/dto/ChatMessageResponse.java | 4 +- .../dto/ChatMessageSendRequest.java | 2 +- .../dto/ChatMessagesResponse.java | 4 +- .../ChatMuteResponse.java} | 4 +- .../dto/ChatRoomCreateRequest.java | 2 +- .../ChatRoomListResponse.java} | 6 +- .../{direct => }/dto/ChatRoomResponse.java | 4 +- .../ChatRoomSummaryResponse.java} | 6 +- .../{direct => }/dto/ChatRoomsResponse.java | 6 +- .../ClubMessagePageResponse.java} | 6 +- .../ClubMessageResponse.java} | 4 +- .../ClubRoomListResponse.java} | 24 ++-- .../ClubRoomResponse.java} | 6 +- .../{direct => }/dto/UnreadMessageCount.java | 2 +- .../chat/{direct => }/enums/ChatRoomType.java | 2 +- .../konect/domain/chat/enums/ChatType.java | 6 + .../chat/{direct => }/model/ChatMessage.java | 2 +- .../chat/{direct => }/model/ChatRoom.java | 2 +- .../{unified => }/model/ChatRoomMember.java | 3 +- .../{unified => }/model/ChatRoomMemberId.java | 2 +- .../repository/ChatMessageRepository.java | 28 ++--- .../repository/ChatRoomMemberRepository.java | 6 +- .../repository/ChatRoomRepository.java | 18 +-- .../repository/RoomUnreadCountProjection.java | 2 +- .../ChatCoordinatorService.java} | 107 +++++++++--------- .../service/ChatPresenceService.java | 2 +- .../{direct => }/service/ChatService.java | 31 +++-- .../ClubChatService.java} | 69 ++++++----- .../domain/chat/unified/enums/ChatType.java | 6 - .../domain/club/service/ClubService.java | 4 +- .../service/NotificationService.java | 2 +- .../domain/user/service/UserService.java | 8 +- 36 files changed, 224 insertions(+), 231 deletions(-) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/controller/ChatApi.java (85%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/controller/ChatController.java (52%) rename src/main/java/gg/agit/konect/domain/chat/{unified/dto/UnifiedChatMessageResponse.java => dto/ChatMessageDetailResponse.java} (93%) rename src/main/java/gg/agit/konect/domain/chat/{group/dto/GroupChatMessagesResponse.java => dto/ChatMessagePageResponse.java} (87%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatMessageResponse.java (94%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatMessageSendRequest.java (92%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatMessagesResponse.java (97%) rename src/main/java/gg/agit/konect/domain/chat/{unified/dto/UnifiedChatMuteResponse.java => dto/ChatMuteResponse.java} (75%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatRoomCreateRequest.java (89%) rename src/main/java/gg/agit/konect/domain/chat/{unified/dto/UnifiedChatRoomsResponse.java => dto/ChatRoomListResponse.java} (65%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatRoomResponse.java (79%) rename src/main/java/gg/agit/konect/domain/chat/{unified/dto/UnifiedChatRoomResponse.java => dto/ChatRoomSummaryResponse.java} (90%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/ChatRoomsResponse.java (94%) rename src/main/java/gg/agit/konect/domain/chat/{unified/dto/UnifiedChatMessagesResponse.java => dto/ClubMessagePageResponse.java} (87%) rename src/main/java/gg/agit/konect/domain/chat/{group/dto/GroupChatMessageResponse.java => dto/ClubMessageResponse.java} (92%) rename src/main/java/gg/agit/konect/domain/chat/{group/dto/GroupChatRoomsResponse.java => dto/ClubRoomListResponse.java} (78%) rename src/main/java/gg/agit/konect/domain/chat/{group/dto/GroupChatRoomResponse.java => dto/ClubRoomResponse.java} (67%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/dto/UnreadMessageCount.java (64%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/enums/ChatRoomType.java (51%) create mode 100644 src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java rename src/main/java/gg/agit/konect/domain/chat/{direct => }/model/ChatMessage.java (97%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/model/ChatRoom.java (98%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/model/ChatRoomMember.java (95%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/model/ChatRoomMemberId.java (92%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/repository/ChatMessageRepository.java (74%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/repository/ChatRoomMemberRepository.java (93%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/repository/ChatRoomRepository.java (87%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/repository/RoomUnreadCountProjection.java (64%) rename src/main/java/gg/agit/konect/domain/chat/{unified/service/UnifiedChatService.java => service/ChatCoordinatorService.java} (58%) rename src/main/java/gg/agit/konect/domain/chat/{unified => }/service/ChatPresenceService.java (97%) rename src/main/java/gg/agit/konect/domain/chat/{direct => }/service/ChatService.java (90%) rename src/main/java/gg/agit/konect/domain/chat/{group/service/GroupChatService.java => service/ClubChatService.java} (82%) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java similarity index 85% rename from src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java rename to src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java index 13fd5a8a6..f900843a7 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatApi.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.controller; +package gg.agit.konect.domain.chat.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -8,13 +8,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMuteResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -53,7 +53,7 @@ ResponseEntity createOrGetChatRoom( - 최근 메시지가 있는 순서대로 정렬됩니다. """) @GetMapping("/rooms") - ResponseEntity getChatRooms( + ResponseEntity getChatRooms( @UserId Integer userId ); @@ -73,7 +73,7 @@ ResponseEntity getChatRooms( - FORBIDDEN_CHAT_ROOM_ACCESS (403): 채팅방에 접근할 권한이 없습니다. """) @GetMapping("/rooms/{chatRoomId}") - ResponseEntity getChatRoomMessages( + ResponseEntity getChatRoomMessages( @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, @PathVariable(value = "chatRoomId") Integer chatRoomId, @@ -95,7 +95,7 @@ ResponseEntity getChatRoomMessages( - FORBIDDEN_CHAT_ROOM_ACCESS (403): 채팅방에 접근할 권한이 없습니다. """) @PostMapping("/rooms/{chatRoomId}/messages") - ResponseEntity sendMessage( + ResponseEntity sendMessage( @PathVariable(value = "chatRoomId") Integer chatRoomId, @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId @@ -103,7 +103,7 @@ ResponseEntity sendMessage( @Operation(summary = "채팅 알림 기능 유무를 토글한다.") @PostMapping("/rooms/{chatRoomId}/mute") - ResponseEntity toggleGroupChatMute( + ResponseEntity toggleChatMute( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java similarity index 52% rename from src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java rename to src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java index 4879e0df2..901f3c01e 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/controller/ChatController.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.controller; +package gg.agit.konect.domain.chat.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -7,15 +7,14 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMuteResponse; -import gg.agit.konect.domain.chat.group.service.GroupChatService; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.service.UnifiedChatService; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.service.ChatCoordinatorService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -25,53 +24,51 @@ @RequestMapping("/chats") public class ChatController implements ChatApi { - private final GroupChatService groupChatService; - private final UnifiedChatService unifiedChatService; + private final ChatCoordinatorService chatCoordinatorService; @Override public ResponseEntity createOrGetChatRoom( @Valid @RequestBody ChatRoomCreateRequest request, @UserId Integer userId ) { - ChatRoomResponse response = unifiedChatService.createOrGetChatRoom(userId, request); + ChatRoomResponse response = chatCoordinatorService.createOrGetChatRoom(userId, request); return ResponseEntity.ok(response); } @Override - public ResponseEntity getChatRooms( + public ResponseEntity getChatRooms( @UserId Integer userId ) { - UnifiedChatRoomsResponse response = unifiedChatService.getChatRooms(userId); + ChatRoomListResponse response = chatCoordinatorService.getChatRooms(userId); return ResponseEntity.ok(response); } @Override - public ResponseEntity getChatRoomMessages( + public ResponseEntity getChatRoomMessages( @RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "limit", defaultValue = "20", required = false) Integer limit, @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - UnifiedChatMessagesResponse response = unifiedChatService.getMessages(userId, chatRoomId, page, limit); + ChatMessagePageResponse response = chatCoordinatorService.getMessages(userId, chatRoomId, page, limit); return ResponseEntity.ok(response); } @Override - public ResponseEntity sendMessage( + public ResponseEntity sendMessage( @PathVariable(value = "chatRoomId") Integer chatRoomId, @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId ) { - UnifiedChatMessageResponse response = unifiedChatService.sendMessage(userId, chatRoomId, request); + ChatMessageDetailResponse response = chatCoordinatorService.sendMessage(userId, chatRoomId, request); return ResponseEntity.ok(response); } @Override - public ResponseEntity toggleGroupChatMute( + public ResponseEntity toggleChatMute( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - Boolean isMuted = groupChatService.toggleMute(userId, chatRoomId); - return ResponseEntity.ok(new UnifiedChatMuteResponse(isMuted)); + return ResponseEntity.ok(chatCoordinatorService.toggleMute(userId, chatRoomId)); } } diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java similarity index 93% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java index ac5f6ad25..aa5906633 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessageResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageDetailResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatMessageResponse( +public record ChatMessageDetailResponse( @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) Integer messageId, diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java similarity index 87% rename from src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java index 00f59d4dd..5a6a4f0cb 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessagesResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagePageResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.group.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record GroupChatMessagesResponse( +public record ChatMessagePageResponse( @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) Long totalCount, @@ -24,6 +24,6 @@ public record GroupChatMessagesResponse( Integer clubId, @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages + List messages ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java similarity index 94% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java index cd8bfec87..437474c34 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -6,7 +6,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatMessage; import io.swagger.v3.oas.annotations.media.Schema; public record ChatMessageResponse( diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java similarity index 92% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java index c02bb88f0..c943c29ea 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessageSendRequest.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageSendRequest.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java index 1d84009a1..6fc9d6577 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatMessagesResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.user.enums.UserRole; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java similarity index 75% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java index 2df7868cc..0f7544282 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMuteResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMuteResponse.java @@ -1,10 +1,10 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatMuteResponse( +public record ChatMuteResponse( @Schema(description = "채팅 알림 음소거 여부", example = "true", requiredMode = REQUIRED) Boolean isMuted ) { diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java similarity index 89% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java index b0dd9bf7f..e4b75b9c1 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomCreateRequest.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomCreateRequest.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java similarity index 65% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java index d97f6e37e..64f59b79a 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -6,8 +6,8 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatRoomsResponse( +public record ChatRoomListResponse( @Schema(description = "채팅방 리스트", requiredMode = REQUIRED) - List rooms + List rooms ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java similarity index 79% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java index 578a4e49b..26d84d28b 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomResponse.java @@ -1,8 +1,8 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoom; import io.swagger.v3.oas.annotations.media.Schema; public record ChatRoomResponse( diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java similarity index 90% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java index f8e016114..0688a20c7 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatRoomResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomSummaryResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -7,10 +7,10 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.unified.enums.ChatType; +import gg.agit.konect.domain.chat.enums.ChatType; import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatRoomResponse( +public record ChatRoomSummaryResponse( @Schema(description = "채팅방 ID", example = "1", requiredMode = REQUIRED) Integer roomId, diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java similarity index 94% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java index e9ae442fb..0be0c6d06 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/ChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -9,8 +9,8 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.enums.ChatRoomType; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.enums.ChatRoomType; import gg.agit.konect.domain.user.model.User; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java similarity index 87% rename from src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java index 14e246d1c..130e86a5c 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/dto/UnifiedChatMessagesResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record UnifiedChatMessagesResponse( +public record ClubMessagePageResponse( @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) Long totalCount, @@ -24,6 +24,6 @@ public record UnifiedChatMessagesResponse( Integer clubId, @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages + List messages ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java similarity index 92% rename from src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java index 4bcc2978e..491d47a00 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatMessageResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.group.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -8,7 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record GroupChatMessageResponse( +public record ClubMessageResponse( @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) Integer messageId, diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java similarity index 78% rename from src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java index bad414b61..6e25ff5d7 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomsResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.group.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; @@ -9,17 +9,17 @@ import com.fasterxml.jackson.annotation.JsonFormat; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; import io.swagger.v3.oas.annotations.media.Schema; -public record GroupChatRoomsResponse( +public record ClubRoomListResponse( @Schema(description = "단체 채팅방 리스트", requiredMode = REQUIRED) - List groupChatRooms + List clubRooms ) { - public record InnerGroupChatRoomResponse( + public record InnerClubRoomResponse( @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer groupChatRoomId, + Integer roomId, @Schema(description = "동아리 이름", example = "BCSD Lab", requiredMode = REQUIRED) String clubName, @@ -37,12 +37,12 @@ public record InnerGroupChatRoomResponse( @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) Integer unreadMessageCount ) { - public static InnerGroupChatRoomResponse from( + public static InnerClubRoomResponse from( ChatRoom room, ChatMessage lastMessage, Map unreadCountMap ) { - return new InnerGroupChatRoomResponse( + return new InnerClubRoomResponse( room.getId(), room.getClub().getName(), room.getClub().getImageUrl(), @@ -53,14 +53,14 @@ public static InnerGroupChatRoomResponse from( } } - public static GroupChatRoomsResponse from( + public static ClubRoomListResponse from( List rooms, Map lastMessageMap, Map unreadCountMap ) { - return new GroupChatRoomsResponse( + return new ClubRoomListResponse( rooms.stream() - .map(room -> InnerGroupChatRoomResponse.from( + .map(room -> InnerClubRoomResponse.from( room, lastMessageMap.get(room.getId()), unreadCountMap diff --git a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java similarity index 67% rename from src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java index e24a16a47..8e1f03909 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/dto/GroupChatRoomResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java @@ -1,11 +1,11 @@ -package gg.agit.konect.domain.chat.group.dto; +package gg.agit.konect.domain.chat.dto; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import io.swagger.v3.oas.annotations.media.Schema; -public record GroupChatRoomResponse( +public record ClubRoomResponse( @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer groupChatRoomId + Integer roomId ) { } diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java similarity index 64% rename from src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java rename to src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java index bbcfacb53..1fff130d2 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/dto/UnreadMessageCount.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/UnreadMessageCount.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.dto; +package gg.agit.konect.domain.chat.dto; public record UnreadMessageCount( Integer chatRoomId, diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java b/src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java similarity index 51% rename from src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java rename to src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java index d954a2714..70f99ebac 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/enums/ChatRoomType.java +++ b/src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.enums; +package gg.agit.konect.domain.chat.enums; public enum ChatRoomType { NORMAL, diff --git a/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java b/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java new file mode 100644 index 000000000..10b398775 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/enums/ChatType.java @@ -0,0 +1,6 @@ +package gg.agit.konect.domain.chat.enums; + +public enum ChatType { + DIRECT, + GROUP +} diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java index e31503916..025b56687 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatMessage.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatMessage.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.model; +package gg.agit.konect.domain.chat.model; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java similarity index 98% rename from src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java index 71238e4b8..31e922e65 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.model; +package gg.agit.konect.domain.chat.model; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java similarity index 95% rename from src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java index f9fe86bbb..35b2ab606 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMember.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java @@ -1,11 +1,10 @@ -package gg.agit.konect.domain.chat.unified.model; +package gg.agit.konect.domain.chat.model; import static jakarta.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; import java.time.LocalDateTime; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.global.model.BaseEntity; import jakarta.persistence.Column; diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java similarity index 92% rename from src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java rename to src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java index b3df34693..8e068756b 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/model/ChatRoomMemberId.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMemberId.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.model; +package gg.agit.konect.domain.chat.model; import java.io.Serial; import java.io.Serializable; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java similarity index 74% rename from src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java rename to src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java index faaf5fe93..05c05c9bb 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatMessageRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.repository; +package gg.agit.konect.domain.chat.repository; import java.util.List; @@ -8,8 +8,8 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -import gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; +import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.user.enums.UserRole; public interface ChatMessageRepository extends Repository { @@ -17,17 +17,17 @@ public interface ChatMessageRepository extends Repository ChatMessage save(ChatMessage chatMessage); @Query(""" - SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - crm.chatRoomId, + SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount( + crm.id.chatRoomId, COUNT(cm) ) FROM ChatRoomMember crm - LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.chatRoomId + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.id.chatRoomId AND cm.sender.id <> :receiverId AND cm.createdAt > crm.lastReadAt - WHERE crm.chatRoomId IN :chatRoomIds - AND crm.userId = :receiverId - GROUP BY crm.chatRoomId + WHERE crm.id.chatRoomId IN :chatRoomIds + AND crm.id.userId = :receiverId + GROUP BY crm.id.chatRoomId """) List countUnreadMessagesByChatRoomIdsAndUserId( @Param("chatRoomIds") List chatRoomIds, @@ -44,18 +44,18 @@ List countUnreadMessagesByChatRoomIdsAndUserId( Page findByChatRoomId(@Param("chatRoomId") Integer chatRoomId, Pageable pageable); @Query(""" - SELECT new gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount( - crm.chatRoomId, + SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount( + crm.id.chatRoomId, COUNT(cm) ) FROM ChatRoomMember crm JOIN crm.user u - LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.chatRoomId + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = crm.id.chatRoomId AND cm.sender.role != :adminRole AND cm.createdAt > crm.lastReadAt - WHERE crm.chatRoomId IN :chatRoomIds + WHERE crm.id.chatRoomId IN :chatRoomIds AND u.role = :adminRole - GROUP BY crm.chatRoomId + GROUP BY crm.id.chatRoomId """) List countUnreadMessagesForAdmin( @Param("chatRoomIds") List chatRoomIds, diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java similarity index 93% rename from src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java rename to src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java index d597de63c..0559d5ca9 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/repository/ChatRoomMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.repository; +package gg.agit.konect.domain.chat.repository; import java.time.LocalDateTime; import java.util.List; @@ -9,8 +9,8 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; -import gg.agit.konect.domain.chat.unified.model.ChatRoomMemberId; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.model.ChatRoomMemberId; public interface ChatRoomMemberRepository extends Repository { diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java similarity index 87% rename from src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java rename to src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java index 85ee7b39e..6df94f69f 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/repository/ChatRoomRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.repository; +package gg.agit.konect.domain.chat.repository; import java.util.List; import java.util.Optional; @@ -7,7 +7,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.user.enums.UserRole; public interface ChatRoomRepository extends Repository { @@ -17,11 +17,11 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT DISTINCT cr FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id LEFT JOIN FETCH cr.sender LEFT JOIN FETCH cr.receiver LEFT JOIN FETCH cr.club - WHERE crm.userId = :userId + WHERE crm.id.userId = :userId AND cr.club IS NULL ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id """) @@ -49,7 +49,7 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT DISTINCT cr FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r WHERE cr.club IS NULL @@ -61,7 +61,7 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT DISTINCT cr FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r WHERE cr.club IS NULL @@ -78,7 +78,7 @@ AND EXISTS ( @Query(""" SELECT cr FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id JOIN FETCH cr.sender s JOIN FETCH cr.receiver r WHERE cr.club IS NULL @@ -102,11 +102,11 @@ Optional findByUserIdAndAdminRole( @Query(""" SELECT DISTINCT cr FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.chatRoomId = cr.id + JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id LEFT JOIN FETCH cr.club LEFT JOIN FETCH cr.sender LEFT JOIN FETCH cr.receiver - WHERE crm.userId = :userId + WHERE crm.id.userId = :userId AND cr.club IS NOT NULL ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id """) diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java b/src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java similarity index 64% rename from src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java rename to src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java index 3965c58d2..62f7579e5 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/repository/RoomUnreadCountProjection.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/RoomUnreadCountProjection.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.repository; +package gg.agit.konect.domain.chat.repository; public interface RoomUnreadCountProjection { diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java similarity index 58% rename from src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java index 2c74ffe70..adfe655d8 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/service/UnifiedChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.service; +package gg.agit.konect.domain.chat.service; import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; @@ -10,45 +10,44 @@ import org.springframework.stereotype.Service; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatMessagesResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.service.ChatService; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomsResponse; -import gg.agit.konect.domain.chat.group.service.GroupChatService; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessageResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatMessagesResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomResponse; -import gg.agit.konect.domain.chat.unified.dto.UnifiedChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.enums.ChatType; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ChatMessageResponse; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; +import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ClubMessageResponse; +import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.notification.enums.NotificationTargetType; import gg.agit.konect.domain.notification.model.NotificationMuteSetting; import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; import gg.agit.konect.global.exception.CustomException; @Service -public class UnifiedChatService { +public class ChatCoordinatorService { private final ChatService chatService; - private final GroupChatService groupChatService; + private final ClubChatService clubChatService; private final ChatRoomRepository chatRoomRepository; private final NotificationMuteSettingRepository notificationMuteSettingRepository; - public UnifiedChatService( + public ChatCoordinatorService( ChatService chatService, - GroupChatService groupChatService, + ClubChatService clubChatService, ChatRoomRepository chatRoomRepository, NotificationMuteSettingRepository notificationMuteSettingRepository ) { this.chatService = chatService; - this.groupChatService = groupChatService; + this.clubChatService = clubChatService; this.chatRoomRepository = chatRoomRepository; this.notificationMuteSettingRepository = notificationMuteSettingRepository; } @@ -57,24 +56,20 @@ public ChatRoomResponse createOrGetChatRoom(Integer userId, ChatRoomCreateReques return chatService.createOrGetChatRoom(userId, request); } - public UnifiedChatRoomsResponse getChatRooms(Integer userId) { + public ChatRoomListResponse getChatRooms(Integer userId) { ChatRoomsResponse directRooms = chatService.getChatRooms(userId); - GroupChatRoomsResponse groupRooms = groupChatService.getChatRooms(userId); + ClubRoomListResponse clubRooms = clubChatService.getChatRooms(userId); List roomIds = new ArrayList<>(); roomIds.addAll( directRooms.chatRooms().stream().map(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId).toList() ); - roomIds.addAll( - groupRooms.groupChatRooms().stream() - .map(GroupChatRoomsResponse.InnerGroupChatRoomResponse::groupChatRoomId) - .toList() - ); + roomIds.addAll(clubRooms.clubRooms().stream().map(ClubRoomListResponse.InnerClubRoomResponse::roomId).toList()); Map muteMap = getMuteMap(roomIds, userId); - List rooms = new ArrayList<>(); + List rooms = new ArrayList<>(); - directRooms.chatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( + directRooms.chatRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( room.chatRoomId(), ChatType.DIRECT, room.chatPartnerName(), @@ -85,43 +80,43 @@ public UnifiedChatRoomsResponse getChatRooms(Integer userId) { muteMap.getOrDefault(room.chatRoomId(), false) ))); - groupRooms.groupChatRooms().forEach(room -> rooms.add(new UnifiedChatRoomResponse( - room.groupChatRoomId(), + clubRooms.clubRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( + room.roomId(), ChatType.GROUP, room.clubName(), room.clubImageUrl(), room.lastMessage(), room.lastSentAt(), room.unreadMessageCount(), - muteMap.getOrDefault(room.groupChatRoomId(), false) + muteMap.getOrDefault(room.roomId(), false) ))); rooms.sort( - Comparator.comparing(UnifiedChatRoomResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(UnifiedChatRoomResponse::roomId) + Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(ChatRoomSummaryResponse::roomId) ); - return new UnifiedChatRoomsResponse(rooms); + return new ChatRoomListResponse(rooms); } - public UnifiedChatMessagesResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); if (room.isDirectRoom()) { - return toUnifiedResponse(chatService.getChatRoomMessages(userId, roomId, page, limit)); + return toMessagePageResponse(chatService.getChatRoomMessages(userId, roomId, page, limit)); } - return toUnifiedResponse(groupChatService.getMessagesByRoomId(roomId, userId, page, limit)); + return toMessagePageResponse(clubChatService.getMessagesByRoomId(roomId, userId, page, limit)); } - public UnifiedChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); if (room.isDirectRoom()) { ChatMessageResponse response = chatService.sendMessage(userId, roomId, request); - return new UnifiedChatMessageResponse( + return new ChatMessageDetailResponse( response.messageId(), response.senderId(), null, @@ -133,8 +128,8 @@ public UnifiedChatMessageResponse sendMessage(Integer userId, Integer roomId, Ch ); } - GroupChatMessageResponse response = groupChatService.sendMessageByRoomId(roomId, userId, request.content()); - return new UnifiedChatMessageResponse( + ClubMessageResponse response = clubChatService.sendMessageByRoomId(roomId, userId, request.content()); + return new ChatMessageDetailResponse( response.messageId(), response.senderId(), response.senderName(), @@ -146,6 +141,10 @@ public UnifiedChatMessageResponse sendMessage(Integer userId, Integer roomId, Ch ); } + public ChatMuteResponse toggleMute(Integer userId, Integer roomId) { + return new ChatMuteResponse(clubChatService.toggleMute(userId, roomId)); + } + private Map getMuteMap(List roomIds, Integer userId) { if (roomIds.isEmpty()) { return Map.of(); @@ -165,9 +164,9 @@ private Map getMuteMap(List roomIds, Integer userId) return muteMap; } - private UnifiedChatMessagesResponse toUnifiedResponse(ChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new UnifiedChatMessageResponse( + private ChatMessagePageResponse toMessagePageResponse(ChatMessagesResponse response) { + List messages = response.messages().stream() + .map(message -> new ChatMessageDetailResponse( message.messageId(), message.senderId(), null, @@ -179,7 +178,7 @@ private UnifiedChatMessagesResponse toUnifiedResponse(ChatMessagesResponse respo )) .toList(); - return new UnifiedChatMessagesResponse( + return new ChatMessagePageResponse( response.totalCount(), response.currentCount(), response.totalPage(), @@ -189,9 +188,9 @@ private UnifiedChatMessagesResponse toUnifiedResponse(ChatMessagesResponse respo ); } - private UnifiedChatMessagesResponse toUnifiedResponse(GroupChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new UnifiedChatMessageResponse( + private ChatMessagePageResponse toMessagePageResponse(ClubMessagePageResponse response) { + List messages = response.messages().stream() + .map(message -> new ChatMessageDetailResponse( message.messageId(), message.senderId(), message.senderName(), @@ -203,7 +202,7 @@ private UnifiedChatMessagesResponse toUnifiedResponse(GroupChatMessagesResponse )) .toList(); - return new UnifiedChatMessagesResponse( + return new ChatMessagePageResponse( response.totalCount(), response.currentCount(), response.totalPage(), diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java index 815b1aae7..956a4d852 100644 --- a/src/main/java/gg/agit/konect/domain/chat/unified/service/ChatPresenceService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatPresenceService.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.unified.service; +package gg.agit.konect.domain.chat.service; import java.time.Duration; diff --git a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java similarity index 90% rename from src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index e4881937f..cf5cf9a33 100644 --- a/src/main/java/gg/agit/konect/domain/chat/direct/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.direct.service; +package gg.agit.konect.domain.chat.service; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; @@ -19,22 +19,21 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatMessagesResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.direct.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.direct.dto.UnreadMessageCount; -import gg.agit.konect.domain.chat.direct.enums.ChatRoomType; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.dto.ChatMessageResponse; +import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; +import gg.agit.konect.domain.chat.dto.UnreadMessageCount; +import gg.agit.konect.domain.chat.enums.ChatRoomType; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; -import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; -import gg.agit.konect.domain.chat.unified.repository.ChatRoomMemberRepository; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; import gg.agit.konect.domain.notification.service.NotificationService; import gg.agit.konect.domain.user.enums.UserRole; import gg.agit.konect.domain.user.model.User; diff --git a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java similarity index 82% rename from src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java rename to src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java index 38362f26d..60c4bfc45 100644 --- a/src/main/java/gg/agit/konect/domain/chat/group/service/GroupChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.chat.group.service; +package gg.agit.konect.domain.chat.service; import java.time.LocalDateTime; import java.util.HashMap; @@ -12,18 +12,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessageResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatMessagesResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomResponse; -import gg.agit.konect.domain.chat.group.dto.GroupChatRoomsResponse; -import gg.agit.konect.domain.chat.unified.model.ChatRoomMember; -import gg.agit.konect.domain.chat.unified.repository.ChatRoomMemberRepository; -import gg.agit.konect.domain.chat.unified.repository.RoomUnreadCountProjection; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; +import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ClubMessageResponse; +import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; +import gg.agit.konect.domain.chat.dto.ClubRoomResponse; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.repository.RoomUnreadCountProjection; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.repository.ClubMemberRepository; @@ -40,7 +39,7 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class GroupChatService { +public class ClubChatService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; @@ -52,18 +51,18 @@ public class GroupChatService { private final NotificationService notificationService; @Transactional - public GroupChatRoomResponse getGroupChatRoom(Integer clubId, Integer userId) { + public ClubRoomResponse getClubRoom(Integer clubId, Integer userId) { ClubMember member = clubMemberRepository.getByClubIdAndUserId(clubId, userId); - ChatRoom room = resolveOrCreateGroupRoom(member.getClub()); + ChatRoom room = resolveOrCreateClubRoom(member.getClub()); ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - return new GroupChatRoomResponse(room.getId()); + return new ClubRoomResponse(room.getId()); } @Transactional - public GroupChatRoomsResponse getChatRooms(Integer userId) { + public ClubRoomListResponse getChatRooms(Integer userId) { List memberships = clubMemberRepository.findAllByUserId(userId); if (memberships.isEmpty()) { - return GroupChatRoomsResponse.from(List.of(), Map.of(), Map.of()); + return ClubRoomListResponse.from(List.of(), Map.of(), Map.of()); } Map membershipByClubId = memberships.stream() @@ -71,7 +70,7 @@ public GroupChatRoomsResponse getChatRooms(Integer userId) { List rooms = memberships.stream() .map(ClubMember::getClub) - .map(this::resolveOrCreateGroupRoom) + .map(this::resolveOrCreateClubRoom) .toList(); for (ChatRoom room : rooms) { @@ -85,15 +84,15 @@ public GroupChatRoomsResponse getChatRooms(Integer userId) { Map lastMessageMap = getLastMessageMap(roomIds); Map unreadCountMap = getUnreadCountMap(roomIds, userId); - return GroupChatRoomsResponse.from(rooms, lastMessageMap, unreadCountMap); + return ClubRoomListResponse.from(rooms, lastMessageMap, unreadCountMap); } @Transactional - public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer userId, Integer page, Integer limit) { - ChatRoom room = getGroupRoom(roomId); + public ClubMessagePageResponse getMessagesByRoomId(Integer roomId, Integer userId, Integer page, Integer limit) { + ChatRoom room = getClubRoom(roomId); ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - syncGroupMembers(room); + syncClubMembers(room); chatPresenceService.recordPresence(roomId, userId); updateLastReadAt(roomId, userId, LocalDateTime.now()); @@ -104,10 +103,10 @@ public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer use List messages = messagePage.getContent(); List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List responseMessages = messages.stream() + List responseMessages = messages.stream() .map(message -> { int unreadCount = getUnreadCount(message, members); - return new GroupChatMessageResponse( + return new ClubMessageResponse( message.getId(), message.getSender().getId(), message.getSender().getName(), @@ -120,7 +119,7 @@ public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer use .toList(); int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new GroupChatMessagesResponse( + return new ClubMessagePageResponse( totalCount, responseMessages.size(), totalPage, @@ -131,13 +130,13 @@ public GroupChatMessagesResponse getMessagesByRoomId(Integer roomId, Integer use } @Transactional - public GroupChatMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { - ChatRoom room = getGroupRoom(roomId); + public ClubMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { + ChatRoom room = getClubRoom(roomId); ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); User sender = member.getUser(); ensureRoomMember(room, sender, member.getCreatedAt()); - syncGroupMembers(room); + syncClubMembers(room); ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); room.updateLastMessage(message.getContent(), message.getCreatedAt()); @@ -155,7 +154,7 @@ public GroupChatMessageResponse sendMessageByRoomId(Integer roomId, Integer user recipientUserIds ); - return new GroupChatMessageResponse( + return new ClubMessageResponse( message.getId(), sender.getId(), sender.getName(), @@ -200,7 +199,7 @@ public Boolean toggleMute(Integer userId, Integer chatRoomId) { }); } - private ChatRoom getGroupRoom(Integer roomId) { + private ChatRoom getClubRoom(Integer roomId) { ChatRoom room = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); if (!room.isGroupRoom() || room.getClub() == null) { @@ -209,12 +208,12 @@ private ChatRoom getGroupRoom(Integer roomId) { return room; } - private ChatRoom resolveOrCreateGroupRoom(Club club) { + private ChatRoom resolveOrCreateClubRoom(Club club) { return chatRoomRepository.findByClubId(club.getId()) .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); } - private void syncGroupMembers(ChatRoom room) { + private void syncClubMembers(ChatRoom room) { List clubMembers = clubMemberRepository.findAllByClubId(room.getClub().getId()); for (ClubMember clubMember : clubMembers) { ensureRoomMember(room, clubMember.getUser(), clubMember.getCreatedAt()); @@ -233,7 +232,7 @@ private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); if (updated == 0) { - ChatRoom room = getGroupRoom(roomId); + ChatRoom room = getClubRoom(roomId); ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); ensureRoomMember(room, member.getUser(), member.getCreatedAt()); } diff --git a/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java b/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java deleted file mode 100644 index 2fa4f32f6..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/unified/enums/ChatType.java +++ /dev/null @@ -1,6 +0,0 @@ -package gg.agit.konect.domain.chat.unified.enums; - -public enum ChatType { - DIRECT, - GROUP -} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index 91055ec85..e4ec3a88e 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -14,8 +14,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.club.dto.ClubBasicInfoUpdateRequest; import gg.agit.konect.domain.club.dto.ClubCondition; import gg.agit.konect.domain.club.dto.ClubCreateRequest; diff --git a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java index 07d15fe0b..8d18e4e03 100644 --- a/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java +++ b/src/main/java/gg/agit/konect/domain/notification/service/NotificationService.java @@ -19,7 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; -import gg.agit.konect.domain.chat.unified.service.ChatPresenceService; +import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.notification.dto.NotificationTokenDeleteRequest; import gg.agit.konect.domain.notification.dto.NotificationTokenRegisterRequest; import gg.agit.konect.domain.notification.enums.NotificationTargetType; diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index fac25d75b..90ff0f060 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -11,10 +11,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import gg.agit.konect.domain.chat.direct.model.ChatMessage; -import gg.agit.konect.domain.chat.direct.model.ChatRoom; -import gg.agit.konect.domain.chat.direct.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.direct.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubPreMember; import gg.agit.konect.domain.club.repository.ClubMemberRepository; From 86d0104e4545a6ede1fe36b5ee96c1b642b511b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:15:28 +0900 Subject: [PATCH 10/26] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=95=98=EB=82=98=EB=A1=9C=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 14 +- .../chat/service/ChatCoordinatorService.java | 214 -------- .../domain/chat/service/ChatService.java | 484 ++++++++++++++++-- .../domain/chat/service/ClubChatService.java | 296 ----------- 4 files changed, 457 insertions(+), 551 deletions(-) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java index 901f3c01e..a37663ad2 100644 --- a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java @@ -14,7 +14,7 @@ import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.service.ChatCoordinatorService; +import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -24,14 +24,14 @@ @RequestMapping("/chats") public class ChatController implements ChatApi { - private final ChatCoordinatorService chatCoordinatorService; + private final ChatService chatService; @Override public ResponseEntity createOrGetChatRoom( @Valid @RequestBody ChatRoomCreateRequest request, @UserId Integer userId ) { - ChatRoomResponse response = chatCoordinatorService.createOrGetChatRoom(userId, request); + ChatRoomResponse response = chatService.createOrGetChatRoom(userId, request); return ResponseEntity.ok(response); } @@ -39,7 +39,7 @@ public ResponseEntity createOrGetChatRoom( public ResponseEntity getChatRooms( @UserId Integer userId ) { - ChatRoomListResponse response = chatCoordinatorService.getChatRooms(userId); + ChatRoomListResponse response = chatService.getChatRooms(userId); return ResponseEntity.ok(response); } @@ -50,7 +50,7 @@ public ResponseEntity getChatRoomMessages( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - ChatMessagePageResponse response = chatCoordinatorService.getMessages(userId, chatRoomId, page, limit); + ChatMessagePageResponse response = chatService.getMessages(userId, chatRoomId, page, limit); return ResponseEntity.ok(response); } @@ -60,7 +60,7 @@ public ResponseEntity sendMessage( @Valid @RequestBody ChatMessageSendRequest request, @UserId Integer userId ) { - ChatMessageDetailResponse response = chatCoordinatorService.sendMessage(userId, chatRoomId, request); + ChatMessageDetailResponse response = chatService.sendMessage(userId, chatRoomId, request); return ResponseEntity.ok(response); } @@ -69,6 +69,6 @@ public ResponseEntity toggleChatMute( @PathVariable(value = "chatRoomId") Integer chatRoomId, @UserId Integer userId ) { - return ResponseEntity.ok(chatCoordinatorService.toggleMute(userId, chatRoomId)); + return ResponseEntity.ok(chatService.toggleMute(userId, chatRoomId)); } } diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java deleted file mode 100644 index adfe655d8..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatCoordinatorService.java +++ /dev/null @@ -1,214 +0,0 @@ -package gg.agit.konect.domain.chat.service; - -import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.stereotype.Service; - -import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; -import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; -import gg.agit.konect.domain.chat.dto.ChatMessageResponse; -import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; -import gg.agit.konect.domain.chat.dto.ChatMuteResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; -import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; -import gg.agit.konect.domain.chat.dto.ClubMessageResponse; -import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; -import gg.agit.konect.domain.chat.enums.ChatType; -import gg.agit.konect.domain.chat.model.ChatRoom; -import gg.agit.konect.domain.chat.repository.ChatRoomRepository; -import gg.agit.konect.domain.notification.enums.NotificationTargetType; -import gg.agit.konect.domain.notification.model.NotificationMuteSetting; -import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; -import gg.agit.konect.global.exception.CustomException; - -@Service -public class ChatCoordinatorService { - - private final ChatService chatService; - private final ClubChatService clubChatService; - private final ChatRoomRepository chatRoomRepository; - private final NotificationMuteSettingRepository notificationMuteSettingRepository; - - public ChatCoordinatorService( - ChatService chatService, - ClubChatService clubChatService, - ChatRoomRepository chatRoomRepository, - NotificationMuteSettingRepository notificationMuteSettingRepository - ) { - this.chatService = chatService; - this.clubChatService = clubChatService; - this.chatRoomRepository = chatRoomRepository; - this.notificationMuteSettingRepository = notificationMuteSettingRepository; - } - - public ChatRoomResponse createOrGetChatRoom(Integer userId, ChatRoomCreateRequest request) { - return chatService.createOrGetChatRoom(userId, request); - } - - public ChatRoomListResponse getChatRooms(Integer userId) { - ChatRoomsResponse directRooms = chatService.getChatRooms(userId); - ClubRoomListResponse clubRooms = clubChatService.getChatRooms(userId); - - List roomIds = new ArrayList<>(); - roomIds.addAll( - directRooms.chatRooms().stream().map(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId).toList() - ); - roomIds.addAll(clubRooms.clubRooms().stream().map(ClubRoomListResponse.InnerClubRoomResponse::roomId).toList()); - - Map muteMap = getMuteMap(roomIds, userId); - List rooms = new ArrayList<>(); - - directRooms.chatRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( - room.chatRoomId(), - ChatType.DIRECT, - room.chatPartnerName(), - room.chatPartnerProfileImage(), - room.lastMessage(), - room.lastSentTime(), - room.unreadCount(), - muteMap.getOrDefault(room.chatRoomId(), false) - ))); - - clubRooms.clubRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( - room.roomId(), - ChatType.GROUP, - room.clubName(), - room.clubImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.unreadMessageCount(), - muteMap.getOrDefault(room.roomId(), false) - ))); - - rooms.sort( - Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(ChatRoomSummaryResponse::roomId) - ); - - return new ChatRoomListResponse(rooms); - } - - public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - - if (room.isDirectRoom()) { - return toMessagePageResponse(chatService.getChatRoomMessages(userId, roomId, page, limit)); - } - - return toMessagePageResponse(clubChatService.getMessagesByRoomId(roomId, userId, page, limit)); - } - - public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - - if (room.isDirectRoom()) { - ChatMessageResponse response = chatService.sendMessage(userId, roomId, request); - return new ChatMessageDetailResponse( - response.messageId(), - response.senderId(), - null, - response.content(), - response.createdAt(), - response.isRead(), - null, - response.isMine() - ); - } - - ClubMessageResponse response = clubChatService.sendMessageByRoomId(roomId, userId, request.content()); - return new ChatMessageDetailResponse( - response.messageId(), - response.senderId(), - response.senderName(), - response.content(), - response.createdAt(), - null, - response.unreadCount(), - response.isMine() - ); - } - - public ChatMuteResponse toggleMute(Integer userId, Integer roomId) { - return new ChatMuteResponse(clubChatService.toggleMute(userId, roomId)); - } - - private Map getMuteMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - List settings = notificationMuteSettingRepository - .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); - - Map muteMap = new HashMap<>(); - for (NotificationMuteSetting setting : settings) { - Integer targetId = setting.getTargetId(); - if (targetId != null) { - muteMap.put(targetId, setting.getIsMuted()); - } - } - - return muteMap; - } - - private ChatMessagePageResponse toMessagePageResponse(ChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new ChatMessageDetailResponse( - message.messageId(), - message.senderId(), - null, - message.content(), - message.createdAt(), - message.isRead(), - null, - message.isMine() - )) - .toList(); - - return new ChatMessagePageResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - null, - messages - ); - } - - private ChatMessagePageResponse toMessagePageResponse(ClubMessagePageResponse response) { - List messages = response.messages().stream() - .map(message -> new ChatMessageDetailResponse( - message.messageId(), - message.senderId(), - message.senderName(), - message.content(), - message.createdAt(), - null, - message.unreadCount(), - message.isMine() - )) - .toList(); - - return new ChatMessagePageResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - response.clubId(), - messages - ); - } -} 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 cf5cf9a33..0eec3b7b3 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 @@ -7,6 +7,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -19,25 +20,40 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; import gg.agit.konect.domain.chat.dto.ChatMessageResponse; import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; +import gg.agit.konect.domain.chat.dto.ChatMuteResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; +import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomsResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; +import gg.agit.konect.domain.chat.dto.ClubMessageResponse; +import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; import gg.agit.konect.domain.chat.dto.UnreadMessageCount; -import gg.agit.konect.domain.chat.enums.ChatRoomType; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.chat.model.ChatRoom; -import gg.agit.konect.domain.chat.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.repository.ChatRoomRepository; -import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.repository.RoomUnreadCountProjection; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.notification.enums.NotificationTargetType; +import gg.agit.konect.domain.notification.model.NotificationMuteSetting; +import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; import gg.agit.konect.domain.notification.service.NotificationService; 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.code.ApiResponseCode; import gg.agit.konect.global.exception.CustomException; import lombok.RequiredArgsConstructor; @@ -49,6 +65,8 @@ public class ChatService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; private final ChatRoomMemberRepository chatRoomMemberRepository; + private final NotificationMuteSettingRepository notificationMuteSettingRepository; + private final ClubMemberRepository clubMemberRepository; private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final NotificationService notificationService; @@ -73,9 +91,131 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat return ChatRoomResponse.from(chatRoom); } - public ChatRoomsResponse getChatRooms(Integer userId) { + @Transactional + public ChatRoomListResponse getChatRooms(Integer userId) { + List directRooms = getDirectChatRooms(userId); + ClubRoomListResponse clubRooms = getClubChatRooms(userId); + + List roomIds = new ArrayList<>(); + roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); + roomIds.addAll(clubRooms.clubRooms().stream().map(ClubRoomListResponse.InnerClubRoomResponse::roomId).toList()); + + Map muteMap = getMuteMap(roomIds, userId); + List rooms = new ArrayList<>(); + + directRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( + room.roomId(), + room.chatType(), + room.roomName(), + room.roomImageUrl(), + room.lastMessage(), + room.lastSentAt(), + room.unreadCount(), + muteMap.getOrDefault(room.roomId(), false) + ))); + + clubRooms.clubRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( + room.roomId(), + ChatType.GROUP, + room.clubName(), + room.clubImageUrl(), + room.lastMessage(), + room.lastSentAt(), + room.unreadMessageCount(), + muteMap.getOrDefault(room.roomId(), false) + ))); + + rooms.sort( + Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(ChatRoomSummaryResponse::roomId) + ); + + return new ChatRoomListResponse(rooms); + } + + @Transactional + public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (room.isDirectRoom()) { + return toMessagePageResponse(getDirectChatRoomMessages(userId, roomId, page, limit)); + } + + return toMessagePageResponse(getClubMessagesByRoomId(roomId, userId, page, limit)); + } + + @Transactional + public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (room.isDirectRoom()) { + ChatMessageResponse response = sendDirectMessage(userId, roomId, request); + return new ChatMessageDetailResponse( + response.messageId(), + response.senderId(), + null, + response.content(), + response.createdAt(), + response.isRead(), + null, + response.isMine() + ); + } + + ClubMessageResponse response = sendClubMessageByRoomId(roomId, userId, request.content()); + return new ChatMessageDetailResponse( + response.messageId(), + response.senderId(), + response.senderName(), + response.content(), + response.createdAt(), + null, + response.unreadCount(), + response.isMine() + ); + } + + @Transactional + public ChatMuteResponse toggleMute(Integer userId, Integer roomId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + + if (room.isGroupRoom()) { + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } else { + validateDirectAccess(room, userId); + } + + User user = userRepository.getById(userId); + Boolean isMuted = notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( + NotificationTargetType.CHAT_ROOM, + roomId, + userId + ) + .map(setting -> { + setting.toggleMute(); + notificationMuteSettingRepository.save(setting); + return setting.getIsMuted(); + }) + .orElseGet(() -> { + notificationMuteSettingRepository.save(NotificationMuteSetting.of( + NotificationTargetType.CHAT_ROOM, + roomId, + user, + true + )); + return true; + }); + + return new ChatMuteResponse(isMuted); + } + + private List getDirectChatRooms(Integer userId) { User user = userRepository.getById(userId); - List chatRoomResponses = new ArrayList<>(); + List roomSummaries = new ArrayList<>(); List personalChatRooms = chatRoomRepository.findByUserId(userId); Map personalUnreadCountMap = getUnreadCountMap(extractChatRoomIds(personalChatRooms), userId); @@ -86,37 +226,56 @@ public ChatRoomsResponse getChatRooms(Integer userId) { Map adminUnreadCountMap = getAdminUnreadCountMap(extractChatRoomIds(adminChatRooms)); for (ChatRoom chatRoom : adminChatRooms) { - chatRoomResponses.add( - ChatRoomsResponse.InnerChatRoomResponse.fromForAdmin(chatRoom, adminUnreadCountMap) - ); + User nonAdminUser = chatRoom.getNonAdminUser(); + roomSummaries.add(new ChatRoomSummaryResponse( + chatRoom.getId(), + ChatType.DIRECT, + nonAdminUser.getName(), + nonAdminUser.getImageUrl(), + chatRoom.getLastMessageContent(), + chatRoom.getLastMessageSentAt(), + adminUnreadCountMap.getOrDefault(chatRoom.getId(), 0), + false + )); addedChatRoomIds.add(chatRoom.getId()); } } for (ChatRoom chatRoom : personalChatRooms) { if (!addedChatRoomIds.contains(chatRoom.getId())) { - ChatRoomType type = isAdminChatRoom(chatRoom) ? ChatRoomType.ADMIN : ChatRoomType.NORMAL; - chatRoomResponses.add( - ChatRoomsResponse.InnerChatRoomResponse.from(chatRoom, user, personalUnreadCountMap, type) - ); + User chatPartner = chatRoom.getChatPartner(user); + roomSummaries.add(new ChatRoomSummaryResponse( + chatRoom.getId(), + ChatType.DIRECT, + chatPartner.getName(), + chatPartner.getImageUrl(), + chatRoom.getLastMessageContent(), + chatRoom.getLastMessageSentAt(), + personalUnreadCountMap.getOrDefault(chatRoom.getId(), 0), + false + )); } } - chatRoomResponses.sort(Comparator + roomSummaries.sort(Comparator .comparing( - ChatRoomsResponse.InnerChatRoomResponse::lastSentTime, + ChatRoomSummaryResponse::lastSentAt, Comparator.nullsLast(Comparator.reverseOrder()) ) - .thenComparing(ChatRoomsResponse.InnerChatRoomResponse::chatRoomId)); + .thenComparing(ChatRoomSummaryResponse::roomId)); - return ChatRoomsResponse.of(chatRoomResponses); + return roomSummaries; } - @Transactional - public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, Integer page, Integer limit) { + private ChatMessagesResponse getDirectChatRoomMessages( + Integer userId, + Integer roomId, + Integer page, + Integer limit + ) { ChatRoom chatRoom = getDirectRoom(roomId); User user = userRepository.getById(userId); - validateChatRoomAccess(userId, chatRoom); + validateDirectAccess(chatRoom, userId); ChatRoomMember member = getRoomMember(roomId, userId); LocalDateTime readAt = LocalDateTime.now(); @@ -130,11 +289,10 @@ public ChatMessagesResponse getChatRoomMessages(Integer userId, Integer roomId, return ChatMessagesResponse.from(messages, userId, maskedAdminId, readAt); } - @Transactional - public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + private ChatMessageResponse sendDirectMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { ChatRoom chatRoom = getDirectRoom(roomId); User sender = userRepository.getById(userId); - validateChatRoomAccess(userId, chatRoom); + validateDirectAccess(chatRoom, userId); User receiver = getMessageReceiver(sender, chatRoom); @@ -150,6 +308,134 @@ public ChatMessageResponse sendMessage(Integer userId, Integer roomId, ChatMessa return ChatMessageResponse.from(chatMessage, userId); } + private ClubRoomListResponse getClubChatRooms(Integer userId) { + List memberships = clubMemberRepository.findAllByUserId(userId); + if (memberships.isEmpty()) { + return ClubRoomListResponse.from(List.of(), Map.of(), Map.of()); + } + + Map membershipByClubId = memberships.stream() + .collect(Collectors.toMap(cm -> cm.getClub().getId(), cm -> cm, (a, b) -> a)); + + List rooms = memberships.stream() + .map(ClubMember::getClub) + .map(this::resolveOrCreateClubRoom) + .toList(); + + for (ChatRoom room : rooms) { + ClubMember member = membershipByClubId.get(room.getClub().getId()); + if (member != null) { + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } + } + + List roomIds = rooms.stream().map(ChatRoom::getId).toList(); + Map lastMessageMap = getLastMessageMap(roomIds); + Map unreadCountMap = getRoomUnreadCountMap(roomIds, userId); + + return ClubRoomListResponse.from(rooms, lastMessageMap, unreadCountMap); + } + + private ClubMessagePageResponse getClubMessagesByRoomId( + Integer roomId, + Integer userId, + Integer page, + Integer limit + ) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + syncClubMembers(room); + + chatPresenceService.recordPresence(roomId, userId); + updateLastReadAt(roomId, userId, LocalDateTime.now()); + + PageRequest pageable = PageRequest.of(page - 1, limit); + long totalCount = chatMessageRepository.countByChatRoomId(roomId); + Page messagePage = chatMessageRepository.findByChatRoomId(roomId, pageable); + List messages = messagePage.getContent(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + + List responseMessages = messages.stream() + .map(message -> { + int unreadCount = getUnreadCount(message, members); + return new ClubMessageResponse( + message.getId(), + message.getSender().getId(), + message.getSender().getName(), + message.getContent(), + message.getCreatedAt(), + unreadCount, + message.isSentBy(userId) + ); + }) + .toList(); + + int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; + return new ClubMessagePageResponse( + totalCount, + responseMessages.size(), + totalPage, + page, + room.getClub().getId(), + responseMessages + ); + } + + private ClubMessageResponse sendClubMessageByRoomId(Integer roomId, Integer userId, String content) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + User sender = member.getUser(); + + ensureRoomMember(room, sender, member.getCreatedAt()); + syncClubMembers(room); + + ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); + room.updateLastMessage(message.getContent(), message.getCreatedAt()); + updateLastReadAt(roomId, userId, message.getCreatedAt()); + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); + + notificationService.sendGroupChatNotification( + roomId, + sender.getId(), + room.getClub().getName(), + sender.getName(), + message.getContent(), + recipientUserIds + ); + + return new ClubMessageResponse( + message.getId(), + sender.getId(), + sender.getName(), + message.getContent(), + message.getCreatedAt(), + getUnreadCount(message, members), + true + ); + } + + private Map getMuteMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + List settings = notificationMuteSettingRepository + .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); + + Map muteMap = new HashMap<>(); + for (NotificationMuteSetting setting : settings) { + Integer targetId = setting.getTargetId(); + if (targetId != null) { + muteMap.put(targetId, setting.getIsMuted()); + } + } + + return muteMap; + } + private ChatRoom getDirectRoom(Integer roomId) { ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); @@ -161,6 +447,27 @@ private ChatRoom getDirectRoom(Integer roomId) { return chatRoom; } + private ChatRoom getClubRoom(Integer roomId) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); + if (!room.isGroupRoom() || room.getClub() == null) { + throw CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM); + } + return room; + } + + private ChatRoom resolveOrCreateClubRoom(Club club) { + return chatRoomRepository.findByClubId(club.getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); + } + + private void syncClubMembers(ChatRoom room) { + List clubMembers = clubMemberRepository.findAllByClubId(room.getClub().getId()); + for (ClubMember clubMember : clubMembers) { + ensureRoomMember(room, clubMember.getUser(), clubMember.getCreatedAt()); + } + } + private List extractChatRoomIds(List chatRooms) { return chatRooms.stream() .map(ChatRoom::getId) @@ -220,12 +527,6 @@ private boolean isAdminChatRoom(ChatRoom chatRoom) { || chatRoom.getReceiver().getRole() == UserRole.ADMIN; } - private void validateChatRoomAccess(Integer userId, ChatRoom chatRoom) { - if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(chatRoom.getId(), userId)) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - } - private User getMessageReceiver(User sender, ChatRoom chatRoom) { if (sender.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { return chatRoom.getNonAdminUser(); @@ -244,11 +545,13 @@ private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); } - private void ensureRoomMember(ChatRoom room, User user, LocalDateTime lastReadAt) { + private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) - .ifPresentOrElse(member -> member.updateLastReadAt(lastReadAt), () -> { - chatRoomMemberRepository.save(ChatRoomMember.of(room, user, lastReadAt)); - }); + .ifPresentOrElse(member -> { + if (member.getLastReadAt().isBefore(joinedAt)) { + member.updateLastReadAt(joinedAt); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); } private void updateMemberLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { @@ -260,4 +563,117 @@ private void updateMemberLastReadAt(Integer roomId, Integer userId, LocalDateTim ensureRoomMember(room, user, lastReadAt); } } + + private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = getClubRoom(roomId); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + ensureRoomMember(room, member.getUser(), member.getCreatedAt()); + } + } + + private int getUnreadCount(ChatMessage message, List members) { + LocalDateTime createdAt = message.getCreatedAt(); + int unreadCount = 0; + + for (ChatRoomMember member : members) { + LocalDateTime baseline = member.getLastReadAt(); + if (baseline == null || baseline.isBefore(member.getCreatedAt())) { + baseline = member.getCreatedAt(); + } + if (baseline.isBefore(createdAt)) { + unreadCount += 1; + } + } + + return unreadCount; + } + + private Map getLastMessageMap(List roomIds) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + return chatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() + .collect(Collectors.toMap(message -> message.getChatRoom().getId(), message -> message)); + } + + private Map getRoomUnreadCountMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + Map unreadCountMap = new HashMap<>(); + List projections = chatRoomMemberRepository.countUnreadByRoomIdsAndUserId( + roomIds, + userId + ); + for (RoomUnreadCountProjection projection : projections) { + unreadCountMap.put(projection.getRoomId(), projection.getUnreadCount().intValue()); + } + + Set existingRoomIds = unreadCountMap.keySet(); + for (Integer roomId : roomIds) { + if (!existingRoomIds.contains(roomId)) { + unreadCountMap.put(roomId, 0); + } + } + + return unreadCountMap; + } + + private void validateDirectAccess(ChatRoom chatRoom, Integer userId) { + if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(chatRoom.getId(), userId)) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + } + + private ChatMessagePageResponse toMessagePageResponse(ChatMessagesResponse response) { + List messages = response.messages().stream() + .map(message -> new ChatMessageDetailResponse( + message.messageId(), + message.senderId(), + null, + message.content(), + message.createdAt(), + message.isRead(), + null, + message.isMine() + )) + .toList(); + + return new ChatMessagePageResponse( + response.totalCount(), + response.currentCount(), + response.totalPage(), + response.currentPage(), + null, + messages + ); + } + + private ChatMessagePageResponse toMessagePageResponse(ClubMessagePageResponse response) { + List messages = response.messages().stream() + .map(message -> new ChatMessageDetailResponse( + message.messageId(), + message.senderId(), + message.senderName(), + message.content(), + message.createdAt(), + null, + message.unreadCount(), + message.isMine() + )) + .toList(); + + return new ChatMessagePageResponse( + response.totalCount(), + response.currentCount(), + response.totalPage(), + response.currentPage(), + response.clubId(), + messages + ); + } } diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java deleted file mode 100644 index 60c4bfc45..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/service/ClubChatService.java +++ /dev/null @@ -1,296 +0,0 @@ -package gg.agit.konect.domain.chat.service; - -import java.time.LocalDateTime; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; -import gg.agit.konect.domain.chat.dto.ClubMessageResponse; -import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; -import gg.agit.konect.domain.chat.dto.ClubRoomResponse; -import gg.agit.konect.domain.chat.model.ChatMessage; -import gg.agit.konect.domain.chat.model.ChatRoom; -import gg.agit.konect.domain.chat.model.ChatRoomMember; -import gg.agit.konect.domain.chat.repository.ChatMessageRepository; -import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; -import gg.agit.konect.domain.chat.repository.ChatRoomRepository; -import gg.agit.konect.domain.chat.repository.RoomUnreadCountProjection; -import gg.agit.konect.domain.club.model.Club; -import gg.agit.konect.domain.club.model.ClubMember; -import gg.agit.konect.domain.club.repository.ClubMemberRepository; -import gg.agit.konect.domain.notification.enums.NotificationTargetType; -import gg.agit.konect.domain.notification.model.NotificationMuteSetting; -import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; -import gg.agit.konect.domain.notification.service.NotificationService; -import gg.agit.konect.domain.user.model.User; -import gg.agit.konect.domain.user.repository.UserRepository; -import gg.agit.konect.global.code.ApiResponseCode; -import gg.agit.konect.global.exception.CustomException; -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ClubChatService { - - private final ChatRoomRepository chatRoomRepository; - private final ChatMessageRepository chatMessageRepository; - private final ChatRoomMemberRepository chatRoomMemberRepository; - private final NotificationMuteSettingRepository notificationMuteSettingRepository; - private final ClubMemberRepository clubMemberRepository; - private final UserRepository userRepository; - private final ChatPresenceService chatPresenceService; - private final NotificationService notificationService; - - @Transactional - public ClubRoomResponse getClubRoom(Integer clubId, Integer userId) { - ClubMember member = clubMemberRepository.getByClubIdAndUserId(clubId, userId); - ChatRoom room = resolveOrCreateClubRoom(member.getClub()); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - return new ClubRoomResponse(room.getId()); - } - - @Transactional - public ClubRoomListResponse getChatRooms(Integer userId) { - List memberships = clubMemberRepository.findAllByUserId(userId); - if (memberships.isEmpty()) { - return ClubRoomListResponse.from(List.of(), Map.of(), Map.of()); - } - - Map membershipByClubId = memberships.stream() - .collect(Collectors.toMap(cm -> cm.getClub().getId(), cm -> cm, (a, b) -> a)); - - List rooms = memberships.stream() - .map(ClubMember::getClub) - .map(this::resolveOrCreateClubRoom) - .toList(); - - for (ChatRoom room : rooms) { - ClubMember member = membershipByClubId.get(room.getClub().getId()); - if (member != null) { - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - } - } - - List roomIds = rooms.stream().map(ChatRoom::getId).toList(); - Map lastMessageMap = getLastMessageMap(roomIds); - Map unreadCountMap = getUnreadCountMap(roomIds, userId); - - return ClubRoomListResponse.from(rooms, lastMessageMap, unreadCountMap); - } - - @Transactional - public ClubMessagePageResponse getMessagesByRoomId(Integer roomId, Integer userId, Integer page, Integer limit) { - ChatRoom room = getClubRoom(roomId); - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - syncClubMembers(room); - - chatPresenceService.recordPresence(roomId, userId); - updateLastReadAt(roomId, userId, LocalDateTime.now()); - - PageRequest pageable = PageRequest.of(page - 1, limit); - long totalCount = chatMessageRepository.countByChatRoomId(roomId); - Page messagePage = chatMessageRepository.findByChatRoomId(roomId, pageable); - List messages = messagePage.getContent(); - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - - List responseMessages = messages.stream() - .map(message -> { - int unreadCount = getUnreadCount(message, members); - return new ClubMessageResponse( - message.getId(), - message.getSender().getId(), - message.getSender().getName(), - message.getContent(), - message.getCreatedAt(), - unreadCount, - message.isSentBy(userId) - ); - }) - .toList(); - - int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new ClubMessagePageResponse( - totalCount, - responseMessages.size(), - totalPage, - page, - room.getClub().getId(), - responseMessages - ); - } - - @Transactional - public ClubMessageResponse sendMessageByRoomId(Integer roomId, Integer userId, String content) { - ChatRoom room = getClubRoom(roomId); - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - User sender = member.getUser(); - - ensureRoomMember(room, sender, member.getCreatedAt()); - syncClubMembers(room); - - ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); - room.updateLastMessage(message.getContent(), message.getCreatedAt()); - updateLastReadAt(roomId, userId, message.getCreatedAt()); - - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); - - notificationService.sendGroupChatNotification( - roomId, - sender.getId(), - room.getClub().getName(), - sender.getName(), - message.getContent(), - recipientUserIds - ); - - return new ClubMessageResponse( - message.getId(), - sender.getId(), - sender.getName(), - message.getContent(), - message.getCreatedAt(), - getUnreadCount(message, members), - true - ); - } - - @Transactional - public Boolean toggleMute(Integer userId, Integer chatRoomId) { - ChatRoom room = chatRoomRepository.findById(chatRoomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); - - if (room.isGroupRoom()) { - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - } else { - validateDirectAccess(room, userId); - } - - User user = userRepository.getById(userId); - return notificationMuteSettingRepository.findByTargetTypeAndTargetIdAndUserId( - NotificationTargetType.CHAT_ROOM, - chatRoomId, - userId - ) - .map(setting -> { - setting.toggleMute(); - notificationMuteSettingRepository.save(setting); - return setting.getIsMuted(); - }) - .orElseGet(() -> { - notificationMuteSettingRepository.save(NotificationMuteSetting.of( - NotificationTargetType.CHAT_ROOM, - chatRoomId, - user, - true - )); - return true; - }); - } - - private ChatRoom getClubRoom(Integer roomId) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); - if (!room.isGroupRoom() || room.getClub() == null) { - throw CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM); - } - return room; - } - - private ChatRoom resolveOrCreateClubRoom(Club club) { - return chatRoomRepository.findByClubId(club.getId()) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); - } - - private void syncClubMembers(ChatRoom room) { - List clubMembers = clubMemberRepository.findAllByClubId(room.getClub().getId()); - for (ClubMember clubMember : clubMembers) { - ensureRoomMember(room, clubMember.getUser(), clubMember.getCreatedAt()); - } - } - - private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { - chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) - .ifPresentOrElse(member -> { - if (member.getLastReadAt().isBefore(joinedAt)) { - member.updateLastReadAt(joinedAt); - } - }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); - } - - private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); - if (updated == 0) { - ChatRoom room = getClubRoom(roomId); - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - } - } - - private void validateDirectAccess(ChatRoom room, Integer userId) { - if (!chatRoomMemberRepository.existsByChatRoomIdAndUserId(room.getId(), userId)) { - throw CustomException.of(ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS); - } - } - - private int getUnreadCount(ChatMessage message, List members) { - LocalDateTime createdAt = message.getCreatedAt(); - int unreadCount = 0; - - for (ChatRoomMember member : members) { - LocalDateTime baseline = member.getLastReadAt(); - if (baseline == null || baseline.isBefore(member.getCreatedAt())) { - baseline = member.getCreatedAt(); - } - if (baseline.isBefore(createdAt)) { - unreadCount += 1; - } - } - - return unreadCount; - } - - private Map getLastMessageMap(List roomIds) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - return chatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() - .collect(Collectors.toMap(message -> message.getChatRoom().getId(), message -> message)); - } - - private Map getUnreadCountMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - Map unreadCountMap = new HashMap<>(); - List projections = chatRoomMemberRepository.countUnreadByRoomIdsAndUserId( - roomIds, - userId - ); - for (RoomUnreadCountProjection projection : projections) { - unreadCountMap.put(projection.getRoomId(), projection.getUnreadCount().intValue()); - } - - Set existingRoomIds = unreadCountMap.keySet(); - for (Integer roomId : roomIds) { - if (!existingRoomIds.contains(roomId)) { - unreadCountMap.put(roomId, 0); - } - } - - return unreadCountMap; - } -} From 276e4d928775d51940d04a98f8e223443a6ac7d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:18:49 +0900 Subject: [PATCH 11/26] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/dto/ChatRoomsResponse.java | 84 ------------------- .../domain/chat/enums/ChatRoomType.java | 6 -- 2 files changed, 90 deletions(-) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java deleted file mode 100644 index 0be0c6d06..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsResponse.java +++ /dev/null @@ -1,84 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.model.ChatRoom; -import gg.agit.konect.domain.chat.enums.ChatRoomType; -import gg.agit.konect.domain.user.model.User; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatRoomsResponse( - @Schema(description = "채팅방 리스트", requiredMode = REQUIRED) - List chatRooms -) { - public record InnerChatRoomResponse( - @Schema(description = "채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer chatRoomId, - - @Schema(description = "상대방 이름", example = "김혜준", requiredMode = REQUIRED) - String chatPartnerName, - - @Schema(description = "상대방 프로필 사진", example = "https://bcsdlab.com/static/img/logo.d89d9cc.png", requiredMode = REQUIRED) - String chatPartnerProfileImage, - - @Schema(description = "마지막 메시지", example = "지원 어디서 해요", requiredMode = NOT_REQUIRED) - String lastMessage, - - @Schema(description = "마지막 메시지 전송 시간", example = "2025.12.19 23:21", requiredMode = NOT_REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm") - LocalDateTime lastSentTime, - - @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) - Integer unreadCount, - - @Schema(description = "채팅방 타입", example = "NORMAL", requiredMode = REQUIRED) - ChatRoomType chatRoomType - ) { - public static InnerChatRoomResponse from( - ChatRoom chatRoom, - User currentUser, - Map unreadCountMap, - ChatRoomType chatRoomType - ) { - User chatPartner = chatRoom.getChatPartner(currentUser); - - return new InnerChatRoomResponse( - chatRoom.getId(), - chatPartner.getName(), - chatPartner.getImageUrl(), - chatRoom.getLastMessageContent(), - chatRoom.getLastMessageSentAt(), - unreadCountMap.getOrDefault(chatRoom.getId(), 0), - chatRoomType - ); - } - - public static InnerChatRoomResponse fromForAdmin( - ChatRoom chatRoom, - Map unreadCountMap - ) { - User nonAdminUser = chatRoom.getNonAdminUser(); - - return new InnerChatRoomResponse( - chatRoom.getId(), - nonAdminUser.getName(), - nonAdminUser.getImageUrl(), - chatRoom.getLastMessageContent(), - chatRoom.getLastMessageSentAt(), - unreadCountMap.getOrDefault(chatRoom.getId(), 0), - ChatRoomType.ADMIN - ); - } - } - - public static ChatRoomsResponse of(List chatRooms) { - return new ChatRoomsResponse(chatRooms); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java b/src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java deleted file mode 100644 index 70f99ebac..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/enums/ChatRoomType.java +++ /dev/null @@ -1,6 +0,0 @@ -package gg.agit.konect.domain.chat.enums; - -public enum ChatRoomType { - NORMAL, - ADMIN -} From 26ff535faf6b47f60ff0bdc6ada2c2185b1a8268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:21:18 +0900 Subject: [PATCH 12/26] =?UTF-8?q?refactor:=201=EB=8C=801,=20=EB=8B=A8?= =?UTF-8?q?=EC=B2=B4=20=EC=B1=84=ED=8C=85=20DTO=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/ClubMessagePageResponse.java | 29 ------ .../domain/chat/dto/ClubMessageResponse.java | 34 ------- .../domain/chat/dto/ClubRoomListResponse.java | 71 --------------- .../domain/chat/dto/ClubRoomResponse.java | 11 --- .../domain/chat/service/ChatService.java | 88 +++++++------------ 5 files changed, 34 insertions(+), 199 deletions(-) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java deleted file mode 100644 index 130e86a5c..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessagePageResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.util.List; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record ClubMessagePageResponse( - @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) - Long totalCount, - - @Schema(description = "현재 페이지에서 조회된 메시지 수", example = "20", requiredMode = REQUIRED) - Integer currentCount, - - @Schema(description = "최대 페이지", example = "5", requiredMode = REQUIRED) - Integer totalPage, - - @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) - Integer currentPage, - - @Schema(description = "동아리 ID", example = "1", requiredMode = NOT_REQUIRED) - Integer clubId, - - @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages -) { -} diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java deleted file mode 100644 index 491d47a00..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ClubMessageResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record ClubMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "발신자 이름", example = "홍길동", requiredMode = REQUIRED) - String senderName, - - @Schema(description = "메시지 내용", example = "안녕하세요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "미확인 인원 수", example = "3", requiredMode = REQUIRED) - Integer unreadCount, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine -) { -} diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java deleted file mode 100644 index 6e25ff5d7..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomListResponse.java +++ /dev/null @@ -1,71 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.model.ChatMessage; -import gg.agit.konect.domain.chat.model.ChatRoom; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ClubRoomListResponse( - @Schema(description = "단체 채팅방 리스트", requiredMode = REQUIRED) - List clubRooms -) { - public record InnerClubRoomResponse( - @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer roomId, - - @Schema(description = "동아리 이름", example = "BCSD Lab", requiredMode = REQUIRED) - String clubName, - - @Schema(description = "동아리 이미지 URL", example = "https://bcsdlab.com/static/img/logo.d89d9cc.png", requiredMode = REQUIRED) - String clubImageUrl, - - @Schema(description = "마지막 메시지", example = "안녕하세요!", requiredMode = NOT_REQUIRED) - String lastMessage, - - @Schema(description = "마지막 메시지 전송 시간", example = "2025.12.19 23:21", requiredMode = NOT_REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm") - LocalDateTime lastSentAt, - - @Schema(description = "읽지 않은 메시지 개수", example = "12", requiredMode = REQUIRED) - Integer unreadMessageCount - ) { - public static InnerClubRoomResponse from( - ChatRoom room, - ChatMessage lastMessage, - Map unreadCountMap - ) { - return new InnerClubRoomResponse( - room.getId(), - room.getClub().getName(), - room.getClub().getImageUrl(), - lastMessage != null ? lastMessage.getContent() : null, - lastMessage != null ? lastMessage.getCreatedAt() : null, - unreadCountMap.getOrDefault(room.getId(), 0) - ); - } - } - - public static ClubRoomListResponse from( - List rooms, - Map lastMessageMap, - Map unreadCountMap - ) { - return new ClubRoomListResponse( - rooms.stream() - .map(room -> InnerClubRoomResponse.from( - room, - lastMessageMap.get(room.getId()), - unreadCountMap - )) - .toList() - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java deleted file mode 100644 index 8e1f03909..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ClubRoomResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record ClubRoomResponse( - @Schema(description = "단체 채팅방 ID", example = "1", requiredMode = REQUIRED) - Integer roomId -) { -} 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 0eec3b7b3..072ce549d 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 @@ -30,9 +30,6 @@ import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; -import gg.agit.konect.domain.chat.dto.ClubMessagePageResponse; -import gg.agit.konect.domain.chat.dto.ClubMessageResponse; -import gg.agit.konect.domain.chat.dto.ClubRoomListResponse; import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.enums.ChatType; import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; @@ -94,11 +91,11 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat @Transactional public ChatRoomListResponse getChatRooms(Integer userId) { List directRooms = getDirectChatRooms(userId); - ClubRoomListResponse clubRooms = getClubChatRooms(userId); + List clubRooms = getClubChatRooms(userId); List roomIds = new ArrayList<>(); roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - roomIds.addAll(clubRooms.clubRooms().stream().map(ClubRoomListResponse.InnerClubRoomResponse::roomId).toList()); + roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); Map muteMap = getMuteMap(roomIds, userId); List rooms = new ArrayList<>(); @@ -114,14 +111,14 @@ public ChatRoomListResponse getChatRooms(Integer userId) { muteMap.getOrDefault(room.roomId(), false) ))); - clubRooms.clubRooms().forEach(room -> rooms.add(new ChatRoomSummaryResponse( + clubRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( room.roomId(), - ChatType.GROUP, - room.clubName(), - room.clubImageUrl(), + room.chatType(), + room.roomName(), + room.roomImageUrl(), room.lastMessage(), room.lastSentAt(), - room.unreadMessageCount(), + room.unreadCount(), muteMap.getOrDefault(room.roomId(), false) ))); @@ -142,7 +139,7 @@ public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integ return toMessagePageResponse(getDirectChatRoomMessages(userId, roomId, page, limit)); } - return toMessagePageResponse(getClubMessagesByRoomId(roomId, userId, page, limit)); + return getClubMessagesByRoomId(roomId, userId, page, limit); } @Transactional @@ -164,17 +161,7 @@ public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, Cha ); } - ClubMessageResponse response = sendClubMessageByRoomId(roomId, userId, request.content()); - return new ChatMessageDetailResponse( - response.messageId(), - response.senderId(), - response.senderName(), - response.content(), - response.createdAt(), - null, - response.unreadCount(), - response.isMine() - ); + return sendClubMessageByRoomId(roomId, userId, request.content()); } @Transactional @@ -308,10 +295,10 @@ private ChatMessageResponse sendDirectMessage(Integer userId, Integer roomId, Ch return ChatMessageResponse.from(chatMessage, userId); } - private ClubRoomListResponse getClubChatRooms(Integer userId) { + private List getClubChatRooms(Integer userId) { List memberships = clubMemberRepository.findAllByUserId(userId); if (memberships.isEmpty()) { - return ClubRoomListResponse.from(List.of(), Map.of(), Map.of()); + return List.of(); } Map membershipByClubId = memberships.stream() @@ -333,10 +320,24 @@ private ClubRoomListResponse getClubChatRooms(Integer userId) { Map lastMessageMap = getLastMessageMap(roomIds); Map unreadCountMap = getRoomUnreadCountMap(roomIds, userId); - return ClubRoomListResponse.from(rooms, lastMessageMap, unreadCountMap); + return rooms.stream() + .map(room -> { + ChatMessage lastMessage = lastMessageMap.get(room.getId()); + return new ChatRoomSummaryResponse( + room.getId(), + ChatType.GROUP, + room.getClub().getName(), + room.getClub().getImageUrl(), + lastMessage != null ? lastMessage.getContent() : null, + lastMessage != null ? lastMessage.getCreatedAt() : null, + unreadCountMap.getOrDefault(room.getId(), 0), + false + ); + }) + .toList(); } - private ClubMessagePageResponse getClubMessagesByRoomId( + private ChatMessagePageResponse getClubMessagesByRoomId( Integer roomId, Integer userId, Integer page, @@ -356,15 +357,16 @@ private ClubMessagePageResponse getClubMessagesByRoomId( List messages = messagePage.getContent(); List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List responseMessages = messages.stream() + List responseMessages = messages.stream() .map(message -> { int unreadCount = getUnreadCount(message, members); - return new ClubMessageResponse( + return new ChatMessageDetailResponse( message.getId(), message.getSender().getId(), message.getSender().getName(), message.getContent(), message.getCreatedAt(), + null, unreadCount, message.isSentBy(userId) ); @@ -372,7 +374,7 @@ private ClubMessagePageResponse getClubMessagesByRoomId( .toList(); int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new ClubMessagePageResponse( + return new ChatMessagePageResponse( totalCount, responseMessages.size(), totalPage, @@ -382,7 +384,7 @@ private ClubMessagePageResponse getClubMessagesByRoomId( ); } - private ClubMessageResponse sendClubMessageByRoomId(Integer roomId, Integer userId, String content) { + private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Integer userId, String content) { ChatRoom room = getClubRoom(roomId); ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); User sender = member.getUser(); @@ -406,12 +408,13 @@ private ClubMessageResponse sendClubMessageByRoomId(Integer roomId, Integer user recipientUserIds ); - return new ClubMessageResponse( + return new ChatMessageDetailResponse( message.getId(), sender.getId(), sender.getName(), message.getContent(), message.getCreatedAt(), + null, getUnreadCount(message, members), true ); @@ -653,27 +656,4 @@ private ChatMessagePageResponse toMessagePageResponse(ChatMessagesResponse respo ); } - private ChatMessagePageResponse toMessagePageResponse(ClubMessagePageResponse response) { - List messages = response.messages().stream() - .map(message -> new ChatMessageDetailResponse( - message.messageId(), - message.senderId(), - message.senderName(), - message.content(), - message.createdAt(), - null, - message.unreadCount(), - message.isMine() - )) - .toList(); - - return new ChatMessagePageResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - response.clubId(), - messages - ); - } } From bc6465d6d5739631030e9aaf2772758de972fbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:25:21 +0900 Subject: [PATCH 13/26] =?UTF-8?q?refactor:=20ChatService=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=AA=A8=EB=8D=B8=20=EB=8B=A8=EC=9D=BC=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A4=91=EB=B3=B5=20DTO=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/dto/ChatMessageResponse.java | 52 --------- .../domain/chat/dto/ChatMessagesResponse.java | 107 ------------------ .../domain/chat/service/ChatService.java | 85 +++++++------- 3 files changed, 45 insertions(+), 199 deletions(-) delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java delete mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java deleted file mode 100644 index 437474c34..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessageResponse.java +++ /dev/null @@ -1,52 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.model.ChatMessage; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "메시지 내용", example = "투명 케이스가 끼워져 있었어요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "읽음 여부", example = "false", requiredMode = REQUIRED) - Boolean isRead, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine -) { - public static ChatMessageResponse from(ChatMessage message, Integer currentUserId) { - return from(message, currentUserId, null); - } - - public static ChatMessageResponse from( - ChatMessage message, - Integer currentUserId, - LocalDateTime lastReadAt - ) { - boolean isRead = message.isSentBy(currentUserId) - || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); - return new ChatMessageResponse( - message.getId(), - message.getSender().getId(), - message.getContent(), - message.getCreatedAt(), - isRead, - message.isSentBy(currentUserId) - ); - } -} diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java deleted file mode 100644 index 6fc9d6577..000000000 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ChatMessagesResponse.java +++ /dev/null @@ -1,107 +0,0 @@ -package gg.agit.konect.domain.chat.dto; - -import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; - -import java.time.LocalDateTime; -import java.util.List; - -import org.springframework.data.domain.Page; - -import com.fasterxml.jackson.annotation.JsonFormat; - -import gg.agit.konect.domain.chat.model.ChatMessage; -import gg.agit.konect.domain.user.enums.UserRole; -import io.swagger.v3.oas.annotations.media.Schema; - -public record ChatMessagesResponse( - @Schema(description = "조건에 해당하는 메시지 수", example = "100", requiredMode = REQUIRED) - Long totalCount, - - @Schema(description = "현재 페이지에서 조회된 메시지 수", example = "20", requiredMode = REQUIRED) - Integer currentCount, - - @Schema(description = "최대 페이지", example = "5", requiredMode = REQUIRED) - Integer totalPage, - - @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) - Integer currentPage, - - @Schema(description = "채팅 메시지 리스트", requiredMode = REQUIRED) - List messages -) { - public record InnerChatMessageResponse( - @Schema(description = "메시지 ID", example = "505", requiredMode = REQUIRED) - Integer messageId, - - @Schema(description = "발신자 ID", example = "1", requiredMode = REQUIRED) - Integer senderId, - - @Schema(description = "메시지 내용", example = "투명 케이스가 끼워져 있었어요!", requiredMode = REQUIRED) - String content, - - @Schema(description = "메시지 전송 시간", example = "2025.07.23 15:53:12.123", requiredMode = REQUIRED) - @JsonFormat(pattern = "yyyy.MM.dd HH:mm:ss.SSS") - LocalDateTime createdAt, - - @Schema(description = "읽음 여부", example = "false", requiredMode = REQUIRED) - Boolean isRead, - - @Schema(description = "내가 보낸 메시지 여부", example = "true", requiredMode = REQUIRED) - Boolean isMine - ) { - public static InnerChatMessageResponse from( - ChatMessage message, - Integer currentUserId, - Integer maskedAdminId, - LocalDateTime lastReadAt - ) { - Integer senderId = resolveSenderId(message, maskedAdminId); - boolean isMine = message.isSentBy(currentUserId); - boolean isRead = isMine || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); - return new InnerChatMessageResponse( - message.getId(), - senderId, - message.getContent(), - message.getCreatedAt(), - isRead, - isMine - ); - } - - private static Integer resolveSenderId(ChatMessage message, Integer maskedAdminId) { - if (maskedAdminId != null && message.getSender().getRole() == UserRole.ADMIN) { - return maskedAdminId; - } - return message.getSender().getId(); - } - } - - public static ChatMessagesResponse from( - Page page, - Integer currentUserId, - Integer maskedAdminId, - LocalDateTime lastReadAt - ) { - return new ChatMessagesResponse( - page.getTotalElements(), - page.getNumberOfElements(), - page.getTotalPages(), - page.getNumber() + 1, - page.stream() - .map(message -> { - Integer senderId = InnerChatMessageResponse.resolveSenderId(message, maskedAdminId); - boolean isMine = message.isSentBy(currentUserId); - boolean isRead = isMine || lastReadAt != null && !message.getCreatedAt().isAfter(lastReadAt); - return new InnerChatMessageResponse( - message.getId(), - senderId, - message.getContent(), - message.getCreatedAt(), - isRead, - isMine - ); - }) - .toList() - ); - } -} 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 072ce549d..30554d265 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 @@ -22,9 +22,7 @@ import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; -import gg.agit.konect.domain.chat.dto.ChatMessageResponse; import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; -import gg.agit.konect.domain.chat.dto.ChatMessagesResponse; import gg.agit.konect.domain.chat.dto.ChatMuteResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; @@ -136,7 +134,7 @@ public ChatMessagePageResponse getMessages(Integer userId, Integer roomId, Integ .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); if (room.isDirectRoom()) { - return toMessagePageResponse(getDirectChatRoomMessages(userId, roomId, page, limit)); + return getDirectChatRoomMessages(userId, roomId, page, limit); } return getClubMessagesByRoomId(roomId, userId, page, limit); @@ -148,17 +146,7 @@ public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, Cha .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); if (room.isDirectRoom()) { - ChatMessageResponse response = sendDirectMessage(userId, roomId, request); - return new ChatMessageDetailResponse( - response.messageId(), - response.senderId(), - null, - response.content(), - response.createdAt(), - response.isRead(), - null, - response.isMine() - ); + return sendDirectMessage(userId, roomId, request); } return sendClubMessageByRoomId(roomId, userId, request.content()); @@ -254,7 +242,7 @@ private List getDirectChatRooms(Integer userId) { return roomSummaries; } - private ChatMessagesResponse getDirectChatRoomMessages( + private ChatMessagePageResponse getDirectChatRoomMessages( Integer userId, Integer roomId, Integer page, @@ -273,10 +261,35 @@ private ChatMessagesResponse getDirectChatRoomMessages( Page messages = chatMessageRepository.findByChatRoomId(roomId, pageable); Integer maskedAdminId = getMaskedAdminId(user, chatRoom); - return ChatMessagesResponse.from(messages, userId, maskedAdminId, readAt); + List responseMessages = messages.getContent().stream() + .map(message -> { + Integer senderId = resolveDirectSenderId(message, maskedAdminId); + boolean isMine = message.isSentBy(userId); + boolean isRead = isMine || !message.getCreatedAt().isAfter(readAt); + return new ChatMessageDetailResponse( + message.getId(), + senderId, + null, + message.getContent(), + message.getCreatedAt(), + isRead, + null, + isMine + ); + }) + .toList(); + + return new ChatMessagePageResponse( + messages.getTotalElements(), + messages.getNumberOfElements(), + messages.getTotalPages(), + messages.getNumber() + 1, + null, + responseMessages + ); } - private ChatMessageResponse sendDirectMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + private ChatMessageDetailResponse sendDirectMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { ChatRoom chatRoom = getDirectRoom(roomId); User sender = userRepository.getById(userId); validateDirectAccess(chatRoom, userId); @@ -292,7 +305,16 @@ private ChatMessageResponse sendDirectMessage(Integer userId, Integer roomId, Ch notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); publishAdminChatEventIfNeeded(receiver, sender, request.content()); - return ChatMessageResponse.from(chatMessage, userId); + return new ChatMessageDetailResponse( + chatMessage.getId(), + chatMessage.getSender().getId(), + null, + chatMessage.getContent(), + chatMessage.getCreatedAt(), + true, + null, + true + ); } private List getClubChatRooms(Integer userId) { @@ -632,28 +654,11 @@ private void validateDirectAccess(ChatRoom chatRoom, Integer userId) { } } - private ChatMessagePageResponse toMessagePageResponse(ChatMessagesResponse response) { - List messages = response.messages().stream() - .map(message -> new ChatMessageDetailResponse( - message.messageId(), - message.senderId(), - null, - message.content(), - message.createdAt(), - message.isRead(), - null, - message.isMine() - )) - .toList(); - - return new ChatMessagePageResponse( - response.totalCount(), - response.currentCount(), - response.totalPage(), - response.currentPage(), - null, - messages - ); + private Integer resolveDirectSenderId(ChatMessage message, Integer maskedAdminId) { + if (maskedAdminId != null && message.getSender().getRole() == UserRole.ADMIN) { + return maskedAdminId; + } + return message.getSender().getId(); } } From 2e494ff29faf4b67ce8efc1f1345049b739465d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:32:15 +0900 Subject: [PATCH 14/26] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=9D=91=EB=8B=B5=20DTO=EB=A5=BC=20Cha?= =?UTF-8?q?tRoomsSummaryResponse=EB=A1=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/domain/chat/controller/ChatApi.java | 4 ++-- .../domain/chat/controller/ChatController.java | 6 +++--- ...stResponse.java => ChatRoomsSummaryResponse.java} | 2 +- .../agit/konect/domain/chat/service/ChatService.java | 12 ++++++++---- 4 files changed, 14 insertions(+), 10 deletions(-) rename src/main/java/gg/agit/konect/domain/chat/dto/{ChatRoomListResponse.java => ChatRoomsSummaryResponse.java} (88%) diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java index f900843a7..dca9763d0 100644 --- a/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java @@ -13,7 +13,7 @@ import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; import gg.agit.konect.domain.chat.dto.ChatMuteResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.global.auth.annotation.UserId; import io.swagger.v3.oas.annotations.Operation; @@ -53,7 +53,7 @@ ResponseEntity createOrGetChatRoom( - 최근 메시지가 있는 순서대로 정렬됩니다. """) @GetMapping("/rooms") - ResponseEntity getChatRooms( + ResponseEntity getChatRooms( @UserId Integer userId ); diff --git a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java index a37663ad2..e42402b10 100644 --- a/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java +++ b/src/main/java/gg/agit/konect/domain/chat/controller/ChatController.java @@ -11,9 +11,9 @@ import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; import gg.agit.konect.domain.chat.dto.ChatMuteResponse; -import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; @@ -36,10 +36,10 @@ public ResponseEntity createOrGetChatRoom( } @Override - public ResponseEntity getChatRooms( + public ResponseEntity getChatRooms( @UserId Integer userId ) { - ChatRoomListResponse response = chatService.getChatRooms(userId); + ChatRoomsSummaryResponse response = chatService.getChatRooms(userId); return ResponseEntity.ok(response); } diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java similarity index 88% rename from src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java rename to src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java index 64f59b79a..5193ebbad 100644 --- a/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomListResponse.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/ChatRoomsSummaryResponse.java @@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record ChatRoomListResponse( +public record ChatRoomsSummaryResponse( @Schema(description = "채팅방 리스트", requiredMode = REQUIRED) List rooms ) { 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 30554d265..53a12c4a0 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 @@ -25,9 +25,9 @@ import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.dto.ChatMuteResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.dto.ChatRoomListResponse; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.enums.ChatType; import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; @@ -87,7 +87,7 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat } @Transactional - public ChatRoomListResponse getChatRooms(Integer userId) { + public ChatRoomsSummaryResponse getChatRooms(Integer userId) { List directRooms = getDirectChatRooms(userId); List clubRooms = getClubChatRooms(userId); @@ -125,7 +125,7 @@ public ChatRoomListResponse getChatRooms(Integer userId) { .thenComparing(ChatRoomSummaryResponse::roomId) ); - return new ChatRoomListResponse(rooms); + return new ChatRoomsSummaryResponse(rooms); } @Transactional @@ -289,7 +289,11 @@ private ChatMessagePageResponse getDirectChatRoomMessages( ); } - private ChatMessageDetailResponse sendDirectMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + private ChatMessageDetailResponse sendDirectMessage( + Integer userId, + Integer roomId, + ChatMessageSendRequest request + ) { ChatRoom chatRoom = getDirectRoom(roomId); User sender = userRepository.getById(userId); validateDirectAccess(chatRoom, userId); From 0b0d8c0ee2387fd83fa4988c08e1ae2f65ea459b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:49:47 +0900 Subject: [PATCH 15/26] =?UTF-8?q?=20feat:=20=EB=8F=99=EC=95=84=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EB=A9=A4=EB=B2=84=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80/=EC=A0=9C=EA=B1=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ChatRoomMemberRepository.java | 11 ++++ .../service/ChatRoomMembershipService.java | 50 +++++++++++++++++++ .../club/service/ClubApplicationService.java | 5 +- .../service/ClubMemberManagementService.java | 4 ++ .../domain/club/service/ClubService.java | 5 +- .../domain/user/service/UserService.java | 10 +++- 6 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java index 0559d5ca9..689f80eea 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java @@ -66,6 +66,17 @@ int updateLastReadAtIfOlder( @Param("lastReadAt") LocalDateTime lastReadAt ); + @Modifying(clearAutomatically = true) + @Query(""" + DELETE FROM ChatRoomMember crm + WHERE crm.id.chatRoomId = :chatRoomId + AND crm.id.userId = :userId + """) + int deleteByChatRoomIdAndUserId( + @Param("chatRoomId") Integer chatRoomId, + @Param("userId") Integer userId + ); + @Query(value = """ SELECT crm.chat_room_id AS roomId, COUNT(cm.id) AS unreadCount FROM chat_room_member crm 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 new file mode 100644 index 000000000..cbe413998 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomMembershipService.java @@ -0,0 +1,50 @@ +package gg.agit.konect.domain.chat.service; + +import java.time.LocalDateTime; + +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.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.club.model.Club; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.user.model.User; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatRoomMembershipService { + + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + + @Transactional + public void addClubMember(ClubMember clubMember) { + addClubMember(clubMember.getClub(), clubMember.getUser(), clubMember.getCreatedAt()); + } + + @Transactional + public void addClubMember(Club club, User user, LocalDateTime joinedAt) { + ChatRoom room = chatRoomRepository.findByClubId(club.getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); + + LocalDateTime baseline = joinedAt != null ? joinedAt : LocalDateTime.now(); + + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + if (member.getLastReadAt().isBefore(baseline)) { + member.updateLastReadAt(baseline); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, baseline))); + } + + @Transactional + public void removeClubMember(Integer clubId, Integer userId) { + chatRoomRepository.findByClubId(clubId) + .ifPresent(room -> chatRoomMemberRepository.deleteByChatRoomIdAndUserId(room.getId(), userId)); + } +} diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java index 47e3c7a1b..f21d26782 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubApplicationService.java @@ -18,6 +18,7 @@ import org.springframework.util.StringUtils; import gg.agit.konect.domain.bank.repository.BankRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubApplicationAnswersResponse; import gg.agit.konect.domain.club.dto.ClubApplicationCondition; import gg.agit.konect.domain.club.dto.ClubApplicationsResponse; @@ -63,6 +64,7 @@ public class ClubApplicationService { private final BankRepository bankRepository; private final ClubPermissionValidator clubPermissionValidator; private final NotificationService notificationService; + private final ChatRoomMembershipService chatRoomMembershipService; public ClubAppliedClubsResponse getAppliedClubs(Integer userId) { List clubApplies = clubApplyRepository.findAllPendingByUserIdWithClub(userId); @@ -121,7 +123,8 @@ public void approveClubApplication(Integer clubId, Integer applicationId, Intege .isFeePaid(true) .build(); - clubMemberRepository.save(newMember); + ClubMember savedMember = clubMemberRepository.save(newMember); + chatRoomMembershipService.addClubMember(savedMember); clubApplyRepository.delete(clubApply); notificationService.sendClubApplicationApprovedNotification( diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java index 10ee73400..acf2bba40 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubMemberManagementService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubPreMemberAddRequest; import gg.agit.konect.domain.club.dto.ClubPreMemberAddResponse; import gg.agit.konect.domain.club.dto.ClubPreMembersResponse; @@ -40,6 +41,7 @@ public class ClubMemberManagementService { private final ClubPreMemberRepository clubPreMemberRepository; private final ClubPermissionValidator clubPermissionValidator; private final UserRepository userRepository; + private final ChatRoomMembershipService chatRoomMembershipService; @Transactional public ClubMember changeMemberPosition( @@ -118,6 +120,7 @@ private ClubPreMemberAddResponse addDirectMember(Club club, User user, ClubPosit .build(); ClubMember savedMember = clubMemberRepository.save(clubMember); + chatRoomMembershipService.addClubMember(savedMember); return ClubPreMemberAddResponse.from(savedMember); } @@ -257,6 +260,7 @@ public void removeMember(Integer clubId, Integer targetUserId, Integer requester } clubMemberRepository.delete(target); + chatRoomMembershipService.removeClubMember(clubId, targetUserId); } private void validateNotSelf(Integer userId1, Integer userId2, ApiResponseCode errorCode) { diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index e4ec3a88e..3f932ccd4 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -16,6 +16,7 @@ import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.dto.ClubBasicInfoUpdateRequest; import gg.agit.konect.domain.club.dto.ClubCondition; import gg.agit.konect.domain.club.dto.ClubCreateRequest; @@ -52,6 +53,7 @@ public class ClubService { private final UserRepository userRepository; private final ClubPermissionValidator clubPermissionValidator; private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMembershipService chatRoomMembershipService; public ClubsResponse getClubs(ClubCondition condition, Integer userId) { User user = userRepository.getById(userId); @@ -116,7 +118,8 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) .isFeePaid(false) .build(); - clubMemberRepository.save(president); + ClubMember savedPresident = clubMemberRepository.save(president); + chatRoomMembershipService.addClubMember(savedClub, user, savedPresident.getCreatedAt()); return getClubDetail(savedClub.getId(), userId); } diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index 90ff0f060..d5223fc5b 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -15,6 +15,7 @@ import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.club.model.ClubPreMember; import gg.agit.konect.domain.club.repository.ClubMemberRepository; @@ -58,6 +59,7 @@ public class UserService { private final StudyTimeQueryService studyTimeQueryService; private final ApplicationEventPublisher applicationEventPublisher; private final AppleTokenRevocationService appleTokenRevocationService; + private final ChatRoomMembershipService chatRoomMembershipService; @Transactional public Integer signup(String email, String providerId, Provider provider, SignupRequest request) { @@ -161,7 +163,8 @@ private void joinPreMembers(User user, Integer universityId, String studentNumbe .isFeePaid(false) .build(); - clubMemberRepository.save(clubMember); + ClubMember savedMember = clubMemberRepository.save(clubMember); + chatRoomMembershipService.addClubMember(savedMember); } clubPreMemberRepository.deleteAll(preMembers); @@ -170,7 +173,10 @@ private void joinPreMembers(User user, Integer universityId, String studentNumbe private void replaceCurrentPresident(Integer clubId, Integer newPresidentUserId) { clubMemberRepository.findPresidentByClubId(clubId) .filter(currentPresident -> !currentPresident.getId().getUserId().equals(newPresidentUserId)) - .ifPresent(clubMemberRepository::delete); + .ifPresent(currentPresident -> { + clubMemberRepository.delete(currentPresident); + chatRoomMembershipService.removeClubMember(clubId, currentPresident.getUser().getId()); + }); } public UserInfoResponse getUserInfo(Integer userId) { From 664623c3dc2610b5d358162d6cec636572070a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Tue, 17 Feb 2026 23:58:34 +0900 Subject: [PATCH 16/26] =?UTF-8?q?refactor:=20chat=5Froom=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=9D=98=20sender=5Fid,=20receiver=5Fid=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/chat/model/ChatRoom.java | 43 +-------- .../repository/ChatMessageRepository.java | 11 +++ .../repository/ChatRoomMemberRepository.java | 8 ++ .../chat/repository/ChatRoomRepository.java | 60 ++---------- .../domain/chat/service/ChatService.java | 95 ++++++++++++++----- .../V26__add_unified_chat_room_table.sql | 50 +++++++++- 6 files changed, 143 insertions(+), 124 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java index 31e922e65..885d0c26f 100644 --- a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java @@ -1,7 +1,6 @@ package gg.agit.konect.domain.chat.model; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_CREATE_CHAT_ROOM_WITH_SELF; -import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; import static lombok.AccessLevel.PROTECTED; @@ -9,7 +8,6 @@ import java.time.LocalDateTime; import gg.agit.konect.domain.club.model.Club; -import gg.agit.konect.domain.user.enums.UserRole; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.global.exception.CustomException; import gg.agit.konect.global.model.BaseEntity; @@ -41,32 +39,19 @@ public class ChatRoom extends BaseEntity { @Column(name = "last_message_sent_at") private LocalDateTime lastMessageSentAt; - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "sender_id") - private User sender; - - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "receiver_id") - private User receiver; - @ManyToOne(fetch = LAZY) @JoinColumn(name = "club_id") private Club club; @Builder - private ChatRoom(Integer id, User sender, User receiver, Club club) { + private ChatRoom(Integer id, Club club) { this.id = id; - this.sender = sender; - this.receiver = receiver; this.club = club; } public static ChatRoom of(User sender, User receiver) { validateIsNotSameParticipant(sender, receiver); - return ChatRoom.builder() - .sender(sender) - .receiver(receiver) - .build(); + return ChatRoom.builder().build(); } public static ChatRoom groupOf(Club club) { @@ -81,23 +66,6 @@ public static void validateIsNotSameParticipant(User sender, User receiver) { } } - public void validateIsParticipant(Integer userId) { - if (!isParticipant(userId)) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - } - - public boolean isParticipant(Integer userId) { - if (sender == null || receiver == null) { - return false; - } - return sender.getId().equals(userId) || receiver.getId().equals(userId); - } - - public User getChatPartner(User currentUser) { - return sender.getId().equals(currentUser.getId()) ? receiver : sender; - } - public boolean isDirectRoom() { return club == null; } @@ -106,13 +74,6 @@ public boolean isGroupRoom() { return club != null; } - public User getNonAdminUser() { - if (sender.getRole() != UserRole.ADMIN) { - return sender; - } - return receiver; - } - public void updateLastMessage(String lastMessageContent, LocalDateTime lastMessageSentAt) { this.lastMessageContent = lastMessageContent; this.lastMessageSentAt = lastMessageSentAt; diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java index 05c05c9bb..f1ac16dec 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java @@ -62,6 +62,17 @@ List countUnreadMessagesForAdmin( @Param("adminRole") UserRole adminRole ); + @Query(""" + SELECT COUNT(cm) > 0 + FROM ChatMessage cm + WHERE cm.chatRoom.id = :chatRoomId + AND cm.sender.role != :adminRole + """) + boolean existsUserReplyByRoomId( + @Param("chatRoomId") Integer chatRoomId, + @Param("adminRole") UserRole adminRole + ); + @Query(""" SELECT m FROM ChatMessage m diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java index 689f80eea..f3f70dafd 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomMemberRepository.java @@ -45,6 +45,14 @@ boolean existsByChatRoomIdAndUserId( """) List findByChatRoomId(@Param("chatRoomId") Integer chatRoomId); + @Query(""" + SELECT crm + FROM ChatRoomMember crm + JOIN FETCH crm.user + WHERE crm.id.chatRoomId IN :chatRoomIds + """) + List findByChatRoomIds(@Param("chatRoomIds") List chatRoomIds); + @Query(""" SELECT crm FROM ChatRoomMember crm diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java index 6df94f69f..29c38ce3e 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java @@ -8,7 +8,6 @@ import org.springframework.data.repository.query.Param; import gg.agit.konect.domain.chat.model.ChatRoom; -import gg.agit.konect.domain.user.enums.UserRole; public interface ChatRoomRepository extends Repository { @@ -18,8 +17,6 @@ public interface ChatRoomRepository extends Repository { SELECT DISTINCT cr FROM ChatRoom cr JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id - LEFT JOIN FETCH cr.sender - LEFT JOIN FETCH cr.receiver LEFT JOIN FETCH cr.club WHERE crm.id.userId = :userId AND cr.club IS NULL @@ -30,8 +27,6 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT cr FROM ChatRoom cr - LEFT JOIN FETCH cr.sender - LEFT JOIN FETCH cr.receiver LEFT JOIN FETCH cr.club WHERE cr.id = :chatRoomId """) @@ -40,57 +35,16 @@ public interface ChatRoomRepository extends Repository { @Query(""" SELECT cr FROM ChatRoom cr + JOIN ChatRoomMember crm + ON crm.id.chatRoomId = cr.id WHERE cr.club IS NULL - AND ((cr.sender.id = :userId1 AND cr.receiver.id = :userId2) - OR (cr.sender.id = :userId2 AND cr.receiver.id = :userId1)) + GROUP BY cr + HAVING COUNT(crm) = 2 + AND SUM(CASE WHEN crm.id.userId = :userId1 THEN 1 ELSE 0 END) = 1 + AND SUM(CASE WHEN crm.id.userId = :userId2 THEN 1 ELSE 0 END) = 1 """) Optional findByTwoUsers(@Param("userId1") Integer userId1, @Param("userId2") Integer userId2); - @Query(""" - SELECT DISTINCT cr - FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE cr.club IS NULL - AND (s.role = :adminRole OR r.role = :adminRole) - ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id - """) - List findAllAdminChatRooms(@Param("adminRole") UserRole adminRole); - - @Query(""" - SELECT DISTINCT cr - FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE cr.club IS NULL - AND (s.role = :adminRole OR r.role = :adminRole) - AND EXISTS ( - SELECT 1 FROM ChatMessage cm - WHERE cm.chatRoom = cr - AND cm.sender.role != :adminRole - ) - ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id - """) - List findAdminChatRoomsWithUserReply(@Param("adminRole") UserRole adminRole); - - @Query(""" - SELECT cr - FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id - JOIN FETCH cr.sender s - JOIN FETCH cr.receiver r - WHERE cr.club IS NULL - AND ((s.id = :userId AND r.role = :adminRole) - OR (s.role = :adminRole AND r.id = :userId) - ) - """) - Optional findByUserIdAndAdminRole( - @Param("userId") Integer userId, - @Param("adminRole") UserRole adminRole - ); - @Query(""" SELECT cr FROM ChatRoom cr @@ -104,8 +58,6 @@ Optional findByUserIdAndAdminRole( FROM ChatRoom cr JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id LEFT JOIN FETCH cr.club - LEFT JOIN FETCH cr.sender - LEFT JOIN FETCH cr.receiver WHERE crm.id.userId = :userId AND cr.club IS NOT NULL ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id 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 53a12c4a0..6e021ae89 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 @@ -193,15 +193,23 @@ private List getDirectChatRooms(Integer userId) { List roomSummaries = new ArrayList<>(); List personalChatRooms = chatRoomRepository.findByUserId(userId); + Map> roomMembersMap = getRoomMembersMap(personalChatRooms); Map personalUnreadCountMap = getUnreadCountMap(extractChatRoomIds(personalChatRooms), userId); Set addedChatRoomIds = new HashSet<>(); if (user.getRole() == UserRole.ADMIN) { - List adminChatRooms = chatRoomRepository.findAdminChatRoomsWithUserReply(UserRole.ADMIN); - Map adminUnreadCountMap = getAdminUnreadCountMap(extractChatRoomIds(adminChatRooms)); + Map adminUnreadCountMap = getAdminUnreadCountMap(extractChatRoomIds(personalChatRooms)); + + for (ChatRoom chatRoom : personalChatRooms) { + List members = roomMembersMap.getOrDefault(chatRoom.getId(), List.of()); + User nonAdminUser = findNonAdminMember(members); + if (nonAdminUser == null) { + continue; + } + if (!chatMessageRepository.existsUserReplyByRoomId(chatRoom.getId(), UserRole.ADMIN)) { + continue; + } - for (ChatRoom chatRoom : adminChatRooms) { - User nonAdminUser = chatRoom.getNonAdminUser(); roomSummaries.add(new ChatRoomSummaryResponse( chatRoom.getId(), ChatType.DIRECT, @@ -218,7 +226,12 @@ private List getDirectChatRooms(Integer userId) { for (ChatRoom chatRoom : personalChatRooms) { if (!addedChatRoomIds.contains(chatRoom.getId())) { - User chatPartner = chatRoom.getChatPartner(user); + List members = roomMembersMap.getOrDefault(chatRoom.getId(), List.of()); + User chatPartner = findDirectPartner(members, user.getId()); + if (chatPartner == null) { + continue; + } + roomSummaries.add(new ChatRoomSummaryResponse( chatRoom.getId(), ChatType.DIRECT, @@ -298,7 +311,8 @@ private ChatMessageDetailResponse sendDirectMessage( User sender = userRepository.getById(userId); validateDirectAccess(chatRoom, userId); - User receiver = getMessageReceiver(sender, chatRoom); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + User receiver = resolveMessageReceiver(sender, members); ChatMessage chatMessage = chatMessageRepository.save( ChatMessage.of(chatRoom, sender, request.content()) @@ -538,29 +552,17 @@ private Map getAdminUnreadCountMap(List chatRoomIds) } private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { - if (user.getRole() == UserRole.ADMIN || !isAdminChatRoom(chatRoom)) { + if (user.getRole() == UserRole.ADMIN) { return null; } - return getAdminFromChatRoom(chatRoom).getId(); - } - private User getAdminFromChatRoom(ChatRoom chatRoom) { - if (chatRoom.getSender().getRole() == UserRole.ADMIN) { - return chatRoom.getSender(); + List members = chatRoomMemberRepository.findByChatRoomId(chatRoom.getId()); + User adminUser = findAdminMember(members); + if (adminUser == null) { + return null; } - return chatRoom.getReceiver(); - } - private boolean isAdminChatRoom(ChatRoom chatRoom) { - return chatRoom.getSender().getRole() == UserRole.ADMIN - || chatRoom.getReceiver().getRole() == UserRole.ADMIN; - } - - private User getMessageReceiver(User sender, ChatRoom chatRoom) { - if (sender.getRole() == UserRole.ADMIN && isAdminChatRoom(chatRoom)) { - return chatRoom.getNonAdminUser(); - } - return chatRoom.getChatPartner(sender); + return adminUser.getId(); } private void publishAdminChatEventIfNeeded(User receiver, User sender, String content) { @@ -665,4 +667,49 @@ private Integer resolveDirectSenderId(ChatMessage message, Integer maskedAdminId return message.getSender().getId(); } + private Map> getRoomMembersMap(List rooms) { + if (rooms.isEmpty()) { + return Map.of(); + } + + List roomIds = rooms.stream().map(ChatRoom::getId).toList(); + return chatRoomMemberRepository.findByChatRoomIds(roomIds).stream() + .collect(Collectors.groupingBy(ChatRoomMember::getChatRoomId)); + } + + private User findDirectPartner(List members, Integer userId) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> !memberUser.getId().equals(userId)) + .findFirst() + .orElse(null); + } + + private User findNonAdminMember(List members) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> memberUser.getRole() != UserRole.ADMIN) + .findFirst() + .orElse(null); + } + + private User findAdminMember(List members) { + return members.stream() + .map(ChatRoomMember::getUser) + .filter(memberUser -> memberUser.getRole() == UserRole.ADMIN) + .findFirst() + .orElse(null); + } + + private User resolveMessageReceiver(User sender, List members) { + if (sender.getRole() == UserRole.ADMIN) { + User nonAdminUser = findNonAdminMember(members); + if (nonAdminUser != null) { + return nonAdminUser; + } + } + + return findDirectPartner(members, sender.getId()); + } + } diff --git a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql index 4f73712ed..0bcc7bbb3 100644 --- a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql +++ b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql @@ -74,18 +74,58 @@ WHERE cr.receiver_id IS NOT NULL ON DUPLICATE KEY UPDATE last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); +SET @chat_room_sender_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_room' + AND kcu.COLUMN_NAME = 'sender_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_chat_room_sender_fk_sql = IF( + @chat_room_sender_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_room DROP FOREIGN KEY `', @chat_room_sender_fk_name, '`') +); + +PREPARE drop_chat_room_sender_fk_stmt FROM @drop_chat_room_sender_fk_sql; +EXECUTE drop_chat_room_sender_fk_stmt; +DEALLOCATE PREPARE drop_chat_room_sender_fk_stmt; + +SET @chat_room_receiver_fk_name = ( + SELECT kcu.CONSTRAINT_NAME + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = DATABASE() + AND kcu.TABLE_NAME = 'chat_room' + AND kcu.COLUMN_NAME = 'receiver_id' + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL + LIMIT 1 +); + +SET @drop_chat_room_receiver_fk_sql = IF( + @chat_room_receiver_fk_name IS NULL, + 'SELECT 1', + CONCAT('ALTER TABLE chat_room DROP FOREIGN KEY `', @chat_room_receiver_fk_name, '`') +); + +PREPARE drop_chat_room_receiver_fk_stmt FROM @drop_chat_room_receiver_fk_sql; +EXECUTE drop_chat_room_receiver_fk_stmt; +DEALLOCATE PREPARE drop_chat_room_receiver_fk_stmt; + +ALTER TABLE chat_room + DROP COLUMN sender_id, + DROP COLUMN receiver_id; + INSERT INTO chat_room ( - sender_id, - receiver_id, last_message_content, last_message_sent_at, club_id, created_at, updated_at ) -SELECT NULL, - NULL, - gm.content, +SELECT gm.content, gm.created_at, gcr.club_id, gcr.created_at, From fb5043b16e911e074b5c7fc724f70e94e2b442b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 00:15:29 +0900 Subject: [PATCH 17/26] =?UTF-8?q?fix:=20direct=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EB=B0=A9=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20chat=5Froom=5Fmemb?= =?UTF-8?q?er=20=EB=88=84=EB=9D=BD=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gg/agit/konect/domain/chat/model/ChatRoom.java | 3 +-- .../chat/service/ChatRoomMembershipService.java | 11 +++++++++++ .../agit/konect/domain/chat/service/ChatService.java | 3 ++- .../agit/konect/domain/user/service/UserService.java | 8 +++++++- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java index 885d0c26f..f437e57f4 100644 --- a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java @@ -49,8 +49,7 @@ private ChatRoom(Integer id, Club club) { this.club = club; } - public static ChatRoom of(User sender, User receiver) { - validateIsNotSameParticipant(sender, receiver); + public static ChatRoom directOf() { return ChatRoom.builder().build(); } 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 cbe413998..812f68505 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 @@ -27,6 +27,13 @@ public void addClubMember(ClubMember clubMember) { addClubMember(clubMember.getClub(), clubMember.getUser(), clubMember.getCreatedAt()); } + @Transactional + public void addDirectMembers(ChatRoom room, User firstUser, User secondUser, LocalDateTime joinedAt) { + LocalDateTime baseline = joinedAt != null ? joinedAt : LocalDateTime.now(); + ensureMember(room, firstUser, baseline); + ensureMember(room, secondUser, baseline); + } + @Transactional public void addClubMember(Club club, User user, LocalDateTime joinedAt) { ChatRoom room = chatRoomRepository.findByClubId(club.getId()) @@ -34,6 +41,10 @@ public void addClubMember(Club club, User user, LocalDateTime joinedAt) { LocalDateTime baseline = joinedAt != null ? joinedAt : LocalDateTime.now(); + ensureMember(room, user, baseline); + } + + private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { if (member.getLastReadAt().isBefore(baseline)) { 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 6e021ae89..273d45289 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 @@ -75,9 +75,10 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat if (currentUser.getId().equals(targetUser.getId())) { throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF); } + ChatRoom.validateIsNotSameParticipant(currentUser, targetUser); ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), targetUser.getId()) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.of(currentUser, targetUser))); + .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); LocalDateTime joinedAt = chatRoom.getCreatedAt() != null ? chatRoom.getCreatedAt() : LocalDateTime.now(); ensureRoomMember(chatRoom, currentUser, joinedAt); diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index d5223fc5b..2c89fcf7b 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -4,6 +4,7 @@ import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_DELETE_CLUB_PRESIDENT; import static gg.agit.konect.global.code.ApiResponseCode.CANNOT_DELETE_USER_WITH_UNPAID_FEE; +import java.time.LocalDateTime; import java.util.List; import org.springframework.context.ApplicationEventPublisher; @@ -117,8 +118,13 @@ private void sendWelcomeMessage(User newUser) { return; } + ChatRoom.validateIsNotSameParticipant(operator, newUser); + ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(operator.getId(), newUser.getId()) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.of(operator, newUser))); + .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); + + LocalDateTime joinedAt = chatRoom.getCreatedAt() != null ? chatRoom.getCreatedAt() : LocalDateTime.now(); + chatRoomMembershipService.addDirectMembers(chatRoom, operator, newUser, joinedAt); ChatMessage chatMessage = chatMessageRepository.save( ChatMessage.of(chatRoom, operator, DEFAULT_WELCOME_MESSAGE) From 521922459e4550ba8bc129e7b1354b4df16e44bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 00:17:40 +0900 Subject: [PATCH 18/26] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=A7=88?= =?UTF-8?q?=EC=A7=80=EB=A7=89=EC=9C=BC=EB=A1=9C=20=EC=9D=BD=EC=9D=80=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20NPE=20=EB=AC=B8=EC=A0=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/chat/service/ChatRoomMembershipService.java | 3 ++- .../java/gg/agit/konect/domain/chat/service/ChatService.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 812f68505..8e7a486c8 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 @@ -47,7 +47,8 @@ public void addClubMember(Club club, User user, LocalDateTime joinedAt) { private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { - if (member.getLastReadAt().isBefore(baseline)) { + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(baseline)) { member.updateLastReadAt(baseline); } }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, baseline))); 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 273d45289..92c72d29e 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 @@ -580,7 +580,8 @@ private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { - if (member.getLastReadAt().isBefore(joinedAt)) { + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(joinedAt)) { member.updateLastReadAt(joinedAt); } }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); From 23ba0d09a7bf8e16271a27c20a64ad8f023ee61e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 00:19:54 +0900 Subject: [PATCH 19/26] =?UTF-8?q?fix:=201=EB=8C=801=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=B0=B8=EA=B0=80=EC=9E=90=20=EC=97=86?= =?UTF-8?q?=EC=9C=BC=EB=A9=B4=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20NPE?= =?UTF-8?q?=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gg/agit/konect/domain/chat/service/ChatService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 92c72d29e..825c12a63 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 @@ -711,7 +711,11 @@ private User resolveMessageReceiver(User sender, List members) { } } - return findDirectPartner(members, sender.getId()); + User partner = findDirectPartner(members, sender.getId()); + if (partner == null) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + return partner; } } From c793206805b08830a3d5e97b2a9ac15e7db70643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:25:41 +0900 Subject: [PATCH 20/26] =?UTF-8?q?fix:=20clubMember.createdAt=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EC=B1=84=ED=8C=85=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=EC=8B=AD=20=EA=B8=B0=EC=A4=80=20=EC=8B=9C=EA=B0=81=20?= =?UTF-8?q?=EA=B0=95=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ChatRoomMembershipService.java | 19 ++++++------------- .../domain/chat/service/ChatService.java | 3 ++- .../domain/club/service/ClubService.java | 2 +- .../domain/user/service/UserService.java | 6 +++++- 4 files changed, 14 insertions(+), 16 deletions(-) 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 8e7a486c8..204b94f86 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 @@ -1,6 +1,7 @@ package gg.agit.konect.domain.chat.service; import java.time.LocalDateTime; +import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,7 +10,6 @@ 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.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; import gg.agit.konect.domain.user.model.User; import lombok.RequiredArgsConstructor; @@ -24,26 +24,19 @@ public class ChatRoomMembershipService { @Transactional public void addClubMember(ClubMember clubMember) { - addClubMember(clubMember.getClub(), clubMember.getUser(), clubMember.getCreatedAt()); + LocalDateTime baseline = Objects.requireNonNull(clubMember.getCreatedAt(), "clubMember.createdAt must not be null"); + ChatRoom room = chatRoomRepository.findByClubId(clubMember.getClub().getId()) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(clubMember.getClub()))); + ensureMember(room, clubMember.getUser(), baseline); } @Transactional public void addDirectMembers(ChatRoom room, User firstUser, User secondUser, LocalDateTime joinedAt) { - LocalDateTime baseline = joinedAt != null ? joinedAt : LocalDateTime.now(); + LocalDateTime baseline = Objects.requireNonNull(joinedAt, "joinedAt must not be null"); ensureMember(room, firstUser, baseline); ensureMember(room, secondUser, baseline); } - @Transactional - public void addClubMember(Club club, User user, LocalDateTime joinedAt) { - ChatRoom room = chatRoomRepository.findByClubId(club.getId()) - .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); - - LocalDateTime baseline = joinedAt != null ? joinedAt : LocalDateTime.now(); - - ensureMember(room, user, baseline); - } - private void ensureMember(ChatRoom room, User user, LocalDateTime baseline) { chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) .ifPresentOrElse(member -> { 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 825c12a63..70464a461 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 @@ -11,6 +11,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -80,7 +81,7 @@ public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreat ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(currentUser.getId(), targetUser.getId()) .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); - LocalDateTime joinedAt = chatRoom.getCreatedAt() != null ? chatRoom.getCreatedAt() : LocalDateTime.now(); + LocalDateTime joinedAt = Objects.requireNonNull(chatRoom.getCreatedAt(), "chatRoom.createdAt must not be null"); ensureRoomMember(chatRoom, currentUser, joinedAt); ensureRoomMember(chatRoom, targetUser, joinedAt); diff --git a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java index 3f932ccd4..07a14ccde 100644 --- a/src/main/java/gg/agit/konect/domain/club/service/ClubService.java +++ b/src/main/java/gg/agit/konect/domain/club/service/ClubService.java @@ -119,7 +119,7 @@ public ClubDetailResponse createClub(Integer userId, ClubCreateRequest request) .build(); ClubMember savedPresident = clubMemberRepository.save(president); - chatRoomMembershipService.addClubMember(savedClub, user, savedPresident.getCreatedAt()); + chatRoomMembershipService.addClubMember(savedPresident); return getClubDetail(savedClub.getId(), userId); } diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index 2c89fcf7b..a74e084b9 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -6,6 +6,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @@ -123,7 +124,10 @@ private void sendWelcomeMessage(User newUser) { ChatRoom chatRoom = chatRoomRepository.findByTwoUsers(operator.getId(), newUser.getId()) .orElseGet(() -> chatRoomRepository.save(ChatRoom.directOf())); - LocalDateTime joinedAt = chatRoom.getCreatedAt() != null ? chatRoom.getCreatedAt() : LocalDateTime.now(); + LocalDateTime joinedAt = Objects.requireNonNull( + chatRoom.getCreatedAt(), + "chatRoom.createdAt must not be null" + ); chatRoomMembershipService.addDirectMembers(chatRoom, operator, newUser, joinedAt); ChatMessage chatMessage = chatMessageRepository.save( From bec91aa07759fe3bab62e281226016d051276998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:27:36 +0900 Subject: [PATCH 21/26] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=20=EA=B8=B0=EC=A4=80=20=EC=8B=9C=EA=B0=81=EC=9D=84=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=9C=EA=B0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=ED=95=98=EA=B3=A0=20unread=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gg/agit/konect/domain/chat/service/ChatService.java | 3 --- 1 file changed, 3 deletions(-) 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 70464a461..dac1636a2 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 @@ -613,9 +613,6 @@ private int getUnreadCount(ChatMessage message, List members) { for (ChatRoomMember member : members) { LocalDateTime baseline = member.getLastReadAt(); - if (baseline == null || baseline.isBefore(member.getCreatedAt())) { - baseline = member.getCreatedAt(); - } if (baseline.isBefore(createdAt)) { unreadCount += 1; } From 29dc8981236906953ec35e576f096a073045fe29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:32:56 +0900 Subject: [PATCH 22/26] =?UTF-8?q?refactor:=20ChatService=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20unread=20=EC=A7=91=EA=B3=84=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/service/ChatService.java | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) 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 dac1636a2..01d10e29f 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 @@ -398,10 +398,11 @@ private ChatMessagePageResponse getClubMessagesByRoomId( Page messagePage = chatMessageRepository.findByChatRoomId(roomId, pageable); List messages = messagePage.getContent(); List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List sortedReadBaselines = toSortedReadBaselines(members); List responseMessages = messages.stream() .map(message -> { - int unreadCount = getUnreadCount(message, members); + int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); return new ChatMessageDetailResponse( message.getId(), message.getSender().getId(), @@ -440,6 +441,7 @@ private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Intege List members = chatRoomMemberRepository.findByChatRoomId(roomId); List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); + List sortedReadBaselines = toSortedReadBaselines(members); notificationService.sendGroupChatNotification( roomId, @@ -457,7 +459,7 @@ private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Intege message.getContent(), message.getCreatedAt(), null, - getUnreadCount(message, members), + countUnreadSince(message.getCreatedAt(), sortedReadBaselines), true ); } @@ -607,18 +609,30 @@ private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime last } } - private int getUnreadCount(ChatMessage message, List members) { - LocalDateTime createdAt = message.getCreatedAt(); - int unreadCount = 0; + private List toSortedReadBaselines(List members) { + List baselines = members.stream() + .map(ChatRoomMember::getLastReadAt) + .sorted() + .toList(); + return baselines; + } + + private int countUnreadSince(LocalDateTime messageCreatedAt, List sortedReadBaselines) { + int left = 0; + int right = sortedReadBaselines.size(); + + while (left < right) { + int mid = (left + right) >>> 1; + LocalDateTime baseline = sortedReadBaselines.get(mid); - for (ChatRoomMember member : members) { - LocalDateTime baseline = member.getLastReadAt(); - if (baseline.isBefore(createdAt)) { - unreadCount += 1; + if (baseline.isBefore(messageCreatedAt)) { + left = mid + 1; + } else { + right = mid; } } - return unreadCount; + return left; } private Map getLastMessageMap(List roomIds) { From d08b3357f35a2ff0cea227e88eec1eed9be17d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:35:23 +0900 Subject: [PATCH 23/26] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gg/agit/konect/domain/chat/service/ChatService.java | 9 --------- 1 file changed, 9 deletions(-) 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 01d10e29f..83bbe72bb 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 @@ -388,7 +388,6 @@ private ChatMessagePageResponse getClubMessagesByRoomId( ChatRoom room = getClubRoom(roomId); ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); ensureRoomMember(room, member.getUser(), member.getCreatedAt()); - syncClubMembers(room); chatPresenceService.recordPresence(roomId, userId); updateLastReadAt(roomId, userId, LocalDateTime.now()); @@ -433,7 +432,6 @@ private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Intege User sender = member.getUser(); ensureRoomMember(room, sender, member.getCreatedAt()); - syncClubMembers(room); ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); room.updateLastMessage(message.getContent(), message.getCreatedAt()); @@ -508,13 +506,6 @@ private ChatRoom resolveOrCreateClubRoom(Club club) { .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(club))); } - private void syncClubMembers(ChatRoom room) { - List clubMembers = clubMemberRepository.findAllByClubId(room.getClub().getId()); - for (ClubMember clubMember : clubMembers) { - ensureRoomMember(room, clubMember.getUser(), clubMember.getCreatedAt()); - } - } - private List extractChatRoomIds(List chatRooms) { return chatRooms.stream() .map(ChatRoom::getId) From b563ef53c49b436a84a112eb2ed197ea0e4ce535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:54:47 +0900 Subject: [PATCH 24/26] =?UTF-8?q?fix:=20V26=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=EC=97=90=EC=84=9C=20deprecated?= =?UTF-8?q?=EB=90=9C=20VALUES()=20=EA=B5=AC=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../V26__add_unified_chat_room_table.sql | 77 ++++++++++++------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql index 0bcc7bbb3..7abeead78 100644 --- a/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql +++ b/src/main/resources/db/migration/V26__add_unified_chat_room_table.sql @@ -53,26 +53,40 @@ CREATE INDEX idx_chat_room_member_room_last_read ON chat_room_member (chat_room_id, last_read_at); INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) -SELECT cr.id, - cr.sender_id, - COALESCE(cr.last_message_sent_at, cr.created_at), - cr.created_at, - NOW() -FROM chat_room cr -WHERE cr.sender_id IS NOT NULL +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cr.sender_id AS user_id, + COALESCE(cr.last_message_sent_at, cr.created_at) AS last_read_at, + cr.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + WHERE cr.sender_id IS NOT NULL + ) src ON DUPLICATE KEY UPDATE - last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) -SELECT cr.id, - cr.receiver_id, - COALESCE(cr.last_message_sent_at, cr.created_at), - cr.created_at, - NOW() -FROM chat_room cr -WHERE cr.receiver_id IS NOT NULL +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cr.receiver_id AS user_id, + COALESCE(cr.last_message_sent_at, cr.created_at) AS last_read_at, + cr.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + WHERE cr.receiver_id IS NOT NULL + ) src ON DUPLICATE KEY UPDATE - last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); SET @chat_room_sender_fk_name = ( SELECT kcu.CONSTRAINT_NAME @@ -146,19 +160,26 @@ FROM group_chat_room gcr WHERE cr.id IS NULL; INSERT INTO chat_room_member (chat_room_id, user_id, last_read_at, created_at, updated_at) -SELECT cr.id, - cm.user_id, - COALESCE(grs.last_read_at, cm.created_at), - cm.created_at, - NOW() -FROM chat_room cr - JOIN club_member cm ON cm.club_id = cr.club_id - LEFT JOIN group_chat_room gcr ON gcr.club_id = cr.club_id - LEFT JOIN group_chat_read_status grs ON grs.room_id = gcr.id - AND grs.user_id = cm.user_id -WHERE cr.club_id IS NOT NULL +SELECT src.chat_room_id, + src.user_id, + src.last_read_at, + src.created_at, + src.updated_at +FROM ( + SELECT cr.id AS chat_room_id, + cm.user_id AS user_id, + COALESCE(grs.last_read_at, cm.created_at) AS last_read_at, + cm.created_at AS created_at, + NOW() AS updated_at + FROM chat_room cr + JOIN club_member cm ON cm.club_id = cr.club_id + LEFT JOIN group_chat_room gcr ON gcr.club_id = cr.club_id + LEFT JOIN group_chat_read_status grs ON grs.room_id = gcr.id + AND grs.user_id = cm.user_id + WHERE cr.club_id IS NOT NULL + ) src ON DUPLICATE KEY UPDATE - last_read_at = GREATEST(chat_room_member.last_read_at, VALUES(last_read_at)); + last_read_at = GREATEST(chat_room_member.last_read_at, src.last_read_at); INSERT INTO chat_message ( chat_room_id, From e3630d9cdbb39e2930a606ad280058324e0b6c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:56:26 +0900 Subject: [PATCH 25/26] =?UTF-8?q?fix:=20lastReadAt=20null=20=EA=B0=92?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20NPE=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=EC=84=B1=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gg/agit/konect/domain/chat/model/ChatRoomMember.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java index 35b2ab606..de7706f24 100644 --- a/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java +++ b/src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java @@ -71,7 +71,11 @@ public Integer getUserId() { } public void updateLastReadAt(LocalDateTime lastReadAt) { - if (this.lastReadAt.isBefore(lastReadAt)) { + if (lastReadAt == null) { + return; + } + + if (this.lastReadAt == null || this.lastReadAt.isBefore(lastReadAt)) { this.lastReadAt = lastReadAt; } } From 772642e6d5ed28804283de42cd1d2fff2c47d21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 18 Feb 2026 11:59:36 +0900 Subject: [PATCH 26/26] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/chat/service/ChatRoomMembershipService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 204b94f86..5e3d6bc30 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 @@ -24,7 +24,7 @@ public class ChatRoomMembershipService { @Transactional public void addClubMember(ClubMember clubMember) { - LocalDateTime baseline = Objects.requireNonNull(clubMember.getCreatedAt(), "clubMember.createdAt must not be null"); + LocalDateTime baseline = Objects.requireNonNull(clubMember.getCreatedAt()); ChatRoom room = chatRoomRepository.findByClubId(clubMember.getClub().getId()) .orElseGet(() -> chatRoomRepository.save(ChatRoom.groupOf(clubMember.getClub()))); ensureMember(room, clubMember.getUser(), baseline); @@ -32,7 +32,7 @@ public void addClubMember(ClubMember clubMember) { @Transactional public void addDirectMembers(ChatRoom room, User firstUser, User secondUser, LocalDateTime joinedAt) { - LocalDateTime baseline = Objects.requireNonNull(joinedAt, "joinedAt must not be null"); + LocalDateTime baseline = Objects.requireNonNull(joinedAt); ensureMember(room, firstUser, baseline); ensureMember(room, secondUser, baseline); }