From 6588e2aa0b87c92c6f79eccaad8bd1fced8cd55e Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 15:59:54 +0900 Subject: [PATCH 01/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20=EA=B4=80=EB=A0=A8=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/domain/ChatMessage.java | 9 +++++++++ .../solidconnection/chat/domain/ChatParticipant.java | 8 ++++++++ .../solidconnection/chat/domain/ChatReadStatus.java | 5 +++++ .../example/solidconnection/chat/domain/ChatRoom.java | 4 ++++ 4 files changed, 26 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java index 8d513c5a7..02e2de525 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java @@ -35,4 +35,13 @@ public class ChatMessage extends BaseEntity { @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL) private List chatAttachments = new ArrayList<>(); + + public ChatMessage(String content, long senderId, ChatRoom chatRoom) { + this.content = content; + this.senderId = senderId; + this.chatRoom = chatRoom; + if (chatRoom != null) { + chatRoom.getChatMessages().add(this); + } + } } diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java b/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java index 169e1dd06..60fc6b795 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatParticipant.java @@ -34,4 +34,12 @@ public class ChatParticipant extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private ChatRoom chatRoom; + + public ChatParticipant(long siteUserId, ChatRoom chatRoom) { + this.siteUserId = siteUserId; + this.chatRoom = chatRoom; + if (chatRoom != null) { + chatRoom.getChatParticipants().add(this); + } + } } diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java b/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java index 13d4ac646..8c731738c 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatReadStatus.java @@ -32,4 +32,9 @@ public class ChatReadStatus extends BaseEntity { @Column(name = "chat_participant_id") private long chatParticipantId; + + public ChatReadStatus(long chatRoomId, long chatParticipantId) { + this.chatRoomId = chatRoomId; + this.chatParticipantId = chatParticipantId; + } } diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java index 020befe5f..28db6c3be 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java @@ -29,4 +29,8 @@ public class ChatRoom extends BaseEntity { @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) private List chatMessages = new ArrayList<>(); + + public ChatRoom(boolean isGroup) { + this.isGroup = isGroup; + } } From 513561311d29bad918e6f773e20f5f3d15beb82f Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:00:50 +0900 Subject: [PATCH 02/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/ChatParticipantResponse.java | 12 ++++++++++ .../chat/dto/ChatRoomListResponse.java | 12 ++++++++++ .../chat/dto/ChatRoomResponse.java | 22 +++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java new file mode 100644 index 000000000..ffa6b9b8c --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatParticipantResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.chat.dto; + +public record ChatParticipantResponse( + long partnerId, + String nickname, + String profileUrl +) { + + public static ChatParticipantResponse of(long partnerId, String nickname, String profileUrl) { + return new ChatParticipantResponse(partnerId, nickname, profileUrl); + } +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java new file mode 100644 index 000000000..add17f1d1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomListResponse.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.chat.dto; + +import java.util.List; + +public record ChatRoomListResponse( + List chatRooms +) { + + public static ChatRoomListResponse of(List chatRooms) { + return new ChatRoomListResponse(chatRooms); + } +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java new file mode 100644 index 000000000..69ec047fb --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatRoomResponse.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.chat.dto; + +import java.time.ZonedDateTime; + +public record ChatRoomResponse( + long id, + String lastChatMessage, + ZonedDateTime lastReceivedTime, + ChatParticipantResponse partner, + long unReadCount +) { + + public static ChatRoomResponse of( + long id, + String lastChatMessage, + ZonedDateTime lastReceivedTime, + ChatParticipantResponse partner, + long unReadCount + ) { + return new ChatRoomResponse(id, lastChatMessage, lastReceivedTime, partner, unReadCount); + } +} From e1f20d622e5023b5ac6cfc369aee1dfd570e5a7f Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:01:36 +0900 Subject: [PATCH 03/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ChatMessageRepository.java | 8 ++++ .../repository/ChatParticipantRepository.java | 8 ++++ .../repository/ChatReadStatusRepository.java | 8 ++++ .../chat/repository/ChatRoomRepository.java | 46 +++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java create mode 100644 src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java create mode 100644 src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java create mode 100644 src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java new file mode 100644 index 000000000..27e23690f --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatMessage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatMessageRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java new file mode 100644 index 000000000..d6486c891 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatParticipant; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatParticipantRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java new file mode 100644 index 000000000..54dfff5b6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatReadStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatReadStatusRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java new file mode 100644 index 000000000..88c92045a --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java @@ -0,0 +1,46 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.ChatRoom; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ChatRoomRepository extends JpaRepository { + + @Query(""" + SELECT cr FROM ChatRoom cr + JOIN cr.chatParticipants cp + WHERE cp.siteUserId = :userId AND cr.isGroup = false + ORDER BY ( + SELECT MAX(cm.createdAt) + FROM ChatMessage cm + WHERE cm.chatRoom = cr + ) DESC NULLS LAST + """) + List findOneOnOneChatRoomsByUserId(@Param("userId") long userId); + + @Query(""" + SELECT cm FROM ChatMessage cm + WHERE cm.chatRoom.id = :chatRoomId + ORDER BY cm.createdAt DESC + LIMIT 1 + """) + Optional findLatestMessageByChatRoomId(@Param("chatRoomId") long chatRoomId); + + @Query(""" + SELECT COUNT(cm) FROM ChatMessage cm + LEFT JOIN ChatReadStatus crs ON crs.chatRoomId = cm.chatRoom.id + AND crs.chatParticipantId = ( + SELECT cp.id FROM ChatParticipant cp + WHERE cp.chatRoom.id = :chatRoomId + AND cp.siteUserId = :userId + ) + WHERE cm.chatRoom.id = :chatRoomId + AND cm.senderId != :userId + AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt) + """) + long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId); +} From 449049f594fa9518c17d6fad329b0086fd932c2e Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:02:49 +0900 Subject: [PATCH 04/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatService.java | 74 +++++++++++++++++++ .../common/exception/ErrorCode.java | 4 + 2 files changed, 78 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/service/ChatService.java diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java new file mode 100644 index 000000000..2bba2a0b7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -0,0 +1,74 @@ +package com.example.solidconnection.chat.service; + +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.dto.ChatParticipantResponse; +import com.example.solidconnection.chat.dto.ChatRoomListResponse; +import com.example.solidconnection.chat.dto.ChatRoomResponse; +import com.example.solidconnection.chat.repository.ChatRoomRepository; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class ChatService { + + public static final int CHAT_PARTNER_LIMIT = 1; + + private final ChatRoomRepository chatRoomRepository; + private final SiteUserRepository siteUserRepository; + + @Transactional(readOnly = true) + public ChatRoomListResponse getChatRooms(long siteUserId) { + List chatRooms = chatRoomRepository.findOneOnOneChatRoomsByUserId(siteUserId); + List chatRoomInfos = chatRooms.stream() + .map(chatRoom -> toChatRoomResponse(chatRoom, siteUserId)) + .toList(); + return ChatRoomListResponse.of(chatRoomInfos); + } + + private ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, long siteUserId) { + Optional latestMessage = chatRoomRepository.findLatestMessageByChatRoomId(chatRoom.getId()); + String lastChatMessage = latestMessage.map(ChatMessage::getContent).orElse(""); + ZonedDateTime lastReceivedTime = latestMessage.map(ChatMessage::getCreatedAt).orElse(null); + + ChatParticipant partnerParticipant = findPartner(chatRoom, siteUserId); + + SiteUser siteUser = siteUserRepository.findById(partnerParticipant.getSiteUserId()) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + ChatParticipantResponse partner = ChatParticipantResponse.of(siteUser.getId(), siteUser.getNickname(), siteUser.getProfileImageUrl()); + + long unReadCount = chatRoomRepository.countUnreadMessages(chatRoom.getId(), siteUserId); + + return ChatRoomResponse.of(chatRoom.getId(), lastChatMessage, lastReceivedTime, partner, unReadCount); + } + + private ChatParticipant findPartner(ChatRoom chatRoom, long siteUserId) { + List partners = chatRoom.getChatParticipants().stream() + .filter(participant -> participant.getSiteUserId() != siteUserId) + .toList(); + validateOneOnOneChat(partners); + return partners.get(0); + } + + private void validateOneOnOneChat(List partners) { + if (partners.isEmpty()) { + throw new CustomException(CHAT_PARTNER_NOT_FOUND); + } + if (partners.size() > CHAT_PARTNER_LIMIT) { + throw new CustomException(INVALID_CHAT_ROOM_STATE); + } + } +} diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 824c531f6..5edfacbdc 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -115,6 +115,10 @@ public enum ErrorCode { // report ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."), + // chat + CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."), + INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."), + // database DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), From 14e4c6c84d5eeff62d89a27616031b5e5a1b8460 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:03:57 +0900 Subject: [PATCH 05/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/controller/ChatController.java diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java new file mode 100644 index 000000000..aaa78d5f1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.chat.controller; + +import com.example.solidconnection.chat.dto.ChatRoomListResponse; +import com.example.solidconnection.chat.service.ChatService; +import com.example.solidconnection.common.resolver.AuthorizedUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/chats") +public class ChatController { + + private final ChatService chatService; + + @GetMapping("/rooms") + public ResponseEntity getChatRooms( + @AuthorizedUser long siteUserId + ) { + ChatRoomListResponse chatRoomListResponse = chatService.getChatRooms(siteUserId); + return ResponseEntity.ok(chatRoomListResponse); + } +} From 6948995dd41b9f01e2646788d20d0944adba227b Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:04:43 +0900 Subject: [PATCH 06/32] =?UTF-8?q?test:=20=EC=B1=84=ED=8C=85=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20fixture=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/fixture/ChatMessageFixture.java | 21 ++++++++++ .../fixture/ChatMessageFixtureBuilder.java | 42 +++++++++++++++++++ .../chat/fixture/ChatParticipantFixture.java | 20 +++++++++ .../ChatParticipantFixtureBuilder.java | 36 ++++++++++++++++ .../chat/fixture/ChatReadStatusFixture.java | 19 +++++++++ .../fixture/ChatReadStatusFixtureBuilder.java | 35 ++++++++++++++++ .../chat/fixture/ChatRoomFixture.java | 18 ++++++++ .../chat/fixture/ChatRoomFixtureBuilder.java | 29 +++++++++++++ 8 files changed, 220 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixture.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixtureBuilder.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixture.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixtureBuilder.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixture.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixtureBuilder.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixture.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixtureBuilder.java diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixture.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixture.java new file mode 100644 index 000000000..f5a30cec8 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixture.java @@ -0,0 +1,21 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.ChatRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatMessageFixture { + + private final ChatMessageFixtureBuilder chatMessageFixtureBuilder; + + public ChatMessage 메시지(String content, long senderId, ChatRoom chatRoom) { + return chatMessageFixtureBuilder.chatMessage() + .content(content) + .senderId(senderId) + .chatRoom(chatRoom) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixtureBuilder.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixtureBuilder.java new file mode 100644 index 000000000..8b30718cb --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatMessageFixtureBuilder.java @@ -0,0 +1,42 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatMessageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatMessageFixtureBuilder { + + private final ChatMessageRepository chatMessageRepository; + + private String content; + private long senderId; + private ChatRoom chatRoom; + + public ChatMessageFixtureBuilder chatMessage() { + return new ChatMessageFixtureBuilder(chatMessageRepository); + } + + public ChatMessageFixtureBuilder content(String content) { + this.content = content; + return this; + } + + public ChatMessageFixtureBuilder senderId(long senderId) { + this.senderId = senderId; + return this; + } + + public ChatMessageFixtureBuilder chatRoom(ChatRoom chatRoom) { + this.chatRoom = chatRoom; + return this; + } + + public ChatMessage create() { + ChatMessage chatMessage = new ChatMessage(content, senderId, chatRoom); + return chatMessageRepository.save(chatMessage); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixture.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixture.java new file mode 100644 index 000000000..20825919d --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixture.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatParticipantFixture { + + private final ChatParticipantFixtureBuilder chatParticipantFixtureBuilder; + + public ChatParticipant 참여자(long siteUserId, ChatRoom chatRoom) { + return chatParticipantFixtureBuilder.chatParticipant() + .siteUserId(siteUserId) + .chatRoom(chatRoom) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixtureBuilder.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixtureBuilder.java new file mode 100644 index 000000000..8514ce77e --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatParticipantFixtureBuilder.java @@ -0,0 +1,36 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatParticipantFixtureBuilder { + + private final ChatParticipantRepository chatParticipantRepository; + + private long siteUserId; + private ChatRoom chatRoom; + + public ChatParticipantFixtureBuilder chatParticipant() { + return new ChatParticipantFixtureBuilder(chatParticipantRepository); + } + + public ChatParticipantFixtureBuilder siteUserId(long siteUserId) { + this.siteUserId = siteUserId; + return this; + } + + public ChatParticipantFixtureBuilder chatRoom(ChatRoom chatRoom) { + this.chatRoom = chatRoom; + return this; + } + + public ChatParticipant create() { + ChatParticipant chatParticipant = new ChatParticipant(siteUserId, chatRoom); + return chatParticipantRepository.save(chatParticipant); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixture.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixture.java new file mode 100644 index 000000000..f254faaf3 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixture.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatReadStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatReadStatusFixture { + + private final ChatReadStatusFixtureBuilder chatReadStatusFixtureBuilder; + + public ChatReadStatus 읽음상태(long chatRoomId, long chatParticipantId) { + return chatReadStatusFixtureBuilder.chatReadStatus() + .chatRoomId(chatRoomId) + .chatParticipantId(chatParticipantId) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixtureBuilder.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixtureBuilder.java new file mode 100644 index 000000000..6f42c7d13 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatReadStatusFixtureBuilder.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatReadStatus; +import com.example.solidconnection.chat.repository.ChatReadStatusRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatReadStatusFixtureBuilder { + + private final ChatReadStatusRepository chatReadStatusRepository; + + private long chatRoomId; + private long chatParticipantId; + + public ChatReadStatusFixtureBuilder chatReadStatus() { + return new ChatReadStatusFixtureBuilder(chatReadStatusRepository); + } + + public ChatReadStatusFixtureBuilder chatRoomId(long chatRoomId) { + this.chatRoomId = chatRoomId; + return this; + } + + public ChatReadStatusFixtureBuilder chatParticipantId(long chatParticipantId) { + this.chatParticipantId = chatParticipantId; + return this; + } + + public ChatReadStatus create() { + ChatReadStatus chatReadStatus = new ChatReadStatus(chatRoomId, chatParticipantId); + return chatReadStatusRepository.save(chatReadStatus); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixture.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixture.java new file mode 100644 index 000000000..cf80313bc --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixture.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatRoom; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixture { + + private final ChatRoomFixtureBuilder chatRoomFixtureBuilder; + + public ChatRoom 채팅방(boolean isGroup) { + return chatRoomFixtureBuilder.chatRoom() + .isGroup(isGroup) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixtureBuilder.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixtureBuilder.java new file mode 100644 index 000000000..bf7ed3387 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatRoomFixtureBuilder.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatRoomRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatRoomFixtureBuilder { + + private final ChatRoomRepository chatRoomRepository; + + private boolean isGroup; + + public ChatRoomFixtureBuilder chatRoom() { + return new ChatRoomFixtureBuilder(chatRoomRepository); + } + + public ChatRoomFixtureBuilder isGroup(boolean isGroup) { + this.isGroup = isGroup; + return this; + } + + public ChatRoom create() { + ChatRoom chatRoom = new ChatRoom(isGroup); + return chatRoomRepository.save(chatRoom); + } +} From 541747a08b460f05c97fac9a826d00737df9480f Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:05:15 +0900 Subject: [PATCH 07/32] =?UTF-8?q?test:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatServiceTest.java | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java new file mode 100644 index 000000000..c9ce6a084 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -0,0 +1,189 @@ +package com.example.solidconnection.chat.service; + +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.dto.ChatRoomListResponse; +import com.example.solidconnection.chat.fixture.ChatMessageFixture; +import com.example.solidconnection.chat.fixture.ChatParticipantFixture; +import com.example.solidconnection.chat.fixture.ChatReadStatusFixture; +import com.example.solidconnection.chat.fixture.ChatRoomFixture; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@TestContainerSpringBootTest +@DisplayName("채팅 서비스 테스트") +class ChatServiceTest { + + @Autowired + private ChatService chatService; + + @Autowired + private SiteUserFixture siteUserFixture; + + @Autowired + private ChatRoomFixture chatRoomFixture; + + @Autowired + private ChatParticipantFixture chatParticipantFixture; + + @Autowired + private ChatMessageFixture chatMessageFixture; + + @Autowired + private ChatReadStatusFixture chatReadStatusFixture; + + private SiteUser user; + private SiteUser mentor1; + private SiteUser mentor2; + + @BeforeEach + void setUp() { + user = siteUserFixture.사용자(); + mentor1 = siteUserFixture.사용자(1, "mentor1"); + mentor2 = siteUserFixture.사용자(2, "mentor2"); + } + + @Nested + class 채팅방_목록을_조회한다 { + + @Test + void 채팅방이_없으면_빈_목록을_반환한다() { + // when + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // then + assertThat(response.chatRooms()).isEmpty(); + } + + @Test + void 최신_메시지_순으로_정렬되어_조회한다() { + // given + ChatRoom chatRoom1 = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom1); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom1); + ChatMessage oldMessage = chatMessageFixture.메시지("오래된 메시지", mentor1.getId(), chatRoom1); + + ChatRoom chatRoom2 = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom2); + chatParticipantFixture.참여자(mentor2.getId(), chatRoom2); + ChatMessage newMessage = chatMessageFixture.메시지("최신 메시지", mentor2.getId(), chatRoom2); + + // when + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // then + assertAll( + () -> assertThat(response.chatRooms()).hasSize(2), + () -> assertThat(response.chatRooms().get(0).partner().partnerId()).isEqualTo(mentor2.getId()), + () -> assertThat(response.chatRooms().get(0).lastChatMessage()).isEqualTo(newMessage.getContent()), + () -> assertThat(response.chatRooms().get(1).partner().partnerId()).isEqualTo(mentor1.getId()), + () -> assertThat(response.chatRooms().get(1).lastChatMessage()).isEqualTo(oldMessage.getContent() + )); + } + + @Test + void 그룹_채팅방은_제외하고_1대1_채팅방만_조회한다() { + // given + ChatRoom oneOnOneRoom = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), oneOnOneRoom); + chatParticipantFixture.참여자(mentor1.getId(), oneOnOneRoom); + + ChatRoom groupRoom = chatRoomFixture.채팅방(true); + chatParticipantFixture.참여자(user.getId(), groupRoom); + chatParticipantFixture.참여자(mentor1.getId(), groupRoom); + chatParticipantFixture.참여자(mentor2.getId(), groupRoom); + + // when + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // then + assertAll( + () -> assertThat(response.chatRooms()).hasSize(1), + () -> assertThat(response.chatRooms().get(0).id()).isEqualTo(oneOnOneRoom.getId()) + ); + } + + @Test + void 채팅_상대방이_없으면_예외가_발생한다() { + // given + ChatRoom chatRoom = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom); + + // when & then + assertThatCode(() -> chatService.getChatRooms(user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_PARTNER_NOT_FOUND.getMessage()); + } + + @Test + void 일대일_채팅방에_참여자가_3명_이상이면_예외가_발생한다() { + // given + ChatRoom chatRoom = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom); + chatParticipantFixture.참여자(mentor2.getId(), chatRoom); + + // when & then + assertThatCode(() -> chatService.getChatRooms(user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_CHAT_ROOM_STATE.getMessage()); + } + } + + @Nested + class 읽지_않은_메시지_수를_조회한다 { + + private ChatRoom chatRoom; + private ChatParticipant participant; + + @BeforeEach + void setUp() { + chatRoom = chatRoomFixture.채팅방(false); + participant = chatParticipantFixture.참여자(user.getId(), chatRoom); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom); + } + + @Test + void 읽음_상태가_없으면_모든_상대방_메시지를_카운팅한다() { + // given + chatMessageFixture.메시지("메시지1", mentor1.getId(), chatRoom); + chatMessageFixture.메시지("메시지2", mentor1.getId(), chatRoom); + + // when + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // then + assertThat(response.chatRooms().get(0).unReadCount()).isEqualTo(2); + } + + @Test + void 읽음_상태_이후_메시지만_읽지_않은_메시지로_카운팅한다() { + // given + chatMessageFixture.메시지("읽은 메시지", mentor1.getId(), chatRoom); + chatReadStatusFixture.읽음상태(chatRoom.getId(), participant.getId()); + + chatMessageFixture.메시지("읽지 않은 메시지1", mentor1.getId(), chatRoom); + chatMessageFixture.메시지("읽지 않은 메시지2", mentor1.getId(), chatRoom); + + // when + ChatRoomListResponse response = chatService.getChatRooms(user.getId()); + + // then + assertThat(response.chatRooms().get(0).unReadCount()).isEqualTo(2); + } + } +} From 67507b1f5e675f6ec1ddcb0bc2d2504685de28cf Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:53:09 +0900 Subject: [PATCH 08/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatAttachmentRepository.java | 8 ++++++++ .../chat/repository/ChatMessageRepository.java | 11 +++++++++++ .../chat/repository/ChatParticipantRepository.java | 1 + 3 files changed, 20 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java new file mode 100644 index 000000000..0d2dd3051 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatAttachmentRepository.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatAttachment; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatAttachmentRepository extends JpaRepository { + +} diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java index 27e23690f..8846b62e0 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java @@ -1,8 +1,19 @@ package com.example.solidconnection.chat.repository; import com.example.solidconnection.chat.domain.ChatMessage; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ChatMessageRepository extends JpaRepository { + @Query(""" + SELECT cm FROM ChatMessage cm + LEFT JOIN FETCH cm.chatAttachments + WHERE cm.chatRoom.id = :roomId + ORDER BY cm.createdAt DESC + """) + Slice findByRoomIdWithPaging(@Param("roomId") long roomId, Pageable pageable); } diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java index d6486c891..0f4de58d7 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java @@ -5,4 +5,5 @@ public interface ChatParticipantRepository extends JpaRepository { + boolean existsByChatRoomIdAndSiteUserId(Long chatRoomId, Long siteUserId); } From cdadb15b74befb101fe60048ced4b07edb040acd Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:53:27 +0900 Subject: [PATCH 09/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EA=B4=80=EB=A0=A8=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/ChatAttachmentResponse.java | 17 +++++++++++++++++ .../chat/dto/ChatMessageResponse.java | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java new file mode 100644 index 000000000..44c11246b --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatAttachmentResponse.java @@ -0,0 +1,17 @@ +package com.example.solidconnection.chat.dto; + +import java.time.ZonedDateTime; + +public record ChatAttachmentResponse( + long id, + boolean isImage, + String url, + String thumbnailUrl, + ZonedDateTime createdAt +) { + + public static ChatAttachmentResponse of(long id, boolean isImage, String url, + String thumbnailUrl, ZonedDateTime createdAt) { + return new ChatAttachmentResponse(id, isImage, url, thumbnailUrl, createdAt); + } +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java new file mode 100644 index 000000000..a3728b7fd --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageResponse.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.chat.dto; + +import java.time.ZonedDateTime; +import java.util.List; + +public record ChatMessageResponse( + long id, + String content, + long senderId, + ZonedDateTime createdAt, + List attachments +) { + + public static ChatMessageResponse of(long id, String content, long senderId, + ZonedDateTime createdAt, List attachments) { + return new ChatMessageResponse(id, content, senderId, createdAt, attachments); + } +} From be5240a443798a8443813ae420ce843348f307d4 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:53:51 +0900 Subject: [PATCH 10/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatService.java | 50 +++++++++++++++++++ .../common/exception/ErrorCode.java | 1 + 2 files changed, 51 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 2bba2a0b7..3fd2b11ac 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -1,16 +1,22 @@ package com.example.solidconnection.chat.service; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; import com.example.solidconnection.chat.domain.ChatMessage; import com.example.solidconnection.chat.domain.ChatParticipant; import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.dto.ChatAttachmentResponse; +import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatParticipantResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.dto.ChatRoomResponse; +import com.example.solidconnection.chat.repository.ChatMessageRepository; +import com.example.solidconnection.chat.repository.ChatParticipantRepository; import com.example.solidconnection.chat.repository.ChatRoomRepository; +import com.example.solidconnection.common.dto.SliceResponse; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -18,6 +24,8 @@ import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,6 +36,8 @@ public class ChatService { public static final int CHAT_PARTNER_LIMIT = 1; private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final ChatParticipantRepository chatParticipantRepository; private final SiteUserRepository siteUserRepository; @Transactional(readOnly = true) @@ -71,4 +81,44 @@ private void validateOneOnOneChat(List partners) { throw new CustomException(INVALID_CHAT_ROOM_STATE); } } + + @Transactional(readOnly = true) + public SliceResponse getChatMessages(long siteUserId, long roomId, Pageable pageable) { + validateChatRoomParticipant(siteUserId, roomId); + + Slice chatMessages = chatMessageRepository.findByRoomIdWithPaging(roomId, pageable); + + List content = chatMessages.getContent().stream() + .map(this::toChatMessageResponse) + .toList(); + + return SliceResponse.of(content, chatMessages); + } + + private void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_ROOM_ACCESS_DENIED); + } + } + + private ChatMessageResponse toChatMessageResponse(ChatMessage message) { + List attachments = message.getChatAttachments().stream() + .map(attachment -> ChatAttachmentResponse.of( + attachment.getId(), + attachment.getIsImage(), + attachment.getUrl(), + attachment.getThumbnailUrl(), + attachment.getCreatedAt() + )) + .toList(); + + return ChatMessageResponse.of( + message.getId(), + message.getContent(), + message.getSenderId(), + message.getCreatedAt(), + attachments + ); + } } diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 5edfacbdc..70f410665 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -118,6 +118,7 @@ public enum ErrorCode { // chat CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."), INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."), + CHAT_ROOM_ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "채팅방 접근이 거부되었습니다."), // database DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), From a437240440969072c989b06242a5e4565ebd77a4 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:54:43 +0900 Subject: [PATCH 11/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatController.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java index aaa78d5f1..e8c905795 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java @@ -1,11 +1,17 @@ package com.example.solidconnection.chat.controller; +import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.service.ChatService; +import com.example.solidconnection.common.dto.SliceResponse; import com.example.solidconnection.common.resolver.AuthorizedUser; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,4 +29,13 @@ public ResponseEntity getChatRooms( ChatRoomListResponse chatRoomListResponse = chatService.getChatRooms(siteUserId); return ResponseEntity.ok(chatRoomListResponse); } + + @GetMapping("/rooms/{room-id}") + public ResponseEntity> getChatMessages( + @AuthorizedUser long siteUserId, + @PathVariable("room-id") Long roomId, + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + SliceResponse response = chatService.getChatMessages(siteUserId, roomId, pageable); + return ResponseEntity.ok(response); + } } From 5ea6eaf4f73cb175c28ea9f0209fe26e44a88aac Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:55:00 +0900 Subject: [PATCH 12/32] =?UTF-8?q?test:=20=EC=B1=84=ED=8C=85=20=EC=B2=A8?= =?UTF-8?q?=EB=B6=80=ED=8C=8C=EC=9D=BC=20=EA=B4=80=EB=A0=A8=20fixture=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/ChatAttachment.java | 10 ++++ .../chat/fixture/ChatAttachmentFixture.java | 30 ++++++++++++ .../fixture/ChatAttachmentFixtureBuilder.java | 48 +++++++++++++++++++ 3 files changed, 88 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixture.java create mode 100644 src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixtureBuilder.java diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java index 5c0f5e651..def9263c8 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java @@ -32,4 +32,14 @@ public class ChatAttachment extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private ChatMessage chatMessage; + + public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMessage chatMessage) { + this.isImage = isImage; + this.url = url; + this.thumbnailUrl = thumbnailUrl; + this.chatMessage = chatMessage; + if (chatMessage != null) { + chatMessage.getChatAttachments().add(this); + } + } } diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixture.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixture.java new file mode 100644 index 000000000..37f85c6e9 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixture.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatAttachment; +import com.example.solidconnection.chat.domain.ChatMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatAttachmentFixture { + + private final ChatAttachmentFixtureBuilder chatAttachmentFixtureBuilder; + + public ChatAttachment 첨부파일(boolean isImage, String url, String thumbnailUrl, ChatMessage chatMessage) { + return chatAttachmentFixtureBuilder.chatAttachment() + .isImage(isImage) + .url(url) + .thumbnailUrl(thumbnailUrl) + .chatMessage(chatMessage) + .create(); + } + + public ChatAttachment 이미지(String url, String thumbnailUrl, ChatMessage chatMessage) { + return 첨부파일(true, url, thumbnailUrl, chatMessage); + } + + public ChatAttachment 파일(String url, ChatMessage chatMessage) { + return 첨부파일(false, url, null, chatMessage); + } +} diff --git a/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixtureBuilder.java b/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixtureBuilder.java new file mode 100644 index 000000000..7db17caf0 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/fixture/ChatAttachmentFixtureBuilder.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.chat.fixture; + +import com.example.solidconnection.chat.domain.ChatAttachment; +import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.repository.ChatAttachmentRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class ChatAttachmentFixtureBuilder { + + private final ChatAttachmentRepository chatAttachmentRepository; + + private boolean isImage; + private String url; + private String thumbnailUrl; + private ChatMessage chatMessage; + + public ChatAttachmentFixtureBuilder chatAttachment() { + return new ChatAttachmentFixtureBuilder(chatAttachmentRepository); + } + + public ChatAttachmentFixtureBuilder isImage(boolean isImage) { + this.isImage = isImage; + return this; + } + + public ChatAttachmentFixtureBuilder url(String url) { + this.url = url; + return this; + } + + public ChatAttachmentFixtureBuilder thumbnailUrl(String thumbnailUrl) { + this.thumbnailUrl = thumbnailUrl; + return this; + } + + public ChatAttachmentFixtureBuilder chatMessage(ChatMessage chatMessage) { + this.chatMessage = chatMessage; + return this; + } + + public ChatAttachment create() { + ChatAttachment attachment = new ChatAttachment(isImage, url, thumbnailUrl, chatMessage); + return chatAttachmentRepository.save(attachment); + } +} From a7df7bbc9216d79c9a1c0fa9d74db49b83edbc5b Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 17:55:12 +0900 Subject: [PATCH 13/32] =?UTF-8?q?test:=20=EC=B1=84=ED=8C=85=20=EB=82=B4?= =?UTF-8?q?=EC=97=AD=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatServiceTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index c9ce6a084..97b3e2f91 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -1,19 +1,24 @@ package com.example.solidconnection.chat.service; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +import com.example.solidconnection.chat.domain.ChatAttachment; import com.example.solidconnection.chat.domain.ChatMessage; import com.example.solidconnection.chat.domain.ChatParticipant; import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; +import com.example.solidconnection.chat.fixture.ChatAttachmentFixture; import com.example.solidconnection.chat.fixture.ChatMessageFixture; import com.example.solidconnection.chat.fixture.ChatParticipantFixture; import com.example.solidconnection.chat.fixture.ChatReadStatusFixture; import com.example.solidconnection.chat.fixture.ChatRoomFixture; +import com.example.solidconnection.common.dto.SliceResponse; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; @@ -23,6 +28,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; @TestContainerSpringBootTest @DisplayName("채팅 서비스 테스트") @@ -46,6 +54,9 @@ class ChatServiceTest { @Autowired private ChatReadStatusFixture chatReadStatusFixture; + @Autowired + private ChatAttachmentFixture chatAttachmentFixture; + private SiteUser user; private SiteUser mentor1; private SiteUser mentor2; @@ -186,4 +197,115 @@ void setUp() { assertThat(response.chatRooms().get(0).unReadCount()).isEqualTo(2); } } + + @Nested + class 채팅_메시지를_조회한다 { + + private static final int NO_NEXT_PAGE_NUMBER = -1; + + private ChatRoom chatRoom; + private Pageable pageable; + + @BeforeEach + void setUp() { + chatRoom = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom); + + pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); + } + + @Test + void 메시지가_없는_채팅방에서_빈_목록을_반환한다() { + // when + SliceResponse response = chatService.getChatMessages(user.getId(), chatRoom.getId(), pageable); + + // then + assertAll( + () -> assertThat(response.content()).isEmpty(), + () -> assertThat(response.nextPageNumber()).isEqualTo(NO_NEXT_PAGE_NUMBER) + ); + } + + @Test + void 첨부파일이_없는_메시지들을_정상_조회한다() { + // given + ChatMessage message1 = chatMessageFixture.메시지("메시지1", mentor1.getId(), chatRoom); + ChatMessage message2 = chatMessageFixture.메시지("메시지2", user.getId(), chatRoom); + + // when + SliceResponse response = chatService.getChatMessages(user.getId(), chatRoom.getId(), pageable); + + // then + assertAll( + () -> assertThat(response.content()).hasSize(2), + () -> assertThat(response.content().get(0).content()).isEqualTo(message2.getContent()), + () -> assertThat(response.content().get(0).senderId()).isEqualTo(user.getId()), + () -> assertThat(response.content().get(1).content()).isEqualTo(message1.getContent()), + () -> assertThat(response.content().get(1).senderId()).isEqualTo(mentor1.getId()) + ); + } + + @Test + void 첨부파일이_있는_메시지를_정상_조회한다() { + // given + ChatMessage messageWithImage = chatMessageFixture.메시지("이미지", mentor1.getId(), chatRoom); + ChatAttachment imageAttachment = chatAttachmentFixture.첨부파일( + true, + "https://example.com/image.png", + "https://example.com/thumb.png", + messageWithImage + ); + + // when + SliceResponse response = chatService.getChatMessages(user.getId(), chatRoom.getId(), pageable); + + // then + assertAll( + () -> assertThat(response.content()).hasSize(1), + () -> assertThat(response.content().get(0).content()).isEqualTo(messageWithImage.getContent()), + () -> assertThat(response.content().get(0).attachments()).hasSize(1), + () -> assertThat(response.content().get(0).attachments().get(0).id()).isEqualTo(imageAttachment.getId()) + ); + } + + @Test + void 페이징이_정상_작동한다() { + for (int i = 1; i <= 25; i++) { + chatMessageFixture.메시지("메시지" + i, (i % 2 == 0) ? user.getId() : mentor1.getId(), chatRoom); + } + + Pageable firstPage = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt")); + Pageable secondPage = PageRequest.of(1, 20, Sort.by(Sort.Direction.DESC, "createdAt")); + + // when + SliceResponse firstResponse = chatService.getChatMessages(user.getId(), chatRoom.getId(), firstPage); + SliceResponse secondResponse = chatService.getChatMessages(user.getId(), chatRoom.getId(), secondPage); + + // then + assertAll( + () -> assertThat(firstResponse.nextPageNumber()).isEqualTo(2), + () -> assertThat(secondResponse.nextPageNumber()).isEqualTo(NO_NEXT_PAGE_NUMBER) + ); + } + + @Test + void 채팅방_참여자가_아니면_예외가_발생한다() { + // when & then + assertThatCode(() -> chatService.getChatMessages(mentor2.getId(), chatRoom.getId(), pageable)) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + } + + @Test + void 존재하지_않는_채팅방에_접근하면_예외가_발생한다() { + // given + long nonExistentRoomId = 999L; + + // when & then + assertThatCode(() -> chatService.getChatMessages(user.getId(), nonExistentRoomId, pageable)) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + } + } } From df2b913d30bf389bec3c8480691957eb271418b5 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:26:42 +0900 Subject: [PATCH 14/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EA=B4=80=EB=A0=A8=20=EB=A0=88?= =?UTF-8?q?=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatParticipantRepository.java | 5 ++++- .../chat/repository/ChatReadStatusRepository.java | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java index 0f4de58d7..4bce2d08c 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatParticipantRepository.java @@ -1,9 +1,12 @@ package com.example.solidconnection.chat.repository; import com.example.solidconnection.chat.domain.ChatParticipant; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface ChatParticipantRepository extends JpaRepository { - boolean existsByChatRoomIdAndSiteUserId(Long chatRoomId, Long siteUserId); + boolean existsByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId); + + Optional findByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId); } diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java index 54dfff5b6..c5c7417a2 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java @@ -1,8 +1,19 @@ package com.example.solidconnection.chat.repository; import com.example.solidconnection.chat.domain.ChatReadStatus; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface ChatReadStatusRepository extends JpaRepository { + @Modifying + @Query(value = """ + INSERT INTO chat_read_status (chat_room_id, chat_participant_id, created_at, updated_at) + VALUES (:chatRoomId, :chatParticipantId, NOW(6), NOW(6)) + ON DUPLICATE KEY UPDATE updated_at = NOW(6) + """, nativeQuery = true) + void upsertReadStatus(@Param("chatRoomId") long chatRoomId, @Param("chatParticipantId") long chatParticipantId); } From c6b5115d45892f57c6fd3c94065cb1d8599e0561 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:26:51 +0900 Subject: [PATCH 15/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatService.java | 27 ++++++++++++++----- .../common/exception/ErrorCode.java | 3 ++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 3fd2b11ac..17cdb0bfa 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -1,5 +1,6 @@ package com.example.solidconnection.chat.service; +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTICIPANT_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; @@ -15,6 +16,7 @@ import com.example.solidconnection.chat.dto.ChatRoomResponse; import com.example.solidconnection.chat.repository.ChatMessageRepository; import com.example.solidconnection.chat.repository.ChatParticipantRepository; +import com.example.solidconnection.chat.repository.ChatReadStatusRepository; import com.example.solidconnection.chat.repository.ChatRoomRepository; import com.example.solidconnection.common.dto.SliceResponse; import com.example.solidconnection.common.exception.CustomException; @@ -38,6 +40,7 @@ public class ChatService { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; private final ChatParticipantRepository chatParticipantRepository; + private final ChatReadStatusRepository chatReadStatusRepository; private final SiteUserRepository siteUserRepository; @Transactional(readOnly = true) @@ -95,13 +98,6 @@ public SliceResponse getChatMessages(long siteUserId, long return SliceResponse.of(content, chatMessages); } - private void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_ROOM_ACCESS_DENIED); - } - } - private ChatMessageResponse toChatMessageResponse(ChatMessage message) { List attachments = message.getChatAttachments().stream() .map(attachment -> ChatAttachmentResponse.of( @@ -121,4 +117,21 @@ private ChatMessageResponse toChatMessageResponse(ChatMessage message) { attachments ); } + + @Transactional + public void markChatMessagesAsRead(long siteUserId, long roomId) { + validateChatRoomParticipant(siteUserId, roomId); + + ChatParticipant participant = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId) + .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND)); + + chatReadStatusRepository.upsertReadStatus(roomId, participant.getId()); + } + + private void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_ROOM_ACCESS_DENIED); + } + } } diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 70f410665..7556862e0 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -45,6 +45,8 @@ public enum ErrorCode { NEWS_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 소식지입니다."), MENTOR_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 멘토입니다."), REPORT_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 신고 대상입니다."), + CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."), + CHAT_PARTICIPANT_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "채팅 참여자를 찾을 수 없습니다."), // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), @@ -116,7 +118,6 @@ public enum ErrorCode { ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."), // chat - CHAT_PARTNER_NOT_FOUND(HttpStatus.BAD_REQUEST.value(), "채팅 상대를 찾을 수 없습니다."), INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."), CHAT_ROOM_ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "채팅방 접근이 거부되었습니다."), From a89dd5fa325d86198f180d096b6f9b21f9d4e225 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:27:05 +0900 Subject: [PATCH 16/32] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/controller/ChatController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java index e8c905795..4800b0ed9 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java @@ -12,6 +12,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -38,4 +39,12 @@ public ResponseEntity> getChatMessages( SliceResponse response = chatService.getChatMessages(siteUserId, roomId, pageable); return ResponseEntity.ok(response); } + + @PutMapping("/rooms/{room-id}/read") + public ResponseEntity markChatMessagesAsRead( + @AuthorizedUser long siteUserId, + @PathVariable("room-id") Long roomId) { + chatService.markChatMessagesAsRead(siteUserId, roomId); + return ResponseEntity.ok().build(); + } } From fa58816db73caa4768be088a0bc85840d7b2b984 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:27:24 +0900 Subject: [PATCH 17/32] =?UTF-8?q?test:=20=EC=B1=84=ED=8C=85=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChatReadStatusRepositoryForTest.java | 10 +++ .../chat/service/ChatServiceTest.java | 70 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/chat/repository/ChatReadStatusRepositoryForTest.java diff --git a/src/test/java/com/example/solidconnection/chat/repository/ChatReadStatusRepositoryForTest.java b/src/test/java/com/example/solidconnection/chat/repository/ChatReadStatusRepositoryForTest.java new file mode 100644 index 000000000..894276b78 --- /dev/null +++ b/src/test/java/com/example/solidconnection/chat/repository/ChatReadStatusRepositoryForTest.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.chat.repository; + +import com.example.solidconnection.chat.domain.ChatReadStatus; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatReadStatusRepositoryForTest extends JpaRepository { + + Optional findByChatRoomIdAndChatParticipantId(long chatRoomId, long chatParticipantId); +} diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 97b3e2f91..003350763 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -10,6 +10,7 @@ import com.example.solidconnection.chat.domain.ChatAttachment; import com.example.solidconnection.chat.domain.ChatMessage; import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatReadStatus; import com.example.solidconnection.chat.domain.ChatRoom; import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; @@ -18,11 +19,13 @@ import com.example.solidconnection.chat.fixture.ChatParticipantFixture; import com.example.solidconnection.chat.fixture.ChatReadStatusFixture; import com.example.solidconnection.chat.fixture.ChatRoomFixture; +import com.example.solidconnection.chat.repository.ChatReadStatusRepositoryForTest; import com.example.solidconnection.common.dto.SliceResponse; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.time.ZonedDateTime; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -39,6 +42,9 @@ class ChatServiceTest { @Autowired private ChatService chatService; + @Autowired + private ChatReadStatusRepositoryForTest chatReadStatusRepositoryForTest; + @Autowired private SiteUserFixture siteUserFixture; @@ -308,4 +314,68 @@ void setUp() { .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); } } + + @Nested + class 채팅_메시지_읽음을_처리한다 { + + private ChatRoom chatRoom; + private ChatParticipant participant; + + @BeforeEach + void setUp() { + chatRoom = chatRoomFixture.채팅방(false); + participant = chatParticipantFixture.참여자(user.getId(), chatRoom); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom); + } + + @Test + void 처음_읽음_처리_시_새로운_읽음_상태를_생성한다() { + // given + chatMessageFixture.메시지("읽지 않은 메시지1", mentor1.getId(), chatRoom); + chatMessageFixture.메시지("읽지 않은 메시지2", mentor1.getId(), chatRoom); + + // when + chatService.markChatMessagesAsRead(user.getId(), chatRoom.getId()); + + // then + ChatReadStatus afterStatus = chatReadStatusRepositoryForTest + .findByChatRoomIdAndChatParticipantId(chatRoom.getId(), participant.getId()) + .orElseThrow(); + + assertThat(afterStatus.getChatRoomId()).isEqualTo(chatRoom.getId()); + } + + @Test + void 기존_읽음_상태가_있으면_updatedAt을_갱신한다() { + // given + ChatReadStatus chatReadStatus = chatReadStatusFixture.읽음상태(chatRoom.getId(), participant.getId()); + ZonedDateTime updatedAt = chatReadStatus.getUpdatedAt(); + chatMessageFixture.메시지("새로운 메시지", mentor1.getId(), chatRoom); + + // when + chatService.markChatMessagesAsRead(user.getId(), chatRoom.getId()); + + // then + ChatReadStatus updatedStatus = chatReadStatusRepositoryForTest + .findByChatRoomIdAndChatParticipantId(chatRoom.getId(), participant.getId()) + .orElseThrow(); + assertAll( + () -> assertThat(updatedStatus.getId()).isEqualTo(chatReadStatus.getId()), + () -> assertThat(updatedStatus.getUpdatedAt()).isAfter(updatedAt) + ); + } + + @Test + void 채팅방_참여자가_아니면_예외가_발생한다() { + // given + ChatRoom chatRoom = chatRoomFixture.채팅방(false); + chatParticipantFixture.참여자(user.getId(), chatRoom); + chatParticipantFixture.참여자(mentor1.getId(), chatRoom); + + // when & then + assertThatCode(() -> chatService.markChatMessagesAsRead(mentor2.getId(), chatRoom.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + } + } } From 8ea1cca3a05f9e87bca0a1e823ab1756ef885daa Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 18:28:31 +0900 Subject: [PATCH 18/32] =?UTF-8?q?style:=20reformat=20code=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/domain/ChatMessage.java | 2 +- .../example/solidconnection/chat/domain/ChatRoom.java | 4 ++-- .../chat/repository/ChatReadStatusRepository.java | 9 ++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java index 02e2de525..07fc99131 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java @@ -34,7 +34,7 @@ public class ChatMessage extends BaseEntity { private ChatRoom chatRoom; @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL) - private List chatAttachments = new ArrayList<>(); + private final List chatAttachments = new ArrayList<>(); public ChatMessage(String content, long senderId, ChatRoom chatRoom) { this.content = content; diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java index 28db6c3be..f0a930c8b 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java @@ -25,10 +25,10 @@ public class ChatRoom extends BaseEntity { private boolean isGroup = false; @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) - private List chatParticipants = new ArrayList<>(); + private final List chatParticipants = new ArrayList<>(); @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) - private List chatMessages = new ArrayList<>(); + private final List chatMessages = new ArrayList<>(); public ChatRoom(boolean isGroup) { this.isGroup = isGroup; diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java index c5c7417a2..7368b82c0 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java @@ -1,7 +1,6 @@ package com.example.solidconnection.chat.repository; import com.example.solidconnection.chat.domain.ChatReadStatus; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -11,9 +10,9 @@ public interface ChatReadStatusRepository extends JpaRepository Date: Sun, 27 Jul 2025 18:29:57 +0900 Subject: [PATCH 19/32] =?UTF-8?q?refactor:=20ChatMessageRepository?= =?UTF-8?q?=EB=A1=9C=20=ED=95=A8=EC=88=98=20=EC=9C=84=EC=B9=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatMessageRepository.java | 9 +++++++++ .../chat/repository/ChatRoomRepository.java | 10 ---------- .../solidconnection/chat/service/ChatService.java | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java index 8846b62e0..9bdb22b00 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java @@ -1,6 +1,7 @@ package com.example.solidconnection.chat.repository; import com.example.solidconnection.chat.domain.ChatMessage; +import java.util.Optional; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -16,4 +17,12 @@ public interface ChatMessageRepository extends JpaRepository ORDER BY cm.createdAt DESC """) Slice findByRoomIdWithPaging(@Param("roomId") long roomId, Pageable pageable); + + @Query(""" + SELECT cm FROM ChatMessage cm + WHERE cm.chatRoom.id = :chatRoomId + ORDER BY cm.createdAt DESC + LIMIT 1 + """) + Optional findLatestMessageByChatRoomId(@Param("chatRoomId") long chatRoomId); } diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java index 88c92045a..dd5193abf 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java @@ -1,9 +1,7 @@ package com.example.solidconnection.chat.repository; -import com.example.solidconnection.chat.domain.ChatMessage; import com.example.solidconnection.chat.domain.ChatRoom; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -22,14 +20,6 @@ SELECT MAX(cm.createdAt) """) List findOneOnOneChatRoomsByUserId(@Param("userId") long userId); - @Query(""" - SELECT cm FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - ORDER BY cm.createdAt DESC - LIMIT 1 - """) - Optional findLatestMessageByChatRoomId(@Param("chatRoomId") long chatRoomId); - @Query(""" SELECT COUNT(cm) FROM ChatMessage cm LEFT JOIN ChatReadStatus crs ON crs.chatRoomId = cm.chatRoom.id diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 17cdb0bfa..a9b33c62b 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -53,7 +53,7 @@ public ChatRoomListResponse getChatRooms(long siteUserId) { } private ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, long siteUserId) { - Optional latestMessage = chatRoomRepository.findLatestMessageByChatRoomId(chatRoom.getId()); + Optional latestMessage = chatMessageRepository.findLatestMessageByChatRoomId(chatRoom.getId()); String lastChatMessage = latestMessage.map(ChatMessage::getContent).orElse(""); ZonedDateTime lastReceivedTime = latestMessage.map(ChatMessage::getCreatedAt).orElse(null); From 747c635cd53ec177e956c79173511e003f87eca5 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:01:48 +0900 Subject: [PATCH 20/32] =?UTF-8?q?feat:=20=EB=A9=98=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=8B=9C=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatRoomRepository.java | 20 +++++++++++++++ .../service/MentoringCommandService.java | 25 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java index dd5193abf..755ce2e88 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java @@ -2,6 +2,7 @@ import com.example.solidconnection.chat.domain.ChatRoom; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -33,4 +34,23 @@ SELECT COUNT(cm) FROM ChatMessage cm AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt) """) long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId); + + @Query(""" + SELECT cr FROM ChatRoom cr + LEFT JOIN FETCH cr.chatParticipants cp + WHERE cr.isGroup = false + AND EXISTS ( + SELECT 1 FROM ChatParticipant cp1 + WHERE cp1.chatRoom = cr AND cp1.siteUserId = :mentorId + ) + AND EXISTS ( + SELECT 1 FROM ChatParticipant cp2 + WHERE cp2.chatRoom = cr AND cp2.siteUserId = :menteeId + ) + AND ( + SELECT COUNT(cp3) FROM ChatParticipant cp3 + WHERE cp3.chatRoom = cr + ) = 2 + """) + Optional findOneOnOneChatRoomByParticipants(@Param("mentorId") long mentorId, @Param("menteeId") long menteeId); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java index ca35f6c95..fc77e796c 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java @@ -5,6 +5,10 @@ import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.UNAUTHORIZED_MENTORING; +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatParticipantRepository; +import com.example.solidconnection.chat.repository.ChatRoomRepository; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; @@ -16,6 +20,8 @@ import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.mentor.repository.MentoringRepository; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,6 +32,8 @@ public class MentoringCommandService { private final MentoringRepository mentoringRepository; private final MentorRepository mentorRepository; + private final ChatRoomRepository chatRoomRepository; + private final ChatParticipantRepository chatParticipantRepository; @Transactional public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) { @@ -49,6 +57,8 @@ public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoring if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) { mentor.increaseMenteeCount(); + // todo : 예외 처리 관련 고려 필요 + createChatRoomIfNotExists(mentor.getSiteUserId(), mentoring.getMenteeId()); } return MentoringConfirmResponse.from(mentoring); @@ -60,6 +70,21 @@ private void validateMentoringNotConfirmed(Mentoring mentoring) { } } + private void createChatRoomIfNotExists(long mentorUserId, long menteeUserId) { + Optional existingChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUserId, menteeUserId); + if (existingChatRoom.isPresent()) { + return; + } + + ChatRoom newChatRoom = new ChatRoom(false); + ChatRoom savedChatRoom = chatRoomRepository.save(newChatRoom); + + ChatParticipant mentorParticipant = new ChatParticipant(mentorUserId, savedChatRoom); + ChatParticipant menteeParticipant = new ChatParticipant(menteeUserId, savedChatRoom); + + chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); + } + @Transactional public MentoringCheckResponse checkMentoring(long siteUserId, long mentoringId) { Mentoring mentoring = mentoringRepository.findById(mentoringId) From 320d5f58e3663a1d416a8cba4698a979a0cf8a80 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:02:13 +0900 Subject: [PATCH 21/32] =?UTF-8?q?test:=20=EB=A9=98=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EC=8B=9C=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/MentoringCommandServiceTest.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java index a7cea53bb..7c9ef8155 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java @@ -7,6 +7,9 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import com.example.solidconnection.chat.domain.ChatParticipant; +import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.repository.ChatRoomRepository; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; @@ -23,6 +26,8 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -42,6 +47,9 @@ class MentoringCommandServiceTest { @Autowired private MentoringRepository mentoringRepository; + @Autowired + private ChatRoomRepository chatRoomRepository; + @Autowired private SiteUserFixture siteUserFixture; @@ -115,6 +123,31 @@ class 멘토링_승인_거절_테스트 { ); } + @Test + void 멘토링_승인시_채팅방이_자동으로_생성된다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED); + + Optional beforeChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(beforeChatRoom).isEmpty(); + + // when + mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + ChatRoom afterChatRoom = chatRoomRepository + .findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()) + .orElseThrow(); + List participantIds = afterChatRoom.getChatParticipants().stream() + .map(ChatParticipant::getSiteUserId) + .toList(); + assertAll( + () -> assertThat(afterChatRoom.isGroup()).isFalse(), + () -> assertThat(participantIds).containsExactly(mentorUser1.getId(), menteeUser.getId()) + ); + } + @Test void 멘토링을_성공적으로_거절한다() { // given @@ -137,6 +170,23 @@ class 멘토링_승인_거절_테스트 { ); } + @Test + void 멘토링_거절시_채팅방이_자동으로_생성되지_않는다() { + // given + Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); + MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED); + + Optional beforeChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(beforeChatRoom).isEmpty(); + + // when + mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); + + // then + Optional afterChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); + assertThat(afterChatRoom).isEmpty(); + } + @Test void 다른_멘토의_멘토링을_승인할_수_없다() { // given From 01894529a0d86bd1d13893b453f311e3c1d57f96 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:10:49 +0900 Subject: [PATCH 22/32] =?UTF-8?q?feat:=20=EB=B0=B0=EC=B9=98=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EC=A6=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/solidconnection/chat/domain/ChatRoom.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java index f0a930c8b..e8e7a3ebb 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatRoom.java @@ -12,6 +12,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; @Entity @Getter @@ -25,6 +26,7 @@ public class ChatRoom extends BaseEntity { private boolean isGroup = false; @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) + @BatchSize(size = 10) private final List chatParticipants = new ArrayList<>(); @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.ALL) From fa1f4745f303fa239f60f73ab71cb0ca2cef9f2c Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 27 Jul 2025 21:11:35 +0900 Subject: [PATCH 23/32] =?UTF-8?q?chore:=20todo=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - n + 1 해결 필요 --- .../com/example/solidconnection/chat/service/ChatService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index a9b33c62b..8d774a85d 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -45,6 +45,7 @@ public class ChatService { @Transactional(readOnly = true) public ChatRoomListResponse getChatRooms(long siteUserId) { + // todo : n + 1 문제 해결 필요! List chatRooms = chatRoomRepository.findOneOnOneChatRoomsByUserId(siteUserId); List chatRoomInfos = chatRooms.stream() .map(chatRoom -> toChatRoomResponse(chatRoom, siteUserId)) From 2d9acb5a5620359969dc3887705010b129e1a9da Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 00:27:22 +0900 Subject: [PATCH 24/32] =?UTF-8?q?refactor:=20JPQL=20=EB=8C=80=EC=8B=A0=20S?= =?UTF-8?q?pring=20Data=20JPA=20=ED=91=9C=EC=A4=80=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatMessageRepository.java | 8 +------- .../example/solidconnection/chat/service/ChatService.java | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java index 9bdb22b00..ad0f15630 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatMessageRepository.java @@ -18,11 +18,5 @@ public interface ChatMessageRepository extends JpaRepository """) Slice findByRoomIdWithPaging(@Param("roomId") long roomId, Pageable pageable); - @Query(""" - SELECT cm FROM ChatMessage cm - WHERE cm.chatRoom.id = :chatRoomId - ORDER BY cm.createdAt DESC - LIMIT 1 - """) - Optional findLatestMessageByChatRoomId(@Param("chatRoomId") long chatRoomId); + Optional findFirstByChatRoomIdOrderByCreatedAtDesc(long chatRoomId); } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 8d774a85d..3a7f4265e 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -54,7 +54,7 @@ public ChatRoomListResponse getChatRooms(long siteUserId) { } private ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, long siteUserId) { - Optional latestMessage = chatMessageRepository.findLatestMessageByChatRoomId(chatRoom.getId()); + Optional latestMessage = chatMessageRepository.findFirstByChatRoomIdOrderByCreatedAtDesc(chatRoom.getId()); String lastChatMessage = latestMessage.map(ChatMessage::getContent).orElse(""); ZonedDateTime lastReceivedTime = latestMessage.map(ChatMessage::getCreatedAt).orElse(null); From d9ad86b7e29cd81eed6d17fd8cebd6ece9a58889 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 00:34:42 +0900 Subject: [PATCH 25/32] =?UTF-8?q?refactor:=20DB=EC=99=80=20=EC=98=81?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=84=20=EB=B6=88=EC=9D=BC=EC=B9=98=20=EB=B0=A9=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EC=98=B5=EC=85=98=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/repository/ChatReadStatusRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java index 7368b82c0..5ff82a75b 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatReadStatusRepository.java @@ -8,7 +8,7 @@ public interface ChatReadStatusRepository extends JpaRepository { - @Modifying + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(value = """ INSERT INTO chat_read_status (chat_room_id, chat_participant_id, created_at, updated_at) VALUES (:chatRoomId, :chatParticipantId, NOW(6), NOW(6)) From 02def7d6e1cd58d16532cea8f14ff896b49e0455 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:01:27 +0900 Subject: [PATCH 26/32] =?UTF-8?q?refactor:=20findPartner=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - isGroup과 findFirst를 활용하여 중간 리스트 생성 제거 --- .../chat/service/ChatService.java | 17 +++++------------ .../chat/service/ChatServiceTest.java | 14 -------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 3a7f4265e..82ab8acf9 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -70,20 +70,13 @@ private ChatRoomResponse toChatRoomResponse(ChatRoom chatRoom, long siteUserId) } private ChatParticipant findPartner(ChatRoom chatRoom, long siteUserId) { - List partners = chatRoom.getChatParticipants().stream() - .filter(participant -> participant.getSiteUserId() != siteUserId) - .toList(); - validateOneOnOneChat(partners); - return partners.get(0); - } - - private void validateOneOnOneChat(List partners) { - if (partners.isEmpty()) { - throw new CustomException(CHAT_PARTNER_NOT_FOUND); - } - if (partners.size() > CHAT_PARTNER_LIMIT) { + if (chatRoom.isGroup()) { throw new CustomException(INVALID_CHAT_ROOM_STATE); } + return chatRoom.getChatParticipants().stream() + .filter(participant -> participant.getSiteUserId() != siteUserId) + .findFirst() + .orElseThrow(() -> new CustomException(CHAT_PARTNER_NOT_FOUND)); } @Transactional(readOnly = true) diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 003350763..d7e422a57 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -145,20 +145,6 @@ class 채팅방_목록을_조회한다 { .isInstanceOf(CustomException.class) .hasMessage(CHAT_PARTNER_NOT_FOUND.getMessage()); } - - @Test - void 일대일_채팅방에_참여자가_3명_이상이면_예외가_발생한다() { - // given - ChatRoom chatRoom = chatRoomFixture.채팅방(false); - chatParticipantFixture.참여자(user.getId(), chatRoom); - chatParticipantFixture.참여자(mentor1.getId(), chatRoom); - chatParticipantFixture.참여자(mentor2.getId(), chatRoom); - - // when & then - assertThatCode(() -> chatService.getChatRooms(user.getId())) - .isInstanceOf(CustomException.class) - .hasMessage(INVALID_CHAT_ROOM_STATE.getMessage()); - } } @Nested From a255b3af7e92c7165a0872027249e174a446bd44 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:07:05 +0900 Subject: [PATCH 27/32] =?UTF-8?q?refactor:=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/service/ChatService.java | 7 ++----- .../solidconnection/chat/service/ChatServiceTest.java | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 82ab8acf9..18bc4b413 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -35,8 +35,6 @@ @Service public class ChatService { - public static final int CHAT_PARTNER_LIMIT = 1; - private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; private final ChatParticipantRepository chatParticipantRepository; @@ -114,9 +112,8 @@ private ChatMessageResponse toChatMessageResponse(ChatMessage message) { @Transactional public void markChatMessagesAsRead(long siteUserId, long roomId) { - validateChatRoomParticipant(siteUserId, roomId); - - ChatParticipant participant = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId) + ChatParticipant participant = chatParticipantRepository + .findByChatRoomIdAndSiteUserId(roomId, siteUserId) .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND)); chatReadStatusRepository.upsertReadStatus(roomId, participant.getId()); diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index d7e422a57..1896d63aa 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -1,8 +1,8 @@ package com.example.solidconnection.chat.service; +import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTICIPANT_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; -import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; @@ -361,7 +361,7 @@ void setUp() { // when & then assertThatCode(() -> chatService.markChatMessagesAsRead(mentor2.getId(), chatRoom.getId())) .isInstanceOf(CustomException.class) - .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); } } } From a2e8b1f36453f681e0530fda0b6da3a069226961 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:09:08 +0900 Subject: [PATCH 28/32] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/service/ChatService.java | 3 +-- .../example/solidconnection/common/exception/ErrorCode.java | 1 - .../solidconnection/chat/service/ChatServiceTest.java | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 18bc4b413..da2fecdd5 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -2,7 +2,6 @@ import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTICIPANT_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; -import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; @@ -122,7 +121,7 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { private void validateChatRoomParticipant(long siteUserId, long roomId) { boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); if (!isParticipant) { - throw new CustomException(CHAT_ROOM_ACCESS_DENIED); + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); } } } diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 7556862e0..47f5af097 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -119,7 +119,6 @@ public enum ErrorCode { // chat INVALID_CHAT_ROOM_STATE(HttpStatus.BAD_REQUEST.value(), "잘못된 채팅방 상태입니다."), - CHAT_ROOM_ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "채팅방 접근이 거부되었습니다."), // database DATA_INTEGRITY_VIOLATION(HttpStatus.CONFLICT.value(), "데이터베이스 무결성 제약조건 위반이 발생했습니다."), diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 1896d63aa..60f17e30c 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -2,7 +2,6 @@ import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTICIPANT_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.CHAT_PARTNER_NOT_FOUND; -import static com.example.solidconnection.common.exception.ErrorCode.CHAT_ROOM_ACCESS_DENIED; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; @@ -286,7 +285,7 @@ void setUp() { // when & then assertThatCode(() -> chatService.getChatMessages(mentor2.getId(), chatRoom.getId(), pageable)) .isInstanceOf(CustomException.class) - .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); } @Test @@ -297,7 +296,7 @@ void setUp() { // when & then assertThatCode(() -> chatService.getChatMessages(user.getId(), nonExistentRoomId, pageable)) .isInstanceOf(CustomException.class) - .hasMessage(CHAT_ROOM_ACCESS_DENIED.getMessage()); + .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); } } From 78e9ad2401f79df502d63c7ce5ddc4a900d7b433 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:10:42 +0900 Subject: [PATCH 29/32] =?UTF-8?q?refactor:=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=95=A8=EC=88=98=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/service/ChatService.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index da2fecdd5..c378f6b50 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -109,6 +109,13 @@ private ChatMessageResponse toChatMessageResponse(ChatMessage message) { ); } + private void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); + } + } + @Transactional public void markChatMessagesAsRead(long siteUserId, long roomId) { ChatParticipant participant = chatParticipantRepository @@ -117,11 +124,4 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { chatReadStatusRepository.upsertReadStatus(roomId, participant.getId()); } - - private void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); - } - } } From 8f698cb26b017b4996c088ccdcb76860c6e24182 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:14:09 +0900 Subject: [PATCH 30/32] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatRoomRepository.java | 20 -------- .../service/MentoringCommandService.java | 25 ---------- .../service/MentoringCommandServiceTest.java | 50 ------------------- 3 files changed, 95 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java index 755ce2e88..dd5193abf 100644 --- a/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java +++ b/src/main/java/com/example/solidconnection/chat/repository/ChatRoomRepository.java @@ -2,7 +2,6 @@ import com.example.solidconnection.chat.domain.ChatRoom; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -34,23 +33,4 @@ SELECT COUNT(cm) FROM ChatMessage cm AND (crs.updatedAt IS NULL OR cm.createdAt > crs.updatedAt) """) long countUnreadMessages(@Param("chatRoomId") long chatRoomId, @Param("userId") long userId); - - @Query(""" - SELECT cr FROM ChatRoom cr - LEFT JOIN FETCH cr.chatParticipants cp - WHERE cr.isGroup = false - AND EXISTS ( - SELECT 1 FROM ChatParticipant cp1 - WHERE cp1.chatRoom = cr AND cp1.siteUserId = :mentorId - ) - AND EXISTS ( - SELECT 1 FROM ChatParticipant cp2 - WHERE cp2.chatRoom = cr AND cp2.siteUserId = :menteeId - ) - AND ( - SELECT COUNT(cp3) FROM ChatParticipant cp3 - WHERE cp3.chatRoom = cr - ) = 2 - """) - Optional findOneOnOneChatRoomByParticipants(@Param("mentorId") long mentorId, @Param("menteeId") long menteeId); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java index fc77e796c..ca35f6c95 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentoringCommandService.java @@ -5,10 +5,6 @@ import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_NOT_FOUND; import static com.example.solidconnection.common.exception.ErrorCode.UNAUTHORIZED_MENTORING; -import com.example.solidconnection.chat.domain.ChatParticipant; -import com.example.solidconnection.chat.domain.ChatRoom; -import com.example.solidconnection.chat.repository.ChatParticipantRepository; -import com.example.solidconnection.chat.repository.ChatRoomRepository; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; @@ -20,8 +16,6 @@ import com.example.solidconnection.mentor.dto.MentoringConfirmResponse; import com.example.solidconnection.mentor.repository.MentorRepository; import com.example.solidconnection.mentor.repository.MentoringRepository; -import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,8 +26,6 @@ public class MentoringCommandService { private final MentoringRepository mentoringRepository; private final MentorRepository mentorRepository; - private final ChatRoomRepository chatRoomRepository; - private final ChatParticipantRepository chatParticipantRepository; @Transactional public MentoringApplyResponse applyMentoring(long siteUserId, MentoringApplyRequest mentoringApplyRequest) { @@ -57,8 +49,6 @@ public MentoringConfirmResponse confirmMentoring(long siteUserId, long mentoring if (mentoringConfirmRequest.status() == VerifyStatus.APPROVED) { mentor.increaseMenteeCount(); - // todo : 예외 처리 관련 고려 필요 - createChatRoomIfNotExists(mentor.getSiteUserId(), mentoring.getMenteeId()); } return MentoringConfirmResponse.from(mentoring); @@ -70,21 +60,6 @@ private void validateMentoringNotConfirmed(Mentoring mentoring) { } } - private void createChatRoomIfNotExists(long mentorUserId, long menteeUserId) { - Optional existingChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUserId, menteeUserId); - if (existingChatRoom.isPresent()) { - return; - } - - ChatRoom newChatRoom = new ChatRoom(false); - ChatRoom savedChatRoom = chatRoomRepository.save(newChatRoom); - - ChatParticipant mentorParticipant = new ChatParticipant(mentorUserId, savedChatRoom); - ChatParticipant menteeParticipant = new ChatParticipant(menteeUserId, savedChatRoom); - - chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); - } - @Transactional public MentoringCheckResponse checkMentoring(long siteUserId, long mentoringId) { Mentoring mentoring = mentoringRepository.findById(mentoringId) diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java index 7c9ef8155..a7cea53bb 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentoringCommandServiceTest.java @@ -7,9 +7,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import com.example.solidconnection.chat.domain.ChatParticipant; -import com.example.solidconnection.chat.domain.ChatRoom; -import com.example.solidconnection.chat.repository.ChatRoomRepository; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; @@ -26,8 +23,6 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; -import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -47,9 +42,6 @@ class MentoringCommandServiceTest { @Autowired private MentoringRepository mentoringRepository; - @Autowired - private ChatRoomRepository chatRoomRepository; - @Autowired private SiteUserFixture siteUserFixture; @@ -123,31 +115,6 @@ class 멘토링_승인_거절_테스트 { ); } - @Test - void 멘토링_승인시_채팅방이_자동으로_생성된다() { - // given - Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); - MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.APPROVED); - - Optional beforeChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); - assertThat(beforeChatRoom).isEmpty(); - - // when - mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); - - // then - ChatRoom afterChatRoom = chatRoomRepository - .findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()) - .orElseThrow(); - List participantIds = afterChatRoom.getChatParticipants().stream() - .map(ChatParticipant::getSiteUserId) - .toList(); - assertAll( - () -> assertThat(afterChatRoom.isGroup()).isFalse(), - () -> assertThat(participantIds).containsExactly(mentorUser1.getId(), menteeUser.getId()) - ); - } - @Test void 멘토링을_성공적으로_거절한다() { // given @@ -170,23 +137,6 @@ class 멘토링_승인_거절_테스트 { ); } - @Test - void 멘토링_거절시_채팅방이_자동으로_생성되지_않는다() { - // given - Mentoring mentoring = mentoringFixture.대기중_멘토링(mentor1.getId(), menteeUser.getId()); - MentoringConfirmRequest request = new MentoringConfirmRequest(VerifyStatus.REJECTED); - - Optional beforeChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); - assertThat(beforeChatRoom).isEmpty(); - - // when - mentoringCommandService.confirmMentoring(mentorUser1.getId(), mentoring.getId(), request); - - // then - Optional afterChatRoom = chatRoomRepository.findOneOnOneChatRoomByParticipants(mentorUser1.getId(), menteeUser.getId()); - assertThat(afterChatRoom).isEmpty(); - } - @Test void 다른_멘토의_멘토링을_승인할_수_없다() { // given From b535f6b3138fdad3aad565f931018c18920d3fc6 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:16:16 +0900 Subject: [PATCH 31/32] =?UTF-8?q?style:=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B0=9C=ED=96=89=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 --- .../solidconnection/chat/controller/ChatController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java index 4800b0ed9..1c3b1a5d1 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatController.java @@ -35,7 +35,8 @@ public ResponseEntity getChatRooms( public ResponseEntity> getChatMessages( @AuthorizedUser long siteUserId, @PathVariable("room-id") Long roomId, - @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) { + @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + ) { SliceResponse response = chatService.getChatMessages(siteUserId, roomId, pageable); return ResponseEntity.ok(response); } @@ -43,7 +44,8 @@ public ResponseEntity> getChatMessages( @PutMapping("/rooms/{room-id}/read") public ResponseEntity markChatMessagesAsRead( @AuthorizedUser long siteUserId, - @PathVariable("room-id") Long roomId) { + @PathVariable("room-id") Long roomId + ) { chatService.markChatMessagesAsRead(siteUserId, roomId); return ResponseEntity.ok().build(); } From 82219b7334ee313ac6fd7728fad0aa585e616fe9 Mon Sep 17 00:00:00 2001 From: Gyuhyeok99 <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 31 Jul 2025 01:24:54 +0900 Subject: [PATCH 32/32] =?UTF-8?q?style:=20=EC=BB=A8=EB=B2=A4=EC=85=98?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EA=B0=9C=ED=96=89=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 - 코드래빗 의견 반영 --- .../example/solidconnection/chat/service/ChatServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 60f17e30c..77120e884 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -107,8 +107,8 @@ class 채팅방_목록을_조회한다 { () -> assertThat(response.chatRooms().get(0).partner().partnerId()).isEqualTo(mentor2.getId()), () -> assertThat(response.chatRooms().get(0).lastChatMessage()).isEqualTo(newMessage.getContent()), () -> assertThat(response.chatRooms().get(1).partner().partnerId()).isEqualTo(mentor1.getId()), - () -> assertThat(response.chatRooms().get(1).lastChatMessage()).isEqualTo(oldMessage.getContent() - )); + () -> assertThat(response.chatRooms().get(1).lastChatMessage()).isEqualTo(oldMessage.getContent()) + ); } @Test