From de77a9d4a3b6e99994367643be1a605d243b23ba Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 18:05:58 +0900 Subject: [PATCH 01/33] =?UTF-8?q?[feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20api=20=EA=B5=AC=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 22 ++++++++++ .../application/room/ChatRoomService.java | 32 ++++++++++++++ .../chacall/domain/chat/domain/ChatRoom.java | 13 +++++- .../domain/repository/ChatRoomRepository.java | 7 +++ .../chat/presentation/ChatController.java | 4 -- .../chat/presentation/ChatRestController.java | 44 +++++++++++++++++++ .../dto/request/CreateChatRoomRequest.java | 11 +++++ .../dto/response/ChatRoomIdResponse.java | 13 ++++++ .../swagger/SwaggerResponseDescription.java | 7 +++ 9 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java delete mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/CreateChatRoomRequest.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomIdResponse.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 27e0dd0d..48f2976c 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -1,4 +1,26 @@ package konkuk.chacall.domain.chat.application; +import konkuk.chacall.domain.chat.application.room.ChatRoomService; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.domain.member.application.validator.MemberValidator; +import konkuk.chacall.domain.user.domain.model.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) public class ChatService { + + private final ChatRoomService chatRoomService; + + private final MemberValidator memberValidator; + + @Transactional + public ChatRoomIdResponse createChatRoom(Long memberId, Long foodTruckId) { + User member = memberValidator.validateAndGetMember(memberId); + + return chatRoomService.createChatRoom(member, foodTruckId); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java new file mode 100644 index 00000000..6fd7ad4b --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -0,0 +1,32 @@ +package konkuk.chacall.domain.chat.application.room; + +import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; +import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; +import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import static konkuk.chacall.global.common.exception.code.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + private final FoodTruckRepository foodTruckRepository; + + public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { + FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId) + .orElseThrow(() -> new EntityNotFoundException(FOOD_TRUCK_NOT_FOUND)); + + ChatRoom chatRoom = ChatRoom.createChatRoom(member, foodTruck.getOwner()); + + return ChatRoomIdResponse.of( + chatRoomRepository.save(chatRoom) + ); + } +} diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index f306536c..ae82553a 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -2,11 +2,13 @@ import jakarta.persistence.*; import konkuk.chacall.domain.user.domain.model.User; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "chat_rooms") +@Getter +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ChatRoom { @@ -22,5 +24,12 @@ public class ChatRoom { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner_id", nullable = false, referencedColumnName = "user_id") private User owner; + + public static ChatRoom createChatRoom(User member, User owner) { + return ChatRoom.builder() + .member(member) + .owner(owner) + .build(); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java new file mode 100644 index 00000000..82432caf --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java @@ -0,0 +1,7 @@ +package konkuk.chacall.domain.chat.domain.repository; + +import konkuk.chacall.domain.chat.domain.ChatRoom; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChatRoomRepository extends JpaRepository { +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java deleted file mode 100644 index c9b1ccfd..00000000 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java +++ /dev/null @@ -1,4 +0,0 @@ -package konkuk.chacall.domain.chat.presentation; - -public class ChatController { -} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java new file mode 100644 index 00000000..de7fd890 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -0,0 +1,44 @@ +package konkuk.chacall.domain.chat.presentation; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import konkuk.chacall.domain.chat.application.ChatService; +import konkuk.chacall.domain.chat.presentation.dto.request.CreateChatRoomRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.global.common.annotation.ExceptionDescription; +import konkuk.chacall.global.common.annotation.UserId; +import konkuk.chacall.global.common.dto.BaseResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static konkuk.chacall.global.common.swagger.SwaggerResponseDescription.*; + +@Tag(name = "Chat API", description = "채팅 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/chats") +public class ChatRestController { + + private final ChatService chatService; + + @Operation( + summary = "채팅방 생성 (채팅 시작)", + description = "예약자와 사장님간의 채팅방을 생성합니다." + ) + @ExceptionDescription(CREATE_CHAT_ROOM) + @PostMapping("/rooms") + public BaseResponse createChatRoom( + @Parameter(hidden = true) @UserId final Long memberId, + @RequestBody @Valid final CreateChatRoomRequest request + ) { + return BaseResponse.ok( + chatService.createChatRoom(memberId, request.foodTruckId()) + ); + } + +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/CreateChatRoomRequest.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/CreateChatRoomRequest.java new file mode 100644 index 00000000..f0330d3d --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/CreateChatRoomRequest.java @@ -0,0 +1,11 @@ +package konkuk.chacall.domain.chat.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record CreateChatRoomRequest( + @Schema(description = "푸드트럭 ID", example = "1") + @NotNull(message = "푸드트럭 ID는 필수입니다.") + Long foodTruckId +) { +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomIdResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomIdResponse.java new file mode 100644 index 00000000..fe632c83 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomIdResponse.java @@ -0,0 +1,13 @@ +package konkuk.chacall.domain.chat.presentation.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import konkuk.chacall.domain.chat.domain.ChatRoom; + +public record ChatRoomIdResponse( + @Schema(description = "생성된 채팅방 ID", example = "1") + Long chatRoomId +) { + public static ChatRoomIdResponse of(ChatRoom chatRoom) { + return new ChatRoomIdResponse(chatRoom.getChatRoomId()); + } +} diff --git a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java index 437f441f..ae06938f 100644 --- a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java @@ -265,6 +265,13 @@ public enum SwaggerResponseDescription { FOOD_TRUCK_STATUS_MISMATCH ))), + // Chat + CREATE_CHAT_ROOM(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + USER_FORBIDDEN, + FOOD_TRUCK_NOT_FOUND + ))), + // Default DEFAULT(new LinkedHashSet<>()) ; From d804a1cd147833f66100b9c48827a08649a83258 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 19:50:14 +0900 Subject: [PATCH 02/33] =?UTF-8?q?[chore]=20mongoDB=20=EB=B0=8F=20websocket?= =?UTF-8?q?=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index e74c7a90..0d4e3355 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,13 @@ dependencies { // HTML -> PDF (OpenHTMLToPDF) implementation 'com.openhtmltopdf:openhtmltopdf-pdfbox:1.0.10' implementation 'com.openhtmltopdf:openhtmltopdf-slf4j:1.0.10' + + // MongoDB + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + + // STOMP + implementation 'org.springframework.boot:spring-boot-starter-websocket' +// implementation 'org.springframework.boot:spring-boot-starter-messaging' } tasks.named('test') { From 47d8b254fd50c0db51bb92fba7cf5a408ce7dd7f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 19:50:23 +0900 Subject: [PATCH 03/33] =?UTF-8?q?[chore]=20mongoDB=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95=20(#6?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 3 +++ src/main/resources/application-local.yml | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 42ea5f82..d849f437 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -21,6 +21,9 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mongodb: + uri: ${MONGODB_URI} + database: ${MONGODB_DATABASE} logging: level: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 8f008935..992b84fb 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -21,7 +21,13 @@ spring: redis: host: localhost port: 6379 - + mongodb: + host: localhost + port: 27017 + database: chacall + username: ${MONGODB_USERNAME} + password: ${MONGODB_PASSWORD} + authentication-database: chacall --- logging: level: From 17c8dc38958e0458d052e1d1b80bafb9b2e740c9 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 19:50:43 +0900 Subject: [PATCH 04/33] =?UTF-8?q?[chore]=20ChatMessage=EB=A5=BC=20MongoDB?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/domain/ChatMessage.java | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java index 9267b1bb..9ef06340 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java @@ -5,39 +5,35 @@ import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.domain.BaseEntity; import lombok.AccessLevel; +import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; import java.time.LocalDateTime; -@Entity -@Table(name = "chat_messages") +@Getter +@Document(collection = "chat_messages") +@CompoundIndex(name = "room_sender_idx", def = "{'roomId': 1, 'senderId': 1}") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ChatMessage extends BaseEntity { +public class ChatMessage { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(nullable = false) - private Long chatMessageId; + private Long id; - @Column(name = "content", nullable = false, length = 1000) + private Long roomId; + private Long senderId; private String content; - - @Column(nullable = false) + private String contentType; private LocalDateTime sendTime; - - @Column(name = "is_read", nullable = false) - private boolean isRead; - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "chat_room_id", nullable = false) - private ChatRoom chatRoom; - - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "user_id", nullable = false) - private User senderUser; - - @Enumerated(EnumType.STRING) - @Column(nullable = false, length = 20) - private MessageContentType contentType; - + private boolean read; + + public ChatMessage(Long roomId, Long senderId, String content, String contentType) { + this.roomId = roomId; + this.senderId = senderId; + this.content = content; + this.contentType = contentType; + this.sendTime = LocalDateTime.now(); + this.read = false; + } } From b0d259043393ec8209f96647cc31786c91f55dec Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 19:50:57 +0900 Subject: [PATCH 05/33] =?UTF-8?q?[chore]=20Stomp=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/WebSocketConfig.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/config/WebSocketConfig.java diff --git a/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java new file mode 100644 index 00000000..5144efb0 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java @@ -0,0 +1,20 @@ +package konkuk.chacall.global.config; + +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); // SockJS 사용 시 + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); // 발신 prefix + registry.enableSimpleBroker("/topic"); // 수신 prefix + } +} From aab874c0fef5e80eda5723b3820a84bff3fe3a0f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 21:08:26 +0900 Subject: [PATCH 06/33] =?UTF-8?q?[chore]=20ChatRoom=EC=9D=98=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=88=98=EC=A0=95=20(Owner=20->?= =?UTF-8?q?=20FoodTruck)=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/chacall/domain/chat/domain/ChatRoom.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index ae82553a..eac5b00e 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -1,6 +1,7 @@ package konkuk.chacall.domain.chat.domain; import jakarta.persistence.*; +import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.user.domain.model.User; import lombok.*; @@ -18,17 +19,17 @@ public class ChatRoom { private Long chatRoomId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "member_id", nullable = false, referencedColumnName = "user_id") + @JoinColumn(name = "member_id", nullable = false) private User member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "owner_id", nullable = false, referencedColumnName = "user_id") - private User owner; + @JoinColumn(name = "food_truck_id", nullable = false) + private FoodTruck foodTruck; - public static ChatRoom createChatRoom(User member, User owner) { + public static ChatRoom createChatRoom(User member, FoodTruck foodTruck) { return ChatRoom.builder() .member(member) - .owner(owner) + .foodTruck(foodTruck) .build(); } } From 765edab359f4c173fb08d687f588269d5a84c26b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 21:09:28 +0900 Subject: [PATCH 07/33] =?UTF-8?q?[feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=97=90=20=EC=B1=84=ED=8C=85=20=EC=83=81?= =?UTF-8?q?=EB=8C=80=20=ED=91=9C=EC=8B=9C=EC=9A=A9=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 7 ++++++ .../application/room/ChatRoomService.java | 22 ++++++++++++++++++- .../domain/repository/ChatRoomRepository.java | 4 ++++ .../chat/presentation/ChatRestController.java | 22 +++++++++++++++---- .../dto/response/ChatOpponentResponse.java | 12 ++++++++++ .../common/exception/code/ErrorCode.java | 8 ++++++- .../swagger/SwaggerResponseDescription.java | 8 ++++++- 7 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 48f2976c..060f294b 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -1,6 +1,7 @@ package konkuk.chacall.domain.chat.application; import konkuk.chacall.domain.chat.application.room.ChatRoomService; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.member.application.validator.MemberValidator; import konkuk.chacall.domain.user.domain.model.User; @@ -23,4 +24,10 @@ public ChatRoomIdResponse createChatRoom(Long memberId, Long foodTruckId) { return chatRoomService.createChatRoom(member, foodTruckId); } + + public ChatOpponentResponse getChatOpponentName(Long memberId, Long roomId) { + User user = memberValidator.validateAndGetMember(memberId); + + return chatRoomService.getChatOpponentName(user, roomId); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index 6fd7ad4b..cb522f01 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -2,10 +2,12 @@ import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.exception.BusinessException; import konkuk.chacall.global.common.exception.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,10 +25,28 @@ public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId) .orElseThrow(() -> new EntityNotFoundException(FOOD_TRUCK_NOT_FOUND)); - ChatRoom chatRoom = ChatRoom.createChatRoom(member, foodTruck.getOwner()); + // 채팅방이 이미 존재하는지 확인 + if(chatRoomRepository.existsByMemberAndFoodTruck(member, foodTruck)) { + throw new EntityNotFoundException(CHAT_ROOM_ALREADY_EXISTS); + } + + ChatRoom chatRoom = ChatRoom.createChatRoom(member, foodTruck); return ChatRoomIdResponse.of( chatRoomRepository.save(chatRoom) ); } + + public ChatOpponentResponse getChatOpponentName(User user, Long roomId) { + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); + + String name = switch (user.getRole()) { + case MEMBER -> chatRoom.getFoodTruck().getFoodTruckInfo().getName(); + case OWNER -> chatRoom.getMember().getName(); + default -> throw new BusinessException(USER_FORBIDDEN); + }; + + return ChatOpponentResponse.of(name); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java index 82432caf..1c943f13 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java @@ -1,7 +1,11 @@ package konkuk.chacall.domain.chat.domain.repository; import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; +import konkuk.chacall.domain.user.domain.model.User; import org.springframework.data.jpa.repository.JpaRepository; public interface ChatRoomRepository extends JpaRepository { + + boolean existsByMemberAndFoodTruck(User member, FoodTruck foodTruck); } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index de7fd890..f71dad18 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -6,15 +6,13 @@ import jakarta.validation.Valid; import konkuk.chacall.domain.chat.application.ChatService; import konkuk.chacall.domain.chat.presentation.dto.request.CreateChatRoomRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.global.common.annotation.ExceptionDescription; import konkuk.chacall.global.common.annotation.UserId; import konkuk.chacall.global.common.dto.BaseResponse; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static konkuk.chacall.global.common.swagger.SwaggerResponseDescription.*; @@ -41,4 +39,20 @@ public BaseResponse createChatRoom( ); } + @Operation( + summary = "채팅 상대 이름 조회", + description = "채팅 상단에 표시되는 채팅 상대의 이름을 조회합니다." + ) + @ExceptionDescription(GET_CHAT_OPPONENT_NAME) + @GetMapping("/rooms/{roomId}") + public BaseResponse getChatOpponentName( + @Parameter(hidden = true) @UserId final Long memberId, + @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId + ) { + return BaseResponse.ok( + chatService.getChatOpponentName(memberId, roomId) + ); + } + + } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java new file mode 100644 index 00000000..c7b304d1 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java @@ -0,0 +1,12 @@ +package konkuk.chacall.domain.chat.presentation.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ChatOpponentResponse( + @Schema(description = "채팅 상대 이름 (일반 유저 -> 푸드트럭 이름 / 사장님 -> 예약자 이름)", example = "홍길동 or 맛있는푸드트럭") + String name +) { + public static ChatOpponentResponse of(String name) { + return new ChatOpponentResponse(name); + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java index b29ff399..b1f180c9 100644 --- a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java @@ -113,7 +113,13 @@ public enum ErrorCode implements ResponseCode { /** * FoodTruckServiceArea */ - FOOD_TRUCK_SERVICE_AREA_NOT_FOUND(HttpStatus.NOT_FOUND, 150001, "푸드트럭 서비스 지역을 찾을 수 없습니다.") + FOOD_TRUCK_SERVICE_AREA_NOT_FOUND(HttpStatus.NOT_FOUND, 150001, "푸드트럭 서비스 지역을 찾을 수 없습니다."), + + /** + * ChatRoom + */ + CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 160001, "채팅방을 찾을 수 없습니다."), + CHAT_ROOM_ALREADY_EXISTS(HttpStatus.CONFLICT, 160002, "이미 존재하는 채팅방입니다.") ; private final HttpStatus httpStatus; diff --git a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java index ae06938f..b023d43c 100644 --- a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java @@ -269,7 +269,13 @@ public enum SwaggerResponseDescription { CREATE_CHAT_ROOM(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, USER_FORBIDDEN, - FOOD_TRUCK_NOT_FOUND + FOOD_TRUCK_NOT_FOUND, + CHAT_ROOM_ALREADY_EXISTS + ))), + GET_CHAT_OPPONENT_NAME(new LinkedHashSet<>(Set.of( + USER_NOT_FOUND, + USER_FORBIDDEN, + CHAT_ROOM_NOT_FOUND ))), // Default From fa9c15f58126418fa642edcbc18feada92640612 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 3 Nov 2025 21:37:40 +0900 Subject: [PATCH 08/33] =?UTF-8?q?[refactor]=20=EC=82=AC=EC=9E=A5=EB=8B=98?= =?UTF-8?q?=EB=81=BC=EB=A6=AC=20=EC=B1=84=ED=8C=85=ED=95=A0=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EC=9D=98=20=EB=B0=9C=EC=83=9D=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EC=98=A4=EB=A5=98=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 14 ++++++++++++-- .../chat/application/room/ChatRoomService.java | 13 +++++++------ .../chat/presentation/ChatRestController.java | 6 ++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 060f294b..d149a1b1 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -1,6 +1,9 @@ package konkuk.chacall.domain.chat.application; +import konkuk.chacall.domain.chat.application.message.ChatMessageService; import konkuk.chacall.domain.chat.application.room.ChatRoomService; +import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.member.application.validator.MemberValidator; @@ -15,6 +18,7 @@ public class ChatService { private final ChatRoomService chatRoomService; + private final ChatMessageService chatMessageService; private final MemberValidator memberValidator; @@ -25,9 +29,15 @@ public ChatRoomIdResponse createChatRoom(Long memberId, Long foodTruckId) { return chatRoomService.createChatRoom(member, foodTruckId); } - public ChatOpponentResponse getChatOpponentName(Long memberId, Long roomId) { + public ChatOpponentResponse getChatOpponentName(Long memberId, Long roomId, boolean isOwner) { User user = memberValidator.validateAndGetMember(memberId); - return chatRoomService.getChatOpponentName(user, roomId); + return chatRoomService.getChatOpponentName(user, roomId, isOwner); + } + + public ChatMessageResponse sendMessage(Long roomId, Long userId, SendChatMessageRequest request) { + User senderUser = memberValidator.validateAndGetMember(userId); + + return chatMessageService.sendMessage(roomId, senderUser, request); } } diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index cb522f01..a5943576 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -6,6 +6,7 @@ import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; +import konkuk.chacall.domain.user.domain.model.Role; import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.exception.BusinessException; import konkuk.chacall.global.common.exception.EntityNotFoundException; @@ -37,15 +38,15 @@ public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { ); } - public ChatOpponentResponse getChatOpponentName(User user, Long roomId) { + public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean isOwner) { ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); - String name = switch (user.getRole()) { - case MEMBER -> chatRoom.getFoodTruck().getFoodTruckInfo().getName(); - case OWNER -> chatRoom.getMember().getName(); - default -> throw new BusinessException(USER_FORBIDDEN); - }; + if(isOwner && user.getRole() != Role.OWNER) { + throw new BusinessException(USER_FORBIDDEN); + } + + String name = isOwner ? chatRoom.getMember().getName() : chatRoom.getFoodTruck().getFoodTruckInfo().getName(); return ChatOpponentResponse.of(name); } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index f71dad18..30a88bf5 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -47,10 +47,12 @@ public BaseResponse createChatRoom( @GetMapping("/rooms/{roomId}") public BaseResponse getChatOpponentName( @Parameter(hidden = true) @UserId final Long memberId, - @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId + @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId, + @Parameter(description = "현재 채팅방 기준 푸드트럭 사장인지 여부", example = "false") + @RequestParam final Boolean isOwner ) { return BaseResponse.ok( - chatService.getChatOpponentName(memberId, roomId) + chatService.getChatOpponentName(memberId, roomId, isOwner) ); } From bbbbbffffe7594754ca13e85295979b8ad884823 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 6 Nov 2025 00:52:04 +0900 Subject: [PATCH 09/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=20=EB=B3=80=EA=B2=BD=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/room/ChatRoomService.java | 7 +++++-- .../domain/chat/presentation/ChatRestController.java | 2 +- .../dto/response/ChatOpponentResponse.java | 10 ++++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index a5943576..7d88bf7e 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -46,8 +46,11 @@ public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean throw new BusinessException(USER_FORBIDDEN); } - String name = isOwner ? chatRoom.getMember().getName() : chatRoom.getFoodTruck().getFoodTruckInfo().getName(); + // 푸드트럭 사장일 경우 예약자 이름 반환 + if(isOwner) return ChatOpponentResponse.of(chatRoom.getMember().getName(), null); - return ChatOpponentResponse.of(name); + // 예약자일 경우 푸드트럭 사장 이름 및 푸드트럭 이름 반환 + FoodTruck foodTruck = chatRoom.getFoodTruck(); + return ChatOpponentResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName()); } } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index 30a88bf5..a4361219 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -19,7 +19,7 @@ @Tag(name = "Chat API", description = "채팅 관련 API") @RestController @RequiredArgsConstructor -@RequestMapping("/chats") +@RequestMapping("/chat") public class ChatRestController { private final ChatService chatService; diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java index c7b304d1..a55ff0fd 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java @@ -3,10 +3,12 @@ import io.swagger.v3.oas.annotations.media.Schema; public record ChatOpponentResponse( - @Schema(description = "채팅 상대 이름 (일반 유저 -> 푸드트럭 이름 / 사장님 -> 예약자 이름)", example = "홍길동 or 맛있는푸드트럭") - String name + @Schema(description = "채팅 상대 이름 (일반 유저 -> 사장님 이름 / 사장님 -> 예약자 이름)", example = "홍길동 or 푸드트럭사장") + String name, + @Schema(description = "푸드트럭 이름 (일반 유저 -> null)", example = "맛있는푸드트럭") + String foodTruckName ) { - public static ChatOpponentResponse of(String name) { - return new ChatOpponentResponse(name); + public static ChatOpponentResponse of(String name, String foodTruckName) { + return new ChatOpponentResponse(name, foodTruckName); } } From 4b1a6dc8fa6371424b7ad42912a51d3ee162b274 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 15:58:23 +0900 Subject: [PATCH 10/33] =?UTF-8?q?[feat]=20=EC=B1=84=ED=8C=85=20api=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/ChatMessageService.java | 60 +++++++++++++++++++ .../domain/chat/domain/ChatMessage.java | 35 ++++++----- .../repository/ChatMessageRepository.java | 11 ++++ .../chat/presentation/ChatController.java | 32 ++++++++++ .../dto/response/ChatMessageResponse.java | 25 ++++++++ .../global/config/WebSocketConfig.java | 8 ++- 6 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatMessageResponse.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java b/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java new file mode 100644 index 00000000..4b4e0a3a --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java @@ -0,0 +1,60 @@ +package konkuk.chacall.domain.chat.application.message; + +import konkuk.chacall.domain.chat.domain.ChatMessage; +import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.repository.ChatMessageRepository; +import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; +import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static konkuk.chacall.global.common.exception.code.ErrorCode.*; + +@Service +@RequiredArgsConstructor +public class ChatMessageService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomRepository chatRoomRepository; + + public ChatMessageResponse sendMessage(Long roomId, User senderUser, SendChatMessageRequest request) { + + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); + + //todo 채팅방 참여자 검증 (쿼리가 최대 3개 호출되는데 추후 캐싱 고려) + chatRoom.validateParticipant(senderUser); + + ChatMessage chatMessage = ChatMessage.createChatMessage( + chatRoom.getChatRoomId(), + senderUser, + request.content(), + request.contentType() + ); + + chatMessageRepository.save(chatMessage); + + return ChatMessageResponse.from(chatMessage); + } + + public List getChatMessages(Long roomId, User user, int page, int size) { + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); + + //todo 채팅방 참여자 검증해야되는데.. 캐싱 고려해서 나중에 + + var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "sendTime")); + + return chatMessageRepository.findByRoomId(chatRoom.getChatRoomId(), pageable) + .stream() + .map(ChatMessageResponse::from) + .toList(); + } +} diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java index 9ef06340..47551b2d 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java @@ -3,10 +3,7 @@ import jakarta.persistence.*; import konkuk.chacall.domain.chat.domain.value.MessageContentType; import konkuk.chacall.domain.user.domain.model.User; -import konkuk.chacall.global.common.domain.BaseEntity; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.mapping.Document; @@ -14,26 +11,32 @@ @Getter @Document(collection = "chat_messages") -@CompoundIndex(name = "room_sender_idx", def = "{'roomId': 1, 'senderId': 1}") +@CompoundIndex(name = "room_time_idx", def = "{'roomId': 1, 'sendTime': 1}") +@Builder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ChatMessage { @Id - private Long id; + private String id; private Long roomId; private Long senderId; private String content; private String contentType; - private LocalDateTime sendTime; - private boolean read; - - public ChatMessage(Long roomId, Long senderId, String content, String contentType) { - this.roomId = roomId; - this.senderId = senderId; - this.content = content; - this.contentType = contentType; - this.sendTime = LocalDateTime.now(); - this.read = false; + + @Builder.Default + private LocalDateTime sendTime = LocalDateTime.now(); + + @Builder.Default + private boolean read = false; + + public static ChatMessage createChatMessage(Long roomId, User sender, String content, MessageContentType contentType) { + return ChatMessage.builder() + .roomId(roomId) + .senderId(sender.getUserId()) + .content(content) + .contentType(contentType.name()) + .build(); } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java new file mode 100644 index 00000000..718ac07b --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java @@ -0,0 +1,11 @@ +package konkuk.chacall.domain.chat.domain.repository; + +import konkuk.chacall.domain.chat.domain.ChatMessage; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; + +public interface ChatMessageRepository extends MongoRepository { + List findByRoomId(Long chatRoomId, PageRequest pageable); +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java new file mode 100644 index 00000000..5283010a --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java @@ -0,0 +1,32 @@ +package konkuk.chacall.domain.chat.presentation; + +import io.swagger.v3.oas.annotations.Parameter; +import konkuk.chacall.domain.chat.application.ChatService; +import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; +import konkuk.chacall.global.common.annotation.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; + +@Controller +@RequiredArgsConstructor +public class ChatController { + + private final ChatService chatService; + private final SimpMessagingTemplate messagingTemplate; + + @MessageMapping("/rooms/{roomId}") // /pub/rooms/{roomId} + public void sendMessage( + @DestinationVariable Long roomId, + @Parameter(hidden = true) @UserId final Long userId, + SendChatMessageRequest request + ){ + ChatMessageResponse chatMessageResponse = chatService.sendMessage(roomId, userId, request); + messagingTemplate.convertAndSend("/sub/rooms/" + roomId, chatMessageResponse); + } + + +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatMessageResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatMessageResponse.java new file mode 100644 index 00000000..ba852bcf --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatMessageResponse.java @@ -0,0 +1,25 @@ +package konkuk.chacall.domain.chat.presentation.dto.response; + +import konkuk.chacall.domain.chat.domain.ChatMessage; + +import java.time.LocalDateTime; + +public record ChatMessageResponse( + Long roomId, + Long senderId, + String content, + String contentType, + LocalDateTime sendTime, + boolean read +) { + public static ChatMessageResponse from(ChatMessage chatMessage) { + return new ChatMessageResponse( + chatMessage.getRoomId(), + chatMessage.getSenderId(), + chatMessage.getContent(), + chatMessage.getContentType(), + chatMessage.getSendTime(), + chatMessage.isRead() + ); + } +} diff --git a/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java index 5144efb0..856ea493 100644 --- a/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java +++ b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java @@ -1,9 +1,13 @@ package konkuk.chacall.global.config; +import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +@Configuration +@EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { @@ -14,7 +18,7 @@ public void registerStompEndpoints(StompEndpointRegistry registry) { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { - registry.setApplicationDestinationPrefixes("/app"); // 발신 prefix - registry.enableSimpleBroker("/topic"); // 수신 prefix + registry.setApplicationDestinationPrefixes("/pub"); // 발신 prefix + registry.enableSimpleBroker("/sub"); // 수신 prefix } } From 3276efc616383d45798f4494bfead082de474248 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 15:59:43 +0900 Subject: [PATCH 11/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=EC=97=90=20=EC=98=88=EC=95=BD=20=EC=97=B0=EA=B4=80=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=A3=BC=EC=9E=85=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chacall/domain/chat/domain/ChatRoom.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index eac5b00e..f14c5b07 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -2,7 +2,10 @@ import jakarta.persistence.*; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; +import konkuk.chacall.domain.reservation.domain.model.Reservation; import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.exception.DomainRuleException; +import konkuk.chacall.global.common.exception.code.ErrorCode; import lombok.*; @Entity @@ -26,11 +29,22 @@ public class ChatRoom { @JoinColumn(name = "food_truck_id", nullable = false) private FoodTruck foodTruck; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reservation_id") + private Reservation reservation; + public static ChatRoom createChatRoom(User member, FoodTruck foodTruck) { return ChatRoom.builder() .member(member) .foodTruck(foodTruck) .build(); } + + public void validateParticipant(User user) { + if(!this.member.getUserId().equals(user.getUserId()) && + !this.foodTruck.getOwner().getUserId().equals(user.getUserId())) { + throw new DomainRuleException(ErrorCode.CHAT_ROOM_FORBIDDEN); + } + } } From 8199551c452881b89c59ad78819aeb9e4b4309b6 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 15:59:59 +0900 Subject: [PATCH 12/33] =?UTF-8?q?[feat]=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chacall/global/common/exception/code/ErrorCode.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java index b1f180c9..9be85ed1 100644 --- a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java @@ -119,7 +119,9 @@ public enum ErrorCode implements ResponseCode { * ChatRoom */ CHAT_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, 160001, "채팅방을 찾을 수 없습니다."), - CHAT_ROOM_ALREADY_EXISTS(HttpStatus.CONFLICT, 160002, "이미 존재하는 채팅방입니다.") + CHAT_ROOM_ALREADY_EXISTS(HttpStatus.CONFLICT, 160002, "이미 존재하는 채팅방입니다."), + CHAT_ROOM_FORBIDDEN(HttpStatus.FORBIDDEN, 160003, "채팅방 접근 권한이 없습니다."), + CHAT_ROOM_FILTER_MISMATCH(HttpStatus.BAD_REQUEST, 160004, "채팅방 필터 값이 올바르지 않습니다."), ; private final HttpStatus httpStatus; From 9c9ca485e31761c55e7d7976cf45edb9fd33f136 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 16:00:09 +0900 Subject: [PATCH 13/33] =?UTF-8?q?[feat]=20=EC=B1=84=ED=8C=85=20api=20dto?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/SendChatMessageRequest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java new file mode 100644 index 00000000..7d0c4521 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java @@ -0,0 +1,12 @@ +package konkuk.chacall.domain.chat.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import konkuk.chacall.domain.chat.domain.value.MessageContentType; + +public record SendChatMessageRequest( + @Schema(description = "메시지 내용", example = "안녕하세요!") + String content, + @Schema(description = "메시지 타입 (TEXT or IMAGE)", example = "TEXT") + MessageContentType contentType +) { +} From 3cf18263aecb376fee8c3ddba6fc3fec966be8c3 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 16:00:35 +0900 Subject: [PATCH 14/33] =?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=20dto=20=EB=B0=8F=20?= =?UTF-8?q?=ED=95=B8=EB=93=A4=EB=9F=AC=20=EA=B5=AC=EC=84=B1=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 21 ++++++++++ .../application/room/ChatRoomService.java | 9 +++++ .../domain/repository/ChatRoomRepository.java | 4 ++ .../chat/presentation/ChatRestController.java | 40 +++++++++++++++++++ .../dto/request/ChatRoomFilter.java | 29 ++++++++++++++ .../dto/request/GetChatRoomRequest.java | 21 ++++++++++ .../dto/response/ChatRoomResponse.java | 12 ++++++ 7 files changed, 136 insertions(+) create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index d149a1b1..81a13ace 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -1,17 +1,25 @@ package konkuk.chacall.domain.chat.application; +import jakarta.validation.Valid; import konkuk.chacall.domain.chat.application.message.ChatMessageService; import konkuk.chacall.domain.chat.application.room.ChatRoomService; +import konkuk.chacall.domain.chat.presentation.dto.request.GetChatRoomRequest; import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.domain.member.application.validator.MemberValidator; import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.dto.CursorPagingRequest; +import konkuk.chacall.global.common.dto.CursorPagingResponse; +import konkuk.chacall.global.common.dto.SortType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -40,4 +48,17 @@ public ChatMessageResponse sendMessage(Long roomId, Long userId, SendChatMessage return chatMessageService.sendMessage(roomId, senderUser, request); } + + public List getChatMessages(Long memberId, Long roomId, int page, int size) { + User user = memberValidator.validateAndGetMember(memberId); + + return chatMessageService.getChatMessages(roomId, user, page, size); + } + + public CursorPagingResponse getChatRooms(Long memberId, GetChatRoomRequest request) { + User member = memberValidator.validateAndGetMember(memberId); + + CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST); + return chatRoomService.getChatRooms(member, request.filter(), request.isOwner(), cursorPagingRequest.cursor(), cursorPagingRequest.size()); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index 7d88bf7e..eedcd5b4 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -1,13 +1,18 @@ package konkuk.chacall.domain.chat.application.room; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.presentation.dto.request.ChatRoomFilter; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; import konkuk.chacall.domain.user.domain.model.Role; import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.dto.CursorPagingResponse; import konkuk.chacall.global.common.exception.BusinessException; import konkuk.chacall.global.common.exception.EntityNotFoundException; import lombok.RequiredArgsConstructor; @@ -53,4 +58,8 @@ public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean FoodTruck foodTruck = chatRoom.getFoodTruck(); return ChatOpponentResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName()); } + + public CursorPagingResponse getChatRooms(User member, ChatRoomFilter filter, Boolean owner, Long cursor, Integer size) { + + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java index 1c943f13..b15a9d3f 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java @@ -5,7 +5,11 @@ import konkuk.chacall.domain.user.domain.model.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface ChatRoomRepository extends JpaRepository { boolean existsByMemberAndFoodTruck(User member, FoodTruck foodTruck); + + Optional findByMemberAndFoodTruck(User member, FoodTruck foodTruck); } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index a4361219..421e4f81 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -4,16 +4,24 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import konkuk.chacall.domain.chat.application.ChatService; import konkuk.chacall.domain.chat.presentation.dto.request.CreateChatRoomRequest; +import konkuk.chacall.domain.chat.presentation.dto.request.GetChatRoomRequest; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.global.common.annotation.ExceptionDescription; import konkuk.chacall.global.common.annotation.UserId; import konkuk.chacall.global.common.dto.BaseResponse; +import konkuk.chacall.global.common.dto.CursorPagingResponse; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static konkuk.chacall.global.common.swagger.SwaggerResponseDescription.*; @Tag(name = "Chat API", description = "채팅 관련 API") @@ -56,5 +64,37 @@ public BaseResponse getChatOpponentName( ); } + @Operation( + summary = "메시지 내역 조회", + description = "특정 채팅방의 메시지 내역을 조회합니다." + ) + @GetMapping("/rooms/{roomId}/messages") + public BaseResponse> getChatMessages( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId, + @Parameter(description = "페이지 번호", example = "0") @RequestParam @NotNull(message = "페이지 번호는 필수입니다.") + final Integer page, + @Parameter(description = "페이지 크기", example = "20") @RequestParam @NotNull(message = "페이지 크기는 필수입니다.") + final Integer size + ) { + return BaseResponse.ok( + chatService.getChatMessages(userId, roomId, page, size) + ); + } + + @Operation( + summary = "채팅방 목록 조회", + description = "사용자가 속한 채팅방 목록을 조회합니다." + ) + @GetMapping("/rooms") + public BaseResponse> getChatRooms( + @Parameter(hidden = true) @UserId final Long userId, + @Valid @ParameterObject final GetChatRoomRequest request + ) { + return BaseResponse.ok( + chatService.getChatRooms(userId, request) + ); + } + } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java new file mode 100644 index 00000000..b113cfd9 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java @@ -0,0 +1,29 @@ +package konkuk.chacall.domain.chat.presentation.dto.request; + +import com.fasterxml.jackson.annotation.JsonCreator; +import konkuk.chacall.global.common.exception.BusinessException; +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ChatRoomFilter { + ALL("전체"), + UNREAD("안 읽음"), + CONFIRMED("예약 확정") + ; + + private final String value; + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static ChatRoomFilter from(String value) { + for (ChatRoomFilter filter : ChatRoomFilter.values()) { + if (filter.getValue().equals(value)) { + return filter; + } + } + throw new BusinessException(ErrorCode.CHAT_ROOM_FILTER_MISMATCH, + new IllegalArgumentException("현재 지원하지 않는 채팅방 필터 값입니다: " + value)); + } +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java new file mode 100644 index 00000000..269e265e --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java @@ -0,0 +1,21 @@ +package konkuk.chacall.domain.chat.presentation.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import konkuk.chacall.global.common.dto.CursorPagingRequest; +import konkuk.chacall.global.common.dto.HasPaging; + +public record GetChatRoomRequest( + @Schema(description = "채팅방 필터", example = "전체", allowableValues = {"전체", "안 읽음", "예약 확정"}) + @NotNull(message = "채팅방 필터는 null 일 수 없습니다.") + ChatRoomFilter filter, + + @Schema(description = "현재 채팅방 기준 푸드트럭 사장인지 여부", example = "false") + @NotNull(message = "isOwner 는 null 일 수 없습니다.") + Boolean isOwner, + + @Valid + CursorPagingRequest cursorPagingRequest +) implements HasPaging +{ } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java new file mode 100644 index 00000000..e70ceff8 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java @@ -0,0 +1,12 @@ +package konkuk.chacall.domain.chat.presentation.dto.response; + +public record ChatRoomResponse( + Long id, + String name, + String foodTruckName, + String profileImageUrl, + String lastMessage, + String lastMessageSendTime, + long unreadCount +) { +} From 4f989f490e1cbdc22fe1f4a2ff53fa73287cd240 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 10 Nov 2025 16:06:50 +0900 Subject: [PATCH 15/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=EC=9D=B4=20=EC=9D=B4=EB=AF=B8=20=EA=B0=99=EC=9D=80=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=EC=99=80=20=ED=91=B8=EB=93=9C=ED=8A=B8=EB=9F=AD=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EC=97=90=20=EC=A1=B4=EC=9E=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4=EB=A9=B4=20=EC=83=88=EB=A1=9C=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=B1=84=ED=8C=85=EB=B0=A9=20id=20=EB=B0=98=ED=99=98=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/application/room/ChatRoomService.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index eedcd5b4..11a653d4 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -18,6 +18,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Optional; + import static konkuk.chacall.global.common.exception.code.ErrorCode.*; @Service @@ -32,15 +34,10 @@ public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { .orElseThrow(() -> new EntityNotFoundException(FOOD_TRUCK_NOT_FOUND)); // 채팅방이 이미 존재하는지 확인 - if(chatRoomRepository.existsByMemberAndFoodTruck(member, foodTruck)) { - throw new EntityNotFoundException(CHAT_ROOM_ALREADY_EXISTS); - } - - ChatRoom chatRoom = ChatRoom.createChatRoom(member, foodTruck); + ChatRoom chatRoom = chatRoomRepository.findByMemberAndFoodTruck(member, foodTruck) + .orElseGet(() -> chatRoomRepository.save(ChatRoom.createChatRoom(member, foodTruck))); - return ChatRoomIdResponse.of( - chatRoomRepository.save(chatRoom) - ); + return ChatRoomIdResponse.of(chatRoom); } public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean isOwner) { From b1077d6a4c8ceaa7ee81142befdf08bd2117b344 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:38:51 +0900 Subject: [PATCH 16/33] =?UTF-8?q?[feat]=20LocalDateTime=EC=9D=84=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=EB=A1=9C=20=EB=B3=80=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chacall/global/common/util/DateUtil.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/common/util/DateUtil.java diff --git a/src/main/java/konkuk/chacall/global/common/util/DateUtil.java b/src/main/java/konkuk/chacall/global/common/util/DateUtil.java new file mode 100644 index 00000000..454386ce --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/util/DateUtil.java @@ -0,0 +1,32 @@ +package konkuk.chacall.global.common.util; + +import java.time.Duration; +import java.time.LocalDateTime; + +public class DateUtil { + // LocalDateTime을 "오후 hh:mm" 형식의 문자열로 변환하는 메서드 (하루가 지난 경우 "어제" 이틀 이상이 지난 경우 "MM월 dd일" 1년 이상이 지난 경우 "yyyy년 MM월" 형식) + public static String formatLocalDateTime(LocalDateTime dateTime) { + LocalDateTime now = LocalDateTime.now(); + Duration duration = Duration.between(dateTime, now); + + if (duration.toDays() < 1) { + // 오늘 + int hour = dateTime.getHour(); + String period = (hour >= 12) ? "오후" : "오전"; + hour = (hour > 12) ? hour - 12 : hour; + if (hour == 0) hour = 12; // 12시 처리 + int minute = dateTime.getMinute(); + return String.format("%s %d:%02d", period, hour, minute); + } else if (duration.toDays() < 2) { + // 어제 + return "어제"; + } else if (duration.toDays() < 365) { + // 올해 + return String.format("%d월 %d일", dateTime.getMonthValue(), dateTime.getDayOfMonth()); + } else { + // 1년 이상 + return String.format("%d년 %d월", dateTime.getYear(), dateTime.getMonthValue()); + } + } + +} From b83585e60c3197b7e216e8dbadf3c9b5cdd063ca Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:39:16 +0900 Subject: [PATCH 17/33] =?UTF-8?q?[feat]=20=EC=9B=B9=EC=86=8C=EC=BC=93?= =?UTF-8?q?=EC=9A=A9=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StompAuthChannelInterceptor.java | 46 +++++++++++++++++++ .../resolver/StompUserIdArgumentResolver.java | 46 +++++++++++++++++++ .../global/config/WebSocketConfig.java | 26 ++++++++++- 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/main/java/konkuk/chacall/global/common/security/interceptor/StompAuthChannelInterceptor.java create mode 100644 src/main/java/konkuk/chacall/global/common/security/resolver/StompUserIdArgumentResolver.java diff --git a/src/main/java/konkuk/chacall/global/common/security/interceptor/StompAuthChannelInterceptor.java b/src/main/java/konkuk/chacall/global/common/security/interceptor/StompAuthChannelInterceptor.java new file mode 100644 index 00000000..32ee5e39 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/security/interceptor/StompAuthChannelInterceptor.java @@ -0,0 +1,46 @@ +package konkuk.chacall.global.common.security.interceptor; + +import konkuk.chacall.global.common.exception.AuthException; +import konkuk.chacall.global.common.exception.code.ErrorCode; +import konkuk.chacall.global.common.security.oauth2.LoginUser; +import konkuk.chacall.global.common.security.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.stereotype.Component; + +import static konkuk.chacall.global.common.security.constant.AuthParameters.*; + +@Component +@RequiredArgsConstructor +public class StompAuthChannelInterceptor implements ChannelInterceptor { + + private final JwtUtil jwtUtil; + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = + MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + + // CONNECT 요청에 대해서 인증 처리 & JWT 토큰 검증 + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + String authorization = accessor.getFirstNativeHeader(JWT_HEADER_KEY.getValue()); + if (authorization == null || !authorization.startsWith(JWT_PREFIX.getValue())) { + throw new AuthException(ErrorCode.AUTH_TOKEN_NOT_FOUND); + } + + String token = authorization.split(" ")[1]; + LoginUser loginUser = jwtUtil.getLoginUser(token); + + // ArgumentsResolver를 위해 세션에 userId 저장 + accessor.getSessionAttributes() + .put(JWT_ACCESS_TOKEN_KEY.getValue(), loginUser.userId()); + } + + return message; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/security/resolver/StompUserIdArgumentResolver.java b/src/main/java/konkuk/chacall/global/common/security/resolver/StompUserIdArgumentResolver.java new file mode 100644 index 00000000..5f3254a3 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/security/resolver/StompUserIdArgumentResolver.java @@ -0,0 +1,46 @@ +package konkuk.chacall.global.common.security.resolver; + +import konkuk.chacall.global.common.annotation.UserId; +import konkuk.chacall.global.common.exception.AuthException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.MessageHeaderAccessor; +import org.springframework.stereotype.Component; + +import static konkuk.chacall.global.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; +import static konkuk.chacall.global.common.security.constant.AuthParameters.JWT_ACCESS_TOKEN_KEY; + +@Component +@RequiredArgsConstructor +public class StompUserIdArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserId.class) + && parameter.getParameterType().equals(Long.class); + } + + @Override + public Long resolveArgument(MethodParameter parameter, Message message) { + + StompHeaderAccessor accessor = + MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + + if (accessor == null || accessor.getSessionAttributes() == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + + Object userId = accessor.getSessionAttributes() + .get(JWT_ACCESS_TOKEN_KEY.getValue()); + + if (userId == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + + return (Long) userId; + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java index 856ea493..be24980b 100644 --- a/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java +++ b/src/main/java/konkuk/chacall/global/config/WebSocketConfig.java @@ -1,19 +1,41 @@ package konkuk.chacall.global.config; +import konkuk.chacall.global.common.security.interceptor.StompAuthChannelInterceptor; +import konkuk.chacall.global.common.security.resolver.StompUserIdArgumentResolver; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; +import java.util.List; + @Configuration @EnableWebSocketMessageBroker +@RequiredArgsConstructor public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompAuthChannelInterceptor stompAuthChannelInterceptor; + private final StompUserIdArgumentResolver stompUserIdArgumentResolver; + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompAuthChannelInterceptor); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(stompUserIdArgumentResolver); + } + @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") - .setAllowedOriginPatterns("*") - .withSockJS(); // SockJS 사용 시 + .setAllowedOriginPatterns("*"); +// .withSockJS(); // SockJS 사용 시 } @Override From b8d033893fdd9fa0518ac30c0f3954f4adfec86b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:39:36 +0900 Subject: [PATCH 18/33] =?UTF-8?q?[feat]=20ChatRoomMetaData=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chacall/domain/chat/domain/ChatRoom.java | 7 -- .../domain/chat/domain/ChatRoomMetaData.java | 79 +++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index f14c5b07..5756836f 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -39,12 +39,5 @@ public static ChatRoom createChatRoom(User member, FoodTruck foodTruck) { .foodTruck(foodTruck) .build(); } - - public void validateParticipant(User user) { - if(!this.member.getUserId().equals(user.getUserId()) && - !this.foodTruck.getOwner().getUserId().equals(user.getUserId())) { - throw new DomainRuleException(ErrorCode.CHAT_ROOM_FORBIDDEN); - } - } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java new file mode 100644 index 00000000..bf343432 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java @@ -0,0 +1,79 @@ +package konkuk.chacall.domain.chat.domain; + +import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.exception.DomainRuleException; +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.index.CompoundIndexes; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.time.ZoneId; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Document(collection = "chat_room_metadata") +@CompoundIndexes({ + // 예약자 기준 목록 조회를 위한 인덱스 + @CompoundIndex( + name = "member_sort_idx", + def = "{ 'memberId': 1, 'sortKey': -1 }" + ), + // 사장 기준 목록 조회을 위한 인덱스 + @CompoundIndex( + name = "owner_sort_idx", + def = "{ 'ownerId': 1, 'sortKey': -1 }" + ) +}) +public class ChatRoomMetaData { + + @Id + private String id; + + // RDB ChatRoom 식별자 + private Long roomId; + + // 참여자 정보 (예약자 / 사장) + private Long memberId; + private Long ownerId; + + // 마지막 메시지 정보 + private String lastMessage; + private LocalDateTime lastMessageSendTime; + + /** + * 정렬 및 커서용 키 + * - lastMessageSendTime 을 epoch milli 로 변환한 값 + */ + private Long sortKey; + + public static ChatRoomMetaData from(ChatRoom chatRoom) { + return ChatRoomMetaData.builder() + .roomId(chatRoom.getChatRoomId()) + .memberId(chatRoom.getMember().getUserId()) + .ownerId(chatRoom.getFoodTruck().getOwner().getUserId()) + .lastMessage(null) + .lastMessageSendTime(null) + .sortKey(Long.MAX_VALUE) // 메시지 없는 방은 가장 뒤로 밀리도록 초기값 + .build(); + } + + public void updateLastMessage(String content, LocalDateTime sendTime) { + this.lastMessage = content; + this.lastMessageSendTime = sendTime; + if (sendTime != null) { + this.sortKey = sendTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + } + + public void validateParticipant(User user) { + if(!this.memberId.equals(user.getUserId()) && + !this.ownerId.equals(user.getUserId())) { + throw new DomainRuleException(ErrorCode.CHAT_ROOM_FORBIDDEN); + } + } +} \ No newline at end of file From 79081a12672ec195f91a507da44373e237f632c2 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:40:40 +0900 Subject: [PATCH 19/33] =?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=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/room/ChatRoomService.java | 56 +++++++++++++++++-- .../ChatRoomMetaDataRepository.java | 54 ++++++++++++++++++ .../domain/repository/ChatRoomRepository.java | 8 ++- .../dto/ChatRoomMetaDataProjection.java | 17 ++++++ .../chat/presentation/ChatController.java | 4 +- .../dto/response/ChatRoomResponse.java | 40 +++++++++++++ 6 files changed, 171 insertions(+), 8 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomMetaDataRepository.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/dto/ChatRoomMetaDataProjection.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index 11a653d4..a4373c0f 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -1,9 +1,10 @@ package konkuk.chacall.domain.chat.application.room; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.ChatRoomMetaData; +import konkuk.chacall.domain.chat.domain.repository.ChatRoomMetaDataRepository; import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; import konkuk.chacall.domain.chat.presentation.dto.request.ChatRoomFilter; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; @@ -18,7 +19,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import java.util.Optional; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import static konkuk.chacall.global.common.exception.code.ErrorCode.*; @@ -28,6 +32,7 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; private final FoodTruckRepository foodTruckRepository; + private final ChatRoomMetaDataRepository chatRoomMetaDataRepository; public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId) @@ -37,6 +42,14 @@ public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { ChatRoom chatRoom = chatRoomRepository.findByMemberAndFoodTruck(member, foodTruck) .orElseGet(() -> chatRoomRepository.save(ChatRoom.createChatRoom(member, foodTruck))); + // MongoDB 메타데이터 존재 여부 확인 + chatRoomMetaDataRepository.findByRoomId(chatRoom.getChatRoomId()) + .orElseGet(() -> { + // 없을 경우 새로 생성 + ChatRoomMetaData metaData = ChatRoomMetaData.from(chatRoom); + return chatRoomMetaDataRepository.save(metaData); + }); + return ChatRoomIdResponse.of(chatRoom); } @@ -56,7 +69,42 @@ public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean return ChatOpponentResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName()); } - public CursorPagingResponse getChatRooms(User member, ChatRoomFilter filter, Boolean owner, Long cursor, Integer size) { + public CursorPagingResponse getChatRooms(User member, ChatRoomFilter filter, Boolean isOwner, Long cursor, Integer size) { + int pageSize = (size == null || size < 1) ? 20 : size; + + Long cursorSortKey = (cursor != null) ? cursor : Long.MAX_VALUE; + int limit = pageSize + 1; + + // 1. MongoDB에서 메타데이터 + unreadCount 조회 + List metaList = + chatRoomMetaDataRepository.findChatRoomsForUser(member.getUserId(), isOwner, cursorSortKey, limit); + + boolean hasNext = metaList.size() > pageSize; + if (hasNext) { + metaList = metaList.subList(0, pageSize); + } + + // 2. roomId 리스트로 RDB에서 ChatRoom 배치 조회 + List roomIds = metaList.stream() + .map(ChatRoomMetaDataProjection::getRoomId) + .toList(); + + List chatRooms = chatRoomRepository.findByChatRoomIdIn(roomIds); + Map chatRoomMap = chatRooms.stream() + .collect(Collectors.toMap(ChatRoom::getChatRoomId, Function.identity())); + + // 3. 메타데이터 + RDB 정보 조합해서 ChatRoomResponse 생성 + List responses = metaList.stream() + .map(meta -> ChatRoomResponse.from(chatRoomMap.get(meta.getRoomId()), meta, isOwner)) + .toList(); + // 4. CursorPagingResponse 생성 + Long lastCursor = responses.isEmpty() ? null : metaList.get(metaList.size() - 1).getSortKey(); + return new CursorPagingResponse<>( + responses, + lastCursor, + hasNext, + null + ); } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomMetaDataRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomMetaDataRepository.java new file mode 100644 index 00000000..28319d31 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomMetaDataRepository.java @@ -0,0 +1,54 @@ +package konkuk.chacall.domain.chat.domain.repository; + +import konkuk.chacall.domain.chat.domain.ChatRoomMetaData; +import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.List; +import java.util.Optional; + +public interface ChatRoomMetaDataRepository extends MongoRepository { + Optional findByRoomId(Long roomId); + + /** + * - 자신의 채팅방만 필터링 + * - 최근 메시지 시간 기준 내림차순 정렬 + * - 커서(sortKey) 기반 페이지네이션 + * - 안읽은 메시지 개수까지 함께 조회 + */ + @Aggregation(pipeline = { + // 1. 내가 속한 채팅방만 필터링 (isOwner 기준) + "{ '$match': { '$expr': { '$and': [" + + " { '$cond': [ ?1, { '$eq': ['$ownerId', ?0] }, { '$eq': ['$memberId', ?0] } ] }," + + " { '$lt': ['$sortKey', ?2] }" + + "] } } }", + + // 2. 안읽은 메시지 개수 계산 (chat_messages 컬렉션 join) + "{ '$lookup': { " + + " 'from': 'chat_messages'," + + " 'let': { 'roomId': '$roomId' }," + + " 'pipeline': [" + + " { '$match': { '$expr': { '$and': [" + + " { '$eq': ['$roomId', '$$roomId'] }," + + " { '$eq': ['$read', false] }," + + " { '$ne': ['$senderId', ?0] }" + + " ] } } }," + + " { '$count': 'unreadCount' }" + + " ]," + + " 'as': 'unreadInfo'" + + "} }", + + // 3. unreadInfo 배열 -> unreadCount 필드로 변환 (없으면 0) + "{ '$addFields': { " + + " 'unreadCount': { '$ifNull': [ { '$arrayElemAt': ['$unreadInfo.unreadCount', 0] }, 0 ] }" + + "} }", + + // 4. sortKey 기준 내림차순 정렬 (가장 최근 대화가 위로) + "{ '$sort': { 'sortKey': -1 } }", + + // 5. 페이지 사이즈 + 1 만큼만 가져와서 hasNext 판단 + "{ '$limit': ?3 }" + }) + List findChatRoomsForUser(Long userId, boolean isOwner, Long cursorSortKey, int limit); +} diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java index b15a9d3f..9290b9c7 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatRoomRepository.java @@ -3,13 +3,17 @@ import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.user.domain.model.User; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface ChatRoomRepository extends JpaRepository { - boolean existsByMemberAndFoodTruck(User member, FoodTruck foodTruck); - Optional findByMemberAndFoodTruck(User member, FoodTruck foodTruck); + + @EntityGraph(attributePaths = {"member", "foodTruck"}) + List findByChatRoomIdIn(List roomIds); } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/dto/ChatRoomMetaDataProjection.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/dto/ChatRoomMetaDataProjection.java new file mode 100644 index 00000000..e2b08233 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/dto/ChatRoomMetaDataProjection.java @@ -0,0 +1,17 @@ +package konkuk.chacall.domain.chat.domain.repository.dto; + +import java.time.LocalDateTime; + +public interface ChatRoomMetaDataProjection { + Long getRoomId(); + + Long getMemberId(); + Long getOwnerId(); + + String getLastMessage(); + LocalDateTime getLastMessageSendTime(); + + Long getSortKey(); + + Long getUnreadCount(); +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java index 5283010a..ad0e3158 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatController.java @@ -6,11 +6,13 @@ import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; import konkuk.chacall.global.common.annotation.UserId; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; +@Slf4j @Controller @RequiredArgsConstructor public class ChatController { @@ -27,6 +29,4 @@ public void sendMessage( ChatMessageResponse chatMessageResponse = chatService.sendMessage(roomId, userId, request); messagingTemplate.convertAndSend("/sub/rooms/" + roomId, chatMessageResponse); } - - } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java index e70ceff8..7be9579b 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java @@ -1,12 +1,52 @@ package konkuk.chacall.domain.chat.presentation.dto.response; +import io.swagger.v3.oas.annotations.media.Schema; +import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; +import konkuk.chacall.domain.user.domain.model.User; +import konkuk.chacall.global.common.util.DateUtil; + +import java.util.Optional; + public record ChatRoomResponse( + @Schema(description = "채팅방 ID", example = "1") Long id, + @Schema(description = "상대방 이름", example = "홍길동") String name, + @Schema(description = "푸드트럭 이름", example = "맛있는 푸드트럭") String foodTruckName, + @Schema(description = "상대방 프로필 이미지 URL", example = "https://example.com/profile.jpg") String profileImageUrl, + @Schema(description = "마지막 메시지 내용", example = "안녕하세요!") String lastMessage, + @Schema(description = "마지막 메시지 전송 시간", example = "오후 5:49 or 어제 or 9월 30일 or 2023년 10월") String lastMessageSendTime, long unreadCount ) { + + public static ChatRoomResponse from(ChatRoom chatRoom, ChatRoomMetaDataProjection meta, boolean isOwner) { + // 현재 뷰 기준 상대방 정보 + User oppenent = isOwner ? chatRoom.getMember() : chatRoom.getFoodTruck().getOwner(); + String name = oppenent.getName(); + + String foodTruckName = chatRoom.getFoodTruck().getFoodTruckInfo().getName(); + String profileImageUrl = oppenent.getProfileImageUrl(); + + String lastMessage = meta.getLastMessage(); + String lastMessageSendTime = (meta.getLastMessageSendTime() != null) + ? DateUtil.formatLocalDateTime(meta.getLastMessageSendTime()) + : null; + + long unreadCount = Optional.ofNullable(meta.getUnreadCount()).orElse(0L); + + return new ChatRoomResponse( + chatRoom.getChatRoomId(), + name, + foodTruckName, + profileImageUrl, + lastMessage, + lastMessageSendTime, + unreadCount + ); + } } From 0a92563368862bc64f4da271bb39fcfa41bff4b2 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:40:54 +0900 Subject: [PATCH 20/33] =?UTF-8?q?[feat]=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=82=B4=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20api=20=EA=B5=AC=ED=98=84=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 9 ++++- .../message/ChatMessageService.java | 34 ++++++++++++------- .../repository/ChatMessageRepository.java | 33 +++++++++++++++++- .../infra/ChatMessageCustomRepository.java | 5 +++ .../ChatMessageCustomRepositoryImpl.java | 29 ++++++++++++++++ .../chat/presentation/ChatRestController.java | 12 ++++++- 6 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepository.java create mode 100644 src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 81a13ace..7f7a9660 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -1,6 +1,5 @@ package konkuk.chacall.domain.chat.application; -import jakarta.validation.Valid; import konkuk.chacall.domain.chat.application.message.ChatMessageService; import konkuk.chacall.domain.chat.application.room.ChatRoomService; import konkuk.chacall.domain.chat.presentation.dto.request.GetChatRoomRequest; @@ -43,6 +42,7 @@ public ChatOpponentResponse getChatOpponentName(Long memberId, Long roomId, bool return chatRoomService.getChatOpponentName(user, roomId, isOwner); } + @Transactional public ChatMessageResponse sendMessage(Long roomId, Long userId, SendChatMessageRequest request) { User senderUser = memberValidator.validateAndGetMember(userId); @@ -61,4 +61,11 @@ public CursorPagingResponse getChatRooms(Long memberId, GetCha CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST); return chatRoomService.getChatRooms(member, request.filter(), request.isOwner(), cursorPagingRequest.cursor(), cursorPagingRequest.size()); } + + @Transactional + public void markMessagesAsRead(Long userId, Long roomId) { + User user = memberValidator.validateAndGetMember(userId); + + chatMessageService.markMessagesAsRead(user, roomId); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java b/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java index 4b4e0a3a..34881075 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/message/ChatMessageService.java @@ -1,9 +1,9 @@ package konkuk.chacall.domain.chat.application.message; import konkuk.chacall.domain.chat.domain.ChatMessage; -import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.ChatRoomMetaData; import konkuk.chacall.domain.chat.domain.repository.ChatMessageRepository; -import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; +import konkuk.chacall.domain.chat.domain.repository.ChatRoomMetaDataRepository; import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; import konkuk.chacall.domain.user.domain.model.User; @@ -22,39 +22,49 @@ public class ChatMessageService { private final ChatMessageRepository chatMessageRepository; - private final ChatRoomRepository chatRoomRepository; + private final ChatRoomMetaDataRepository chatRoomMetaDataRepository; public ChatMessageResponse sendMessage(Long roomId, User senderUser, SendChatMessageRequest request) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) + ChatRoomMetaData chatRoomMetaData = chatRoomMetaDataRepository.findByRoomId(roomId) .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); - //todo 채팅방 참여자 검증 (쿼리가 최대 3개 호출되는데 추후 캐싱 고려) - chatRoom.validateParticipant(senderUser); + chatRoomMetaData.validateParticipant(senderUser); ChatMessage chatMessage = ChatMessage.createChatMessage( - chatRoom.getChatRoomId(), + chatRoomMetaData.getRoomId(), senderUser, request.content(), request.contentType() ); - chatMessageRepository.save(chatMessage); + ChatMessage savedMessage = chatMessageRepository.save(chatMessage); + chatRoomMetaData.updateLastMessage(savedMessage.getContent(), savedMessage.getSendTime()); + chatRoomMetaDataRepository.save(chatRoomMetaData); - return ChatMessageResponse.from(chatMessage); + return ChatMessageResponse.from(savedMessage); } public List getChatMessages(Long roomId, User user, int page, int size) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) + ChatRoomMetaData chatRoomMetaData = chatRoomMetaDataRepository.findByRoomId(roomId) .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); - //todo 채팅방 참여자 검증해야되는데.. 캐싱 고려해서 나중에 + chatRoomMetaData.validateParticipant(user); var pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "sendTime")); - return chatMessageRepository.findByRoomId(chatRoom.getChatRoomId(), pageable) + return chatMessageRepository.findByRoomId(chatRoomMetaData.getRoomId(), pageable) .stream() .map(ChatMessageResponse::from) .toList(); } + + public void markMessagesAsRead(User user, Long roomId) { + ChatRoomMetaData chatRoomMetaData = chatRoomMetaDataRepository.findByRoomId(roomId) + .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); + + chatRoomMetaData.validateParticipant(user); + + chatMessageRepository.markMessagesAsReadByUserInRoom(user.getUserId(), roomId); + } } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java index 718ac07b..6139bdad 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/ChatMessageRepository.java @@ -1,11 +1,42 @@ package konkuk.chacall.domain.chat.domain.repository; import konkuk.chacall.domain.chat.domain.ChatMessage; +import konkuk.chacall.domain.chat.domain.repository.infra.ChatMessageCustomRepository; +import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; import org.springframework.data.domain.PageRequest; +import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.MongoRepository; import java.util.List; -public interface ChatMessageRepository extends MongoRepository { +public interface ChatMessageRepository extends MongoRepository, ChatMessageCustomRepository { List findByRoomId(Long chatRoomId, PageRequest pageable); + + /** + * 채팅방 목록에 필요한 집계 데이터 + * - roomId + * - 마지막 메시지 내용 + * - 마지막 메시지 전송 시간 + * - 로그인 유저 기준 안 읽은 메시지 개수 + */ + @Aggregation(pipeline = { + "{ '$match': { 'roomId': { '$in': ?0 } } }", + "{ '$sort': { 'sendTime': -1 } }", + "{ '$group': { " + + " '_id': '$roomId'," + + " 'roomId': { '$first': '$roomId' }," + + " 'lastMessage': { '$first': '$content' }," + + " 'lastMessageSendTime': { '$first': '$sendTime' }," + + " 'unreadCount': { " + + " '$sum': { " + + " '$cond': [" + + " { '$and': [ { '$eq': ['$read', false] }, { '$ne': ['$senderId', ?1] } ] }," + + " 1," + + " 0" + + " ]" + + " }" + + " }" + + "} }" + }) + List aggregateChatRoomSummaries(List roomIds, Long userId); } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepository.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepository.java new file mode 100644 index 00000000..e66125ab --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepository.java @@ -0,0 +1,5 @@ +package konkuk.chacall.domain.chat.domain.repository.infra; + +public interface ChatMessageCustomRepository { + void markMessagesAsReadByUserInRoom(Long userId, Long roomId); +} diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java new file mode 100644 index 00000000..959b46f5 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java @@ -0,0 +1,29 @@ +package konkuk.chacall.domain.chat.domain.repository.infra; + +import konkuk.chacall.domain.chat.domain.ChatMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; + +@RequiredArgsConstructor +public class ChatMessageCustomRepositoryImpl implements ChatMessageCustomRepository { + private final MongoTemplate mongoTemplate; + + @Override + public void markMessagesAsReadByUserInRoom(Long userId, Long roomId) { + Query query = new Query(); + query.addCriteria(Criteria.where("roomId").is(roomId)); + // 내가 보낸 메시지는 읽음 처리 대상이 아님 + query.addCriteria(Criteria.where("senderId").ne(userId)); + // 아직 읽지 않은 메시지만 + query.addCriteria(Criteria.where("read").is(false)); + + Update update = new Update(); + update.set("read", true); + + // 조건에 맞는 모든 문서를 한 번에 업데이트 + mongoTemplate.updateMulti(query, update, ChatMessage.class); + } +} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index 421e4f81..3b01b421 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -96,5 +96,15 @@ public BaseResponse> getChatRooms( ); } - + @Operation( + summary = "채팅방 내 메시지 읽음 처리" + ) + @PatchMapping("/rooms/{roomId}/read") + public BaseResponse markMessagesAsRead( + @Parameter(hidden = true) @UserId final Long userId, + @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId + ) { + chatService.markMessagesAsRead(userId, roomId); + return BaseResponse.ok(null); + } } From 3c8ae4a37f6bbb25348e80bbc3b8155ca6603686 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 01:59:57 +0900 Subject: [PATCH 21/33] =?UTF-8?q?[feat]=20ChatRoomFilter=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 2 +- .../application/room/ChatRoomService.java | 3 +- .../dto/request/ChatRoomFilter.java | 29 ------------------- .../dto/request/GetChatRoomRequest.java | 4 --- 4 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 7f7a9660..913c01bd 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -59,7 +59,7 @@ public CursorPagingResponse getChatRooms(Long memberId, GetCha User member = memberValidator.validateAndGetMember(memberId); CursorPagingRequest cursorPagingRequest = request.pagingOrDefault(SortType.NEWEST); - return chatRoomService.getChatRooms(member, request.filter(), request.isOwner(), cursorPagingRequest.cursor(), cursorPagingRequest.size()); + return chatRoomService.getChatRooms(member, request.isOwner(), cursorPagingRequest.cursor(), cursorPagingRequest.size()); } @Transactional diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index a4373c0f..bcf38ff1 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -5,7 +5,6 @@ import konkuk.chacall.domain.chat.domain.repository.ChatRoomMetaDataRepository; import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; -import konkuk.chacall.domain.chat.presentation.dto.request.ChatRoomFilter; import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; @@ -69,7 +68,7 @@ public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean return ChatOpponentResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName()); } - public CursorPagingResponse getChatRooms(User member, ChatRoomFilter filter, Boolean isOwner, Long cursor, Integer size) { + public CursorPagingResponse getChatRooms(User member, Boolean isOwner, Long cursor, Integer size) { int pageSize = (size == null || size < 1) ? 20 : size; Long cursorSortKey = (cursor != null) ? cursor : Long.MAX_VALUE; diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java deleted file mode 100644 index b113cfd9..00000000 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/ChatRoomFilter.java +++ /dev/null @@ -1,29 +0,0 @@ -package konkuk.chacall.domain.chat.presentation.dto.request; - -import com.fasterxml.jackson.annotation.JsonCreator; -import konkuk.chacall.global.common.exception.BusinessException; -import konkuk.chacall.global.common.exception.code.ErrorCode; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum ChatRoomFilter { - ALL("전체"), - UNREAD("안 읽음"), - CONFIRMED("예약 확정") - ; - - private final String value; - - @JsonCreator(mode = JsonCreator.Mode.DELEGATING) - public static ChatRoomFilter from(String value) { - for (ChatRoomFilter filter : ChatRoomFilter.values()) { - if (filter.getValue().equals(value)) { - return filter; - } - } - throw new BusinessException(ErrorCode.CHAT_ROOM_FILTER_MISMATCH, - new IllegalArgumentException("현재 지원하지 않는 채팅방 필터 값입니다: " + value)); - } -} diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java index 269e265e..c8cf9c3d 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/GetChatRoomRequest.java @@ -7,10 +7,6 @@ import konkuk.chacall.global.common.dto.HasPaging; public record GetChatRoomRequest( - @Schema(description = "채팅방 필터", example = "전체", allowableValues = {"전체", "안 읽음", "예약 확정"}) - @NotNull(message = "채팅방 필터는 null 일 수 없습니다.") - ChatRoomFilter filter, - @Schema(description = "현재 채팅방 기준 푸드트럭 사장인지 여부", example = "false") @NotNull(message = "isOwner 는 null 일 수 없습니다.") Boolean isOwner, From 965f82b3cfa54599a3e914fbfe79b5953c08b33a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 02:26:34 +0900 Subject: [PATCH 22/33] =?UTF-8?q?[fix]=20Id=20import=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/chacall/domain/chat/domain/ChatMessage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java index 47551b2d..9aae6314 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java @@ -1,9 +1,9 @@ package konkuk.chacall.domain.chat.domain; -import jakarta.persistence.*; import konkuk.chacall.domain.chat.domain.value.MessageContentType; import konkuk.chacall.domain.user.domain.model.User; import lombok.*; +import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.mapping.Document; From 2abc49426a3f74516f72e4edccd9e4164b2ec86b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 02:32:46 +0900 Subject: [PATCH 23/33] =?UTF-8?q?[refactor]=20dto=20schema=20=EB=B0=8F=20v?= =?UTF-8?q?alidation=20=EC=B6=94=EA=B0=80=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/presentation/dto/request/SendChatMessageRequest.java | 4 ++++ .../chat/presentation/dto/response/ChatRoomResponse.java | 1 + 2 files changed, 5 insertions(+) diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java index 7d0c4521..6af00a2b 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/request/SendChatMessageRequest.java @@ -1,12 +1,16 @@ package konkuk.chacall.domain.chat.presentation.dto.request; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import konkuk.chacall.domain.chat.domain.value.MessageContentType; public record SendChatMessageRequest( @Schema(description = "메시지 내용", example = "안녕하세요!") + @NotBlank(message = "메시지 내용은 필수입니다.") String content, @Schema(description = "메시지 타입 (TEXT or IMAGE)", example = "TEXT") + @NotNull(message = "메시지 타입은 필수입니다.") MessageContentType contentType ) { } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java index 7be9579b..48c0b2ac 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java @@ -21,6 +21,7 @@ public record ChatRoomResponse( String lastMessage, @Schema(description = "마지막 메시지 전송 시간", example = "오후 5:49 or 어제 or 9월 30일 or 2023년 10월") String lastMessageSendTime, + @Schema(description = "읽지 않은 메시지 수", example = "3") long unreadCount ) { From c1557657a13076664bb639e18ceb386a7aad5298 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 11 Nov 2025 02:36:08 +0900 Subject: [PATCH 24/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=8B=9C=20sortKey=20=EC=9C=A0=EB=8B=88=ED=81=AC?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java index bf343432..956d0b8f 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoomMetaData.java @@ -58,7 +58,7 @@ public static ChatRoomMetaData from(ChatRoom chatRoom) { .ownerId(chatRoom.getFoodTruck().getOwner().getUserId()) .lastMessage(null) .lastMessageSendTime(null) - .sortKey(Long.MAX_VALUE) // 메시지 없는 방은 가장 뒤로 밀리도록 초기값 + .sortKey(Long.MAX_VALUE - chatRoom.getChatRoomId()) // 메시지 없는 방은 가장 뒤로 밀리도록 초기값 .build(); } From f580a074b3d2ddc539570dbf39454627d119b4fa Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 27 Nov 2025 00:28:12 +0900 Subject: [PATCH 25/33] =?UTF-8?q?[refactor]=20Reservation=EA=B3=BC=20ChatR?= =?UTF-8?q?oom=20=EC=97=B0=EA=B4=80=EA=B4=80=EA=B3=84=20=EB=A7=BA=EA=B8=B0?= =?UTF-8?q?=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/chacall/domain/chat/domain/ChatRoom.java | 4 ---- .../application/info/ReservationInfoService.java | 10 +++++++++- .../domain/reservation/domain/model/Reservation.java | 10 +++++++++- .../dto/request/CreateReservationRequest.java | 4 ++++ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index 5756836f..73f96f45 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -29,10 +29,6 @@ public class ChatRoom { @JoinColumn(name = "food_truck_id", nullable = false) private FoodTruck foodTruck; - @OneToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "reservation_id") - private Reservation reservation; - public static ChatRoom createChatRoom(User member, FoodTruck foodTruck) { return ChatRoom.builder() .member(member) diff --git a/src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java b/src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java index 886c327f..72eb0282 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java +++ b/src/main/java/konkuk/chacall/domain/reservation/application/info/ReservationInfoService.java @@ -1,5 +1,7 @@ package konkuk.chacall.domain.reservation.application.info; +import konkuk.chacall.domain.chat.domain.ChatRoom; +import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; import konkuk.chacall.domain.reservation.domain.model.Reservation; @@ -20,11 +22,15 @@ public class ReservationInfoService { private final FoodTruckRepository foodTruckRepository; private final ReservationRepository reservationRepository; + private final ChatRoomRepository chatRoomRepository; public Long createReservation(CreateReservationRequest request, User owner, User member) { FoodTruck foodTruck = foodTruckRepository.findById(request.foodTruckId()) .orElseThrow(() -> new EntityNotFoundException(ErrorCode.FOOD_TRUCK_NOT_FOUND)); + ChatRoom chatRoom = chatRoomRepository.findById(request.chatRoomId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CHAT_ROOM_NOT_FOUND)); + Reservation reservation = Reservation.create( request.address(), request.detailAddress(), @@ -36,7 +42,9 @@ public Long createReservation(CreateReservationRequest request, User owner, User request.etcRequest(), owner, member, - foodTruck); + foodTruck, + chatRoom + ); return reservationRepository.save(reservation).getReservationId(); } diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java b/src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java index 90f023b0..93272a82 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/model/Reservation.java @@ -1,6 +1,7 @@ package konkuk.chacall.domain.reservation.domain.model; import jakarta.persistence.*; +import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.reservation.domain.value.ReservationDateList; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.reservation.domain.value.ReservationInfo; @@ -47,6 +48,11 @@ public class Reservation extends BaseEntity { @JoinColumn(name = "food_truck_id", nullable = false) private FoodTruck foodTruck; + // 추후에 nullable = false 로 변경할 예정 + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "chat_room_id", unique = true) + private ChatRoom chatRoom; + // 해당 예약과 연관된 사람인지 검증 (사장님, 예약자) public void validateAccessibleBy(Long userId) { if (!isForFoodTruckOwnedBy(userId) && !isReservedBy(userId)) { @@ -99,7 +105,8 @@ public static Reservation create( String etcRequest, User owner, User member, - FoodTruck foodTruck + FoodTruck foodTruck, + ChatRoom chatRoom ) { validateCreateReservation(owner, member, foodTruck); @@ -121,6 +128,7 @@ public static Reservation create( .pdfUrl(null) .member(member) .foodTruck(foodTruck) + .chatRoom(chatRoom) .build(); } diff --git a/src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java b/src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java index d35ee4cc..e04801a4 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java +++ b/src/main/java/konkuk/chacall/domain/reservation/presentation/dto/request/CreateReservationRequest.java @@ -10,6 +10,10 @@ public record CreateReservationRequest( @NotNull(message = "푸드트럭 ID는 필수 입력 값입니다.") Long foodTruckId, + @Schema(description = "채팅방 ID", example = "1") + @NotNull(message = "채팅방 ID는 필수 입력 값입니다.") + Long chatRoomId, + @Schema(description = "예약자(일반 유저) ID", example = "2") @NotNull(message = "예약자 ID는 필수 입력 값입니다.") Long reservationUserId, From 687c97b19ea82700f303bc3513a1e5837b31a03f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 27 Nov 2025 00:41:40 +0900 Subject: [PATCH 26/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=83=81=EB=8C=80=20=EC=9D=B4=EB=A6=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?->=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EB=A9=94=ED=83=80=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/presentation/ChatRestController.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java index 3b01b421..26e30e93 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/ChatRestController.java @@ -9,7 +9,7 @@ import konkuk.chacall.domain.chat.presentation.dto.request.CreateChatRoomRequest; import konkuk.chacall.domain.chat.presentation.dto.request.GetChatRoomRequest; import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; -import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomMetaDataResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.global.common.annotation.ExceptionDescription; @@ -48,19 +48,19 @@ public BaseResponse createChatRoom( } @Operation( - summary = "채팅 상대 이름 조회", - description = "채팅 상단에 표시되는 채팅 상대의 이름을 조회합니다." + summary = "채팅방 메타데이터 조회", + description = "채팅 상단에 표시되는 채팅 상대의 이름과 관련된 예약 ID(있는 경우)를 조회합니다." ) - @ExceptionDescription(GET_CHAT_OPPONENT_NAME) + @ExceptionDescription(GET_CHAT_ROOM_META_DATA) @GetMapping("/rooms/{roomId}") - public BaseResponse getChatOpponentName( + public BaseResponse getChatRoomMetaData( @Parameter(hidden = true) @UserId final Long memberId, @Parameter(description = "채팅방 ID", example = "1") @PathVariable final Long roomId, @Parameter(description = "현재 채팅방 기준 푸드트럭 사장인지 여부", example = "false") @RequestParam final Boolean isOwner ) { return BaseResponse.ok( - chatService.getChatOpponentName(memberId, roomId, isOwner) + chatService.getChatRoomMetaData(memberId, roomId, isOwner) ); } From c4690d165e7f4df3428dee3a29674d1c230acf26 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 27 Nov 2025 00:42:21 +0900 Subject: [PATCH 27/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=EC=97=90=EC=84=9C=20=EA=B4=80=EB=A0=A8=EB=90=9C?= =?UTF-8?q?=20=EC=98=88=EC=95=BD=20ID=20=EC=A1=B0=ED=9A=8C=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/ChatService.java | 6 +++--- .../chat/application/room/ChatRoomService.java | 15 +++++++++++---- ...esponse.java => ChatRoomMetaDataResponse.java} | 10 ++++++---- .../domain/repository/ReservationRepository.java | 3 +++ .../swagger/SwaggerResponseDescription.java | 2 +- 5 files changed, 24 insertions(+), 12 deletions(-) rename src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/{ChatOpponentResponse.java => ChatRoomMetaDataResponse.java} (51%) diff --git a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java index 913c01bd..4c4ece38 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/ChatService.java @@ -5,7 +5,7 @@ import konkuk.chacall.domain.chat.presentation.dto.request.GetChatRoomRequest; import konkuk.chacall.domain.chat.presentation.dto.request.SendChatMessageRequest; import konkuk.chacall.domain.chat.presentation.dto.response.ChatMessageResponse; -import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomMetaDataResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.domain.member.application.validator.MemberValidator; @@ -36,10 +36,10 @@ public ChatRoomIdResponse createChatRoom(Long memberId, Long foodTruckId) { return chatRoomService.createChatRoom(member, foodTruckId); } - public ChatOpponentResponse getChatOpponentName(Long memberId, Long roomId, boolean isOwner) { + public ChatRoomMetaDataResponse getChatRoomMetaData(Long memberId, Long roomId, boolean isOwner) { User user = memberValidator.validateAndGetMember(memberId); - return chatRoomService.getChatOpponentName(user, roomId, isOwner); + return chatRoomService.getChatRoomMetaData(user, roomId, isOwner); } @Transactional diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index bcf38ff1..7ca5bddb 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -5,11 +5,13 @@ import konkuk.chacall.domain.chat.domain.repository.ChatRoomMetaDataRepository; import konkuk.chacall.domain.chat.domain.repository.ChatRoomRepository; import konkuk.chacall.domain.chat.domain.repository.dto.ChatRoomMetaDataProjection; -import konkuk.chacall.domain.chat.presentation.dto.response.ChatOpponentResponse; +import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomMetaDataResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomIdResponse; import konkuk.chacall.domain.chat.presentation.dto.response.ChatRoomResponse; import konkuk.chacall.domain.foodtruck.domain.model.FoodTruck; import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; +import konkuk.chacall.domain.reservation.domain.model.Reservation; +import konkuk.chacall.domain.reservation.domain.repository.ReservationRepository; import konkuk.chacall.domain.user.domain.model.Role; import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.dto.CursorPagingResponse; @@ -32,6 +34,7 @@ public class ChatRoomService { private final ChatRoomRepository chatRoomRepository; private final FoodTruckRepository foodTruckRepository; private final ChatRoomMetaDataRepository chatRoomMetaDataRepository; + private final ReservationRepository reservationRepository; public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { FoodTruck foodTruck = foodTruckRepository.findById(foodTruckId) @@ -52,20 +55,24 @@ public ChatRoomIdResponse createChatRoom(User member, Long foodTruckId) { return ChatRoomIdResponse.of(chatRoom); } - public ChatOpponentResponse getChatOpponentName(User user, Long roomId, boolean isOwner) { + public ChatRoomMetaDataResponse getChatRoomMetaData(User user, Long roomId, boolean isOwner) { ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> new EntityNotFoundException(CHAT_ROOM_NOT_FOUND)); + Long reservationId = reservationRepository.findByChatRoom(chatRoom) + .map(Reservation::getReservationId) + .orElse(null); // 없으면 null + if(isOwner && user.getRole() != Role.OWNER) { throw new BusinessException(USER_FORBIDDEN); } // 푸드트럭 사장일 경우 예약자 이름 반환 - if(isOwner) return ChatOpponentResponse.of(chatRoom.getMember().getName(), null); + if(isOwner) return ChatRoomMetaDataResponse.of(chatRoom.getMember().getName(), null, reservationId); // 예약자일 경우 푸드트럭 사장 이름 및 푸드트럭 이름 반환 FoodTruck foodTruck = chatRoom.getFoodTruck(); - return ChatOpponentResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName()); + return ChatRoomMetaDataResponse.of(foodTruck.getOwner().getName(), foodTruck.getFoodTruckInfo().getName(), reservationId); } public CursorPagingResponse getChatRooms(User member, Boolean isOwner, Long cursor, Integer size) { diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java similarity index 51% rename from src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java rename to src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java index a55ff0fd..67562ad8 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatOpponentResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java @@ -2,13 +2,15 @@ import io.swagger.v3.oas.annotations.media.Schema; -public record ChatOpponentResponse( +public record ChatRoomMetaDataResponse( @Schema(description = "채팅 상대 이름 (일반 유저 -> 사장님 이름 / 사장님 -> 예약자 이름)", example = "홍길동 or 푸드트럭사장") String name, @Schema(description = "푸드트럭 이름 (일반 유저 -> null)", example = "맛있는푸드트럭") - String foodTruckName + String foodTruckName, + @Schema(description = "채팅방과 관련된 예약 ID (있는 경우: ID 반환, 없는 경우: null", example = "1") + Long reservationId ) { - public static ChatOpponentResponse of(String name, String foodTruckName) { - return new ChatOpponentResponse(name, foodTruckName); + public static ChatRoomMetaDataResponse of(String name, String foodTruckName, Long reservationId) { + return new ChatRoomMetaDataResponse(name, foodTruckName, reservationId); } } diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java index 30fd3d7c..cd407237 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java @@ -1,5 +1,6 @@ package konkuk.chacall.domain.reservation.domain.repository; +import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.reservation.domain.model.Reservation; import konkuk.chacall.domain.reservation.domain.value.ReservationStatus; import org.springframework.data.domain.Pageable; @@ -48,4 +49,6 @@ Slice findMemberReservationsByStatusWithCursor( @Modifying @Query("DELETE FROM Reservation r WHERE r.foodTruck.foodTruckId = :foodTruckId") void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId); + + Optional findByChatRoom(ChatRoom chatRoom); } diff --git a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java index b023d43c..a74bbc6f 100644 --- a/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/chacall/global/common/swagger/SwaggerResponseDescription.java @@ -272,7 +272,7 @@ public enum SwaggerResponseDescription { FOOD_TRUCK_NOT_FOUND, CHAT_ROOM_ALREADY_EXISTS ))), - GET_CHAT_OPPONENT_NAME(new LinkedHashSet<>(Set.of( + GET_CHAT_ROOM_META_DATA(new LinkedHashSet<>(Set.of( USER_NOT_FOUND, USER_FORBIDDEN, CHAT_ROOM_NOT_FOUND From 9c2ed7b0110e61f0c5e4696c5989eabe4abb595a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Thu, 27 Nov 2025 00:58:30 +0900 Subject: [PATCH 28/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EB=82=B4=20=EC=9D=BD=EC=9D=8C=20=EC=B2=98=EB=A6=AC=20api=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChatMessageCustomRepositoryImpl.java | 38 +++++++++++++------ .../response/ChatRoomMetaDataResponse.java | 2 +- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java index 959b46f5..03f467f1 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/repository/infra/ChatMessageCustomRepositoryImpl.java @@ -2,6 +2,7 @@ import konkuk.chacall.domain.chat.domain.ChatMessage; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; @@ -13,17 +14,30 @@ public class ChatMessageCustomRepositoryImpl implements ChatMessageCustomReposit @Override public void markMessagesAsReadByUserInRoom(Long userId, Long roomId) { - Query query = new Query(); - query.addCriteria(Criteria.where("roomId").is(roomId)); - // 내가 보낸 메시지는 읽음 처리 대상이 아님 - query.addCriteria(Criteria.where("senderId").ne(userId)); - // 아직 읽지 않은 메시지만 - query.addCriteria(Criteria.where("read").is(false)); - - Update update = new Update(); - update.set("read", true); - - // 조건에 맞는 모든 문서를 한 번에 업데이트 - mongoTemplate.updateMulti(query, update, ChatMessage.class); + // 1) 가장 최근의 읽음 메시지 1개 찾기 + Query lastReadQuery = new Query(); + lastReadQuery.addCriteria(Criteria.where("roomId").is(roomId)); + lastReadQuery.addCriteria(Criteria.where("senderId").ne(userId)); + lastReadQuery.addCriteria(Criteria.where("read").is(true)); + lastReadQuery.with(Sort.by(Sort.Direction.DESC, "sendTime")); + lastReadQuery.limit(1); + + ChatMessage lastReadMessage = + mongoTemplate.findOne(lastReadQuery, ChatMessage.class); + + Update update = new Update().set("read", true); + + Query updateQuery = new Query(); + updateQuery.addCriteria(Criteria.where("roomId").is(roomId)); + updateQuery.addCriteria(Criteria.where("senderId").ne(userId)); + updateQuery.addCriteria(Criteria.where("read").is(false)); + + // 2) 마지막 읽음 메시지가 있다면 sendTime 조건 추가 + if (lastReadMessage != null) { + updateQuery.addCriteria(Criteria.where("sendTime").gt(lastReadMessage.getSendTime())); + } + + // 3) 일괄 업데이트 + mongoTemplate.updateMulti(updateQuery, update, ChatMessage.class); } } diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java index 67562ad8..3e5e4521 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomMetaDataResponse.java @@ -7,7 +7,7 @@ public record ChatRoomMetaDataResponse( String name, @Schema(description = "푸드트럭 이름 (일반 유저 -> null)", example = "맛있는푸드트럭") String foodTruckName, - @Schema(description = "채팅방과 관련된 예약 ID (있는 경우: ID 반환, 없는 경우: null", example = "1") + @Schema(description = "채팅방과 관련된 예약 ID (있는 경우: ID 반환, 없는 경우: null)", example = "1") Long reservationId ) { public static ChatRoomMetaDataResponse of(String name, String foodTruckName, Long reservationId) { From 4e7cf6586a1119f9d60e9ac35a8ccc5bab6e28b6 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 29 Nov 2025 15:37:07 +0900 Subject: [PATCH 29/33] =?UTF-8?q?[refactor]=20ChatMessage=20=EC=9D=B8?= =?UTF-8?q?=EB=8D=B1=EC=8A=A4=20=EA=B5=90=EC=B2=B4=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/chacall/domain/chat/domain/ChatMessage.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java index 9aae6314..6d55d355 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatMessage.java @@ -11,7 +11,10 @@ @Getter @Document(collection = "chat_messages") -@CompoundIndex(name = "room_time_idx", def = "{'roomId': 1, 'sendTime': 1}") +@CompoundIndex( + name = "room_sender_read_time_idx", + def = "{'roomId': 1, 'senderId': 1, 'read': 1, 'sendTime': 1}" +) @Builder @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) From 539bd6e5391320ba593f40c3dca19fbe10d534ec Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 29 Nov 2025 15:59:46 +0900 Subject: [PATCH 30/33] =?UTF-8?q?[refactor]=20=EC=B1=84=ED=8C=85=EB=B0=A9?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=EC=97=90=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=ED=99=95=EC=A0=95=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/chat/application/room/ChatRoomService.java | 10 +++++++--- .../presentation/dto/response/ChatRoomResponse.java | 9 ++++++--- .../domain/repository/ReservationRepository.java | 8 ++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index 7ca5bddb..7bf16991 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -99,12 +99,16 @@ public CursorPagingResponse getChatRooms(User member, Boolean Map chatRoomMap = chatRooms.stream() .collect(Collectors.toMap(ChatRoom::getChatRoomId, Function.identity())); - // 3. 메타데이터 + RDB 정보 조합해서 ChatRoomResponse 생성 + // 3. 예약 확정 여부 조회 + Map reservationConfirmedMap = reservationRepository.findReservationConfirmedByChatRoomIds(roomIds); + + // 4. 메타데이터 + RDB 정보 조합해서 ChatRoomResponse 생성 List responses = metaList.stream() - .map(meta -> ChatRoomResponse.from(chatRoomMap.get(meta.getRoomId()), meta, isOwner)) + .map(meta -> ChatRoomResponse.from(chatRoomMap.get(meta.getRoomId()), meta, isOwner, + reservationConfirmedMap.getOrDefault(meta.getRoomId(), false))) .toList(); - // 4. CursorPagingResponse 생성 + // 5. CursorPagingResponse 생성 Long lastCursor = responses.isEmpty() ? null : metaList.get(metaList.size() - 1).getSortKey(); return new CursorPagingResponse<>( responses, diff --git a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java index 48c0b2ac..4ac88f33 100644 --- a/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java +++ b/src/main/java/konkuk/chacall/domain/chat/presentation/dto/response/ChatRoomResponse.java @@ -22,10 +22,12 @@ public record ChatRoomResponse( @Schema(description = "마지막 메시지 전송 시간", example = "오후 5:49 or 어제 or 9월 30일 or 2023년 10월") String lastMessageSendTime, @Schema(description = "읽지 않은 메시지 수", example = "3") - long unreadCount + long unreadCount, + @Schema(description = "예약 확정 여부", example = "true") + boolean isReservationConfirmed ) { - public static ChatRoomResponse from(ChatRoom chatRoom, ChatRoomMetaDataProjection meta, boolean isOwner) { + public static ChatRoomResponse from(ChatRoom chatRoom, ChatRoomMetaDataProjection meta, boolean isOwner, boolean isReservationConfirmed) { // 현재 뷰 기준 상대방 정보 User oppenent = isOwner ? chatRoom.getMember() : chatRoom.getFoodTruck().getOwner(); String name = oppenent.getName(); @@ -47,7 +49,8 @@ public static ChatRoomResponse from(ChatRoom chatRoom, ChatRoomMetaDataProjectio profileImageUrl, lastMessage, lastMessageSendTime, - unreadCount + unreadCount, + isReservationConfirmed ); } } diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java index cd407237..0f910fa5 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java @@ -11,6 +11,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -51,4 +53,10 @@ Slice findMemberReservationsByStatusWithCursor( void deleteAllByFoodTruckId(@Param("foodTruckId") Long foodTruckId); Optional findByChatRoom(ChatRoom chatRoom); + + @Query("SELECT r.chatRoom.chatRoomId AS roomId, " + + "CASE WHEN r.reservationStatus = 'CONFIRMED' THEN true ELSE false END AS confirmed " + + "FROM Reservation r " + + "WHERE r.chatRoom.chatRoomId IN :roomIds") + Map findReservationConfirmedByChatRoomIds(List roomIds); } From 6ce9df5457b98f859978f1dd36b0d2cc1c53b56d Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 29 Nov 2025 16:23:44 +0900 Subject: [PATCH 31/33] =?UTF-8?q?[fix]=20=EC=A7=80=EC=9B=90=EC=95=88?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=8C=8C=EC=8B=B1=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(Map=20->=20Projection)=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/application/room/ChatRoomService.java | 15 ++++++++++++++- .../domain/repository/ReservationRepository.java | 3 ++- .../dto/ReservationConfirmedProjection.java | 8 ++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/main/java/konkuk/chacall/domain/reservation/domain/repository/dto/ReservationConfirmedProjection.java diff --git a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java index 7bf16991..485fca0e 100644 --- a/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java +++ b/src/main/java/konkuk/chacall/domain/chat/application/room/ChatRoomService.java @@ -12,6 +12,7 @@ import konkuk.chacall.domain.foodtruck.domain.repository.FoodTruckRepository; import konkuk.chacall.domain.reservation.domain.model.Reservation; import konkuk.chacall.domain.reservation.domain.repository.ReservationRepository; +import konkuk.chacall.domain.reservation.domain.repository.dto.ReservationConfirmedProjection; import konkuk.chacall.domain.user.domain.model.Role; import konkuk.chacall.domain.user.domain.model.User; import konkuk.chacall.global.common.dto.CursorPagingResponse; @@ -20,6 +21,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -100,7 +102,18 @@ public CursorPagingResponse getChatRooms(User member, Boolean .collect(Collectors.toMap(ChatRoom::getChatRoomId, Function.identity())); // 3. 예약 확정 여부 조회 - Map reservationConfirmedMap = reservationRepository.findReservationConfirmedByChatRoomIds(roomIds); + Map reservationConfirmedMap; + if (roomIds.isEmpty()) { + reservationConfirmedMap = Collections.emptyMap(); + } else { + List confirmedList = reservationRepository.findReservationConfirmedByChatRoomIds(roomIds); + + reservationConfirmedMap = confirmedList.stream() + .collect(Collectors.toMap( + ReservationConfirmedProjection::getRoomId, + ReservationConfirmedProjection::getConfirmed + )); + } // 4. 메타데이터 + RDB 정보 조합해서 ChatRoomResponse 생성 List responses = metaList.stream() diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java index 0f910fa5..eb070051 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java @@ -2,6 +2,7 @@ import konkuk.chacall.domain.chat.domain.ChatRoom; import konkuk.chacall.domain.reservation.domain.model.Reservation; +import konkuk.chacall.domain.reservation.domain.repository.dto.ReservationConfirmedProjection; import konkuk.chacall.domain.reservation.domain.value.ReservationStatus; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -58,5 +59,5 @@ Slice findMemberReservationsByStatusWithCursor( "CASE WHEN r.reservationStatus = 'CONFIRMED' THEN true ELSE false END AS confirmed " + "FROM Reservation r " + "WHERE r.chatRoom.chatRoomId IN :roomIds") - Map findReservationConfirmedByChatRoomIds(List roomIds); + List findReservationConfirmedByChatRoomIds(List roomIds); } diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/dto/ReservationConfirmedProjection.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/dto/ReservationConfirmedProjection.java new file mode 100644 index 00000000..e63b21a7 --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/dto/ReservationConfirmedProjection.java @@ -0,0 +1,8 @@ +package konkuk.chacall.domain.reservation.domain.repository.dto; + +public interface ReservationConfirmedProjection { + + Long getRoomId(); + + Boolean getConfirmed(); +} \ No newline at end of file From c3b74c95bc7b5eb528baaf2ae631688c4546bbcd Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 29 Nov 2025 16:30:31 +0900 Subject: [PATCH 32/33] =?UTF-8?q?[refactor]=20Param=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/domain/repository/ReservationRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java index eb070051..1b1331ea 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java @@ -59,5 +59,5 @@ Slice findMemberReservationsByStatusWithCursor( "CASE WHEN r.reservationStatus = 'CONFIRMED' THEN true ELSE false END AS confirmed " + "FROM Reservation r " + "WHERE r.chatRoom.chatRoomId IN :roomIds") - List findReservationConfirmedByChatRoomIds(List roomIds); + List findReservationConfirmedByChatRoomIds(@Param("roomIds") List roomIds); } From 48f106be54c0758e6912ac832eb817a77248190a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sat, 29 Nov 2025 16:31:06 +0900 Subject: [PATCH 33/33] =?UTF-8?q?[refactor]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20import=20=EC=A0=9C=EA=B1=B0=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/domain/repository/ReservationRepository.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java index 1b1331ea..2a6583c9 100644 --- a/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/konkuk/chacall/domain/reservation/domain/repository/ReservationRepository.java @@ -13,7 +13,6 @@ import org.springframework.data.repository.query.Param; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set;