From b5c8884de48aa973ee0060eb2d91abce2096593f Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:51:18 +0900 Subject: [PATCH 01/21] =?UTF-8?q?chore:=20=ED=86=A0=ED=94=BD=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - topic/{roomId} -> topic/chat/{roomId} - 의미적 명확성을 위해 --- .../com/example/solidconnection/chat/config/StompHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index 660f01f28..ec1db0bcb 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -56,9 +56,9 @@ private String extractRoomId(String destination) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } String[] parts = destination.split("/"); - if (parts.length < 3 || !parts[1].equals("topic")) { + if (parts.length < 4 || !parts[1].equals("topic") || !parts[2].equals("chat")) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - return parts[2]; + return parts[3]; } } From 678ef9de8de23deff41a87df68c21b25e012b3c0 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:51:47 +0900 Subject: [PATCH 02/21] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20DTO=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/dto/ChatMessageSendRequest.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java new file mode 100644 index 000000000..92a18f5eb --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.chat.dto; + +public record ChatMessageSendRequest( + long senderId, + String content +) { + +} From ecf92e3d9076250993d7ca7b0182c5066d69a557 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:54:20 +0900 Subject: [PATCH 03/21] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20Service=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatService.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index c378f6b50..f0536d57d 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -10,6 +10,7 @@ import com.example.solidconnection.chat.domain.ChatRoom; import com.example.solidconnection.chat.dto.ChatAttachmentResponse; import com.example.solidconnection.chat.dto.ChatMessageResponse; +import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.dto.ChatParticipantResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.dto.ChatRoomResponse; @@ -27,6 +28,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,6 +42,8 @@ public class ChatService { private final ChatReadStatusRepository chatReadStatusRepository; private final SiteUserRepository siteUserRepository; + private final SimpMessagingTemplate simpMessagingTemplate; + @Transactional(readOnly = true) public ChatRoomListResponse getChatRooms(long siteUserId) { // todo : n + 1 문제 해결 필요! @@ -109,13 +113,6 @@ private ChatMessageResponse toChatMessageResponse(ChatMessage message) { ); } - private void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); - } - } - @Transactional public void markChatMessagesAsRead(long siteUserId, long roomId) { ChatParticipant participant = chatParticipantRepository @@ -124,4 +121,27 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { chatReadStatusRepository.upsertReadStatus(roomId, participant.getId()); } + + @Transactional + public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long roomId) { + validateChatRoomParticipant(chatMessageSendRequest.senderId(), roomId); + + ChatMessage chatMessage = new ChatMessage( + chatMessageSendRequest.content(), + chatMessageSendRequest.senderId(), + chatRoomRepository.findById(roomId) + .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) + ); + + chatMessageRepository.save(chatMessage); + + simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessage); + } + + private void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); + } + } } From 4928e0c2df7ecfd00a205b2124e595584af116a7 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 18:54:30 +0900 Subject: [PATCH 04/21] =?UTF-8?q?feat:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20Controller=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatMessageController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java new file mode 100644 index 000000000..0a566bd99 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.chat.controller; + +import com.example.solidconnection.chat.dto.ChatMessageSendRequest; +import com.example.solidconnection.chat.service.ChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ChatMessageController { + + private final ChatService chatService; + + @MessageMapping("/chat/{roomId}") + public void sendChatMessage( + @DestinationVariable Long roomId, + @Payload ChatMessageSendRequest chatMessageSendRequest + ) { + + chatService.sendChatMessage(chatMessageSendRequest, roomId); + } +} From 2e1db36185b8a103c3ebc28ea6ddbe3e37c7546e Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Fri, 1 Aug 2025 19:57:04 +0900 Subject: [PATCH 05/21] =?UTF-8?q?chore:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=96=B4=EB=85=B8=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=EC=9D=84=20RestController=EC=97=90=EC=84=9C?= =?UTF-8?q?=20Controller=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatMessageController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 0a566bd99..b31c20982 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -6,9 +6,9 @@ import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.stereotype.Controller; -@RestController +@Controller @RequiredArgsConstructor public class ChatMessageController { From 500b664652d1c4bc0fbad17a5cbef35152f347ae Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 20:01:24 +0900 Subject: [PATCH 06/21] =?UTF-8?q?chore:=20WebSocket=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=EC=9D=84=20=EC=9C=84=ED=95=9C=20HTTP=20?= =?UTF-8?q?=ED=95=B8=EB=93=9C=EC=85=B0=EC=9D=B4=ED=81=AC=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=EC=9D=84=20=EC=88=98=ED=96=89=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/security/config/SecurityConfiguration.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java index 3667e9d84..12a31c5d6 100644 --- a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java @@ -62,6 +62,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .requestMatchers("/connect/**").authenticated() .requestMatchers("/admin/**").hasRole(ADMIN.name()) .anyRequest().permitAll() ) From 560e13cd872391c935b3d022f0b16339de53df8c Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:02:56 +0900 Subject: [PATCH 07/21] =?UTF-8?q?fix:=20=ED=95=B8=EB=93=9C=EC=85=B0?= =?UTF-8?q?=EC=9D=B4=ED=81=AC=20=ED=9B=84=20Principal=EC=9D=84=20WebSocket?= =?UTF-8?q?=20=EC=84=B8=EC=85=98=EC=97=90=20=EC=A0=84=EB=8B=AC=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이에 컨트롤러 인자로 siteUserId를 받도록 하고, DTO에 senderId를 삭제한다. --- .../chat/config/CustomHandshakeHandler.java | 27 ++++++++++++++++ .../chat/config/SiteUserPrincipal.java | 11 +++++++ .../chat/config/StompHandler.java | 30 ++++++----------- .../chat/config/StompWebSocketConfig.java | 7 +++- .../config/WebSocketHandshakeInterceptor.java | 32 +++++++++++++++++++ .../controller/ChatMessageController.java | 7 ++-- .../chat/dto/ChatMessageSendRequest.java | 1 - .../chat/service/ChatService.java | 6 ++-- 8 files changed, 93 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java create mode 100644 src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java create mode 100644 src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java diff --git a/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java new file mode 100644 index 000000000..6c3054355 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/CustomHandshakeHandler.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; +import java.util.Map; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.support.DefaultHandshakeHandler; + +// WebSocket 세션의 Principal을 결정한다. +@Component +public class CustomHandshakeHandler extends DefaultHandshakeHandler { + + @Override + protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, + Map attributes) { + + Object userAttribute = attributes.get("user"); + + if (userAttribute instanceof Principal) { + Principal principal = (Principal) userAttribute; + return principal; + } + + return super.determineUser(request, wsHandler, attributes); + } +} diff --git a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java new file mode 100644 index 000000000..0fe494506 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; + +public record SiteUserPrincipal(Long id, String email) implements Principal { + + @Override + public String getName() { + return this.email; + } +} diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index ec1db0bcb..7cea816c7 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -2,11 +2,9 @@ import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED; -import com.example.solidconnection.auth.token.JwtTokenProvider; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; -import io.jsonwebtoken.Claims; -import lombok.RequiredArgsConstructor; +import java.security.Principal; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; @@ -15,42 +13,34 @@ import org.springframework.stereotype.Component; @Component -@RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { - private final JwtTokenProvider jwtTokenProvider; - @Override public Message preSend(Message message, MessageChannel channel) { final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); if (StompCommand.CONNECT.equals(accessor.getCommand())) { - Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED); + Principal user = accessor.getUser(); + if (user == null) { + throw new CustomException(AUTHENTICATION_FAILED); + } } if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { - Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED); + SiteUserPrincipal user = (SiteUserPrincipal) accessor.getUser(); + if (user == null) { + throw new CustomException(AUTHENTICATION_FAILED); + } - String email = claims.getSubject(); String destination = accessor.getDestination(); - String roomId = extractRoomId(destination); - // todo: roomId 기반 실제 구독 권한 검사 로직 추가 + // todo: roomId와 user.getId() 기반으로 실제 구독 권한 검사 로직 } return message; } - private Claims validateAndExtractClaims(StompHeaderAccessor accessor, ErrorCode errorCode) { - String bearerToken = accessor.getFirstNativeHeader("Authorization"); - if (bearerToken == null || !bearerToken.startsWith("Bearer ")) { - throw new CustomException(errorCode); - } - String token = bearerToken.substring(7); - return jwtTokenProvider.parseClaims(token); - } - private String extractRoomId(String destination) { if (destination == null) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); diff --git a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java index 86b6eef5d..78baf8a13 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompWebSocketConfig.java @@ -22,12 +22,17 @@ public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { private final StompHandler stompHandler; private final StompProperties stompProperties; private final CorsProperties corsProperties; + private final WebSocketHandshakeInterceptor webSocketHandshakeInterceptor; + private final CustomHandshakeHandler customHandshakeHandler; @Override public void registerStompEndpoints(StompEndpointRegistry registry) { List strings = corsProperties.allowedOrigins(); String[] allowedOrigins = strings.toArray(String[]::new); - registry.addEndpoint("/connect").setAllowedOrigins(allowedOrigins).withSockJS(); + registry.addEndpoint("/connect") + .setAllowedOrigins(allowedOrigins) // postman 테스트를 위해 sockJS 비활성화 + .addInterceptors(webSocketHandshakeInterceptor) + .setHandshakeHandler(customHandshakeHandler); } @Override diff --git a/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java new file mode 100644 index 000000000..9e8aafe2d --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/config/WebSocketHandshakeInterceptor.java @@ -0,0 +1,32 @@ +package com.example.solidconnection.chat.config; + +import java.security.Principal; +import java.util.Map; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +// Principal을 WebSocket 세션에 저장하는 것에만 집중한다. +@Component +public class WebSocketHandshakeInterceptor implements HandshakeInterceptor { + + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Map attributes) { + Principal principal = request.getPrincipal(); + + if (principal != null) { + attributes.put("user", principal); + return true; + } + + return false; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler wsHandler, Exception exception) { + } +} diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index b31c20982..87436a422 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -2,6 +2,7 @@ import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.service.ChatService; +import com.example.solidconnection.common.resolver.AuthorizedUser; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -17,9 +18,9 @@ public class ChatMessageController { @MessageMapping("/chat/{roomId}") public void sendChatMessage( @DestinationVariable Long roomId, - @Payload ChatMessageSendRequest chatMessageSendRequest + @Payload ChatMessageSendRequest chatMessageSendRequest, + @AuthorizedUser Long siteUserId ) { - - chatService.sendChatMessage(chatMessageSendRequest, roomId); + chatService.sendChatMessage(chatMessageSendRequest, siteUserId, roomId); } } diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java index 92a18f5eb..623053268 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.chat.dto; public record ChatMessageSendRequest( - long senderId, String content ) { diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index f0536d57d..c08184ed9 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -123,12 +123,12 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { } @Transactional - public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long roomId) { - validateChatRoomParticipant(chatMessageSendRequest.senderId(), roomId); + public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long siteUserId, long roomId) { + validateChatRoomParticipant(siteUserId, roomId); ChatMessage chatMessage = new ChatMessage( chatMessageSendRequest.content(), - chatMessageSendRequest.senderId(), + siteUserId, chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) ); From 482b594184945fd31aaf2299477d9bdc4f19d1ad Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:30:26 +0900 Subject: [PATCH 08/21] =?UTF-8?q?fix:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90=EB=A1=9C=20Principal=EB=A5=BC=20=EB=B0=9B=EA=B3=A0,?= =?UTF-8?q?=20=EC=9D=B4=ED=9B=84=20SiteUserDetails=EC=97=90=EC=84=9C=20sit?= =?UTF-8?q?eUserId=EB=A5=BC=20=EC=B6=94=EC=B6=9C=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/controller/ChatMessageController.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 87436a422..1673e503c 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -2,7 +2,9 @@ import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.service.ChatService; -import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; +import java.security.Principal; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -19,8 +21,11 @@ public class ChatMessageController { public void sendChatMessage( @DestinationVariable Long roomId, @Payload ChatMessageSendRequest chatMessageSendRequest, - @AuthorizedUser Long siteUserId + Principal principal ) { - chatService.sendChatMessage(chatMessageSendRequest, siteUserId, roomId); + TokenAuthentication tokenAuthentication = (TokenAuthentication) principal; + SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal(); + + chatService.sendChatMessage(chatMessageSendRequest, siteUserDetails.getSiteUser().getId(), roomId); } } From db8f107fc35139717c1ff722d71bb1a0eb62ea90 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:39:07 +0900 Subject: [PATCH 09/21] =?UTF-8?q?fix:=20DTO=EB=A5=BC=20=ED=86=B5=ED=95=B4?= =?UTF-8?q?=20=EC=88=9C=ED=99=98=EC=B0=B8=EC=A1=B0=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/ChatMessageSendResponse.java | 19 +++++++++++++++++++ .../chat/service/ChatService.java | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java new file mode 100644 index 000000000..065c7ba1c --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.chat.dto; + +import com.example.solidconnection.chat.domain.ChatMessage; + +public record ChatMessageSendResponse( + long messageId, + String content, + long senderId +) { + + public static ChatMessageSendResponse from(ChatMessage chatMessage) { + return new ChatMessageSendResponse( + chatMessage.getId(), + chatMessage.getContent(), + chatMessage.getSenderId() + ); + } + +} diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index c08184ed9..8ecb1cf78 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -11,6 +11,7 @@ import com.example.solidconnection.chat.dto.ChatAttachmentResponse; import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatMessageSendRequest; +import com.example.solidconnection.chat.dto.ChatMessageSendResponse; import com.example.solidconnection.chat.dto.ChatParticipantResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.dto.ChatRoomResponse; @@ -135,7 +136,9 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long chatMessageRepository.save(chatMessage); - simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessage); + ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); + + simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } private void validateChatRoomParticipant(long siteUserId, long roomId) { From 5b1304d303827e892927562ebbc53db477734dcc Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:58:24 +0900 Subject: [PATCH 10/21] =?UTF-8?q?chore:=20=EC=8B=A4=EC=A0=9C=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=EA=B6=8C=ED=95=9C=20TODO=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 검증 로직이 핸들러에서 사용됨에 따라 발생하는 순환 참조를 막기 위해 Lazy 어노테이션을 사용한 생성자를 직접 작성 --- .../chat/config/StompHandler.java | 16 +++++++++++++--- .../chat/service/ChatService.java | 19 ++++++++++++++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index 7cea816c7..a93a95270 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -2,9 +2,13 @@ import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED; +import com.example.solidconnection.chat.service.ChatService; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.security.authentication.TokenAuthentication; +import com.example.solidconnection.security.userdetails.SiteUserDetails; import java.security.Principal; +import lombok.RequiredArgsConstructor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; @@ -13,8 +17,11 @@ import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { + private final ChatService chatService; + @Override public Message preSend(Message message, MessageChannel channel) { final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); @@ -27,15 +34,18 @@ public Message preSend(Message message, MessageChannel channel) { } if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) { - SiteUserPrincipal user = (SiteUserPrincipal) accessor.getUser(); + Principal user = accessor.getUser(); if (user == null) { throw new CustomException(AUTHENTICATION_FAILED); } + TokenAuthentication tokenAuthentication = (TokenAuthentication) user; + SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal(); + String destination = accessor.getDestination(); - String roomId = extractRoomId(destination); + long roomId = Long.parseLong(extractRoomId(destination)); - // todo: roomId와 user.getId() 기반으로 실제 구독 권한 검사 로직 + chatService.validateChatRoomParticipant(siteUserDetails.getSiteUser().getId(), roomId); } return message; diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 8ecb1cf78..56bba8306 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -26,14 +26,13 @@ import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; -import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -@RequiredArgsConstructor @Service public class ChatService { @@ -45,6 +44,20 @@ public class ChatService { private final SimpMessagingTemplate simpMessagingTemplate; + public ChatService(ChatRoomRepository chatRoomRepository, + ChatMessageRepository chatMessageRepository, + ChatParticipantRepository chatParticipantRepository, + ChatReadStatusRepository chatReadStatusRepository, + SiteUserRepository siteUserRepository, + @Lazy SimpMessagingTemplate simpMessagingTemplate) { + this.chatRoomRepository = chatRoomRepository; + this.chatMessageRepository = chatMessageRepository; + this.chatParticipantRepository = chatParticipantRepository; + this.chatReadStatusRepository = chatReadStatusRepository; + this.siteUserRepository = siteUserRepository; + this.simpMessagingTemplate = simpMessagingTemplate; + } + @Transactional(readOnly = true) public ChatRoomListResponse getChatRooms(long siteUserId) { // todo : n + 1 문제 해결 필요! @@ -141,7 +154,7 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } - private void validateChatRoomParticipant(long siteUserId, long roomId) { + public void validateChatRoomParticipant(long siteUserId, long roomId) { boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); if (!isParticipant) { throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); From 865e520f69c9157c9edc28294ea2d3b3f8338179 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sat, 2 Aug 2025 21:59:38 +0900 Subject: [PATCH 11/21] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/security/config/SecurityConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java index 12a31c5d6..706fedd52 100644 --- a/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/security/config/SecurityConfiguration.java @@ -5,8 +5,8 @@ import com.example.solidconnection.common.exception.CustomAccessDeniedHandler; import com.example.solidconnection.common.exception.CustomAuthenticationEntryPoint; import com.example.solidconnection.security.filter.ExceptionHandlerFilter; -import com.example.solidconnection.security.filter.TokenAuthenticationFilter; import com.example.solidconnection.security.filter.SignOutCheckFilter; +import com.example.solidconnection.security.filter.TokenAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; From ab6422f4266bd44b7fd43e2d6d04dd27aaecba73 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Mon, 4 Aug 2025 09:17:33 +0900 Subject: [PATCH 12/21] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20Si?= =?UTF-8?q?teUserPrincipal=20=EC=A0=9C=EA=B1=B0=20=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정규표현식을 사용하여 채팅방 ID 추출 - DTO 검증 추가 - 구체화 클래스가 아닌 인터페이스 사용하도록 (DIP) - senderId가 siteUserId가 아니라 chatParticipantId로 설정되도록 변경 --- .../chat/config/SiteUserPrincipal.java | 11 ------- .../chat/config/StompHandler.java | 11 +++++-- .../controller/ChatMessageController.java | 3 +- .../chat/dto/ChatMessageSendRequest.java | 5 ++++ .../chat/service/ChatService.java | 30 ++++++++++--------- 5 files changed, 31 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java diff --git a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java b/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java deleted file mode 100644 index 0fe494506..000000000 --- a/src/main/java/com/example/solidconnection/chat/config/SiteUserPrincipal.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.solidconnection.chat.config; - -import java.security.Principal; - -public record SiteUserPrincipal(Long id, String email) implements Principal { - - @Override - public String getName() { - return this.email; - } -} diff --git a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java index a93a95270..2e99bf9c4 100644 --- a/src/main/java/com/example/solidconnection/chat/config/StompHandler.java +++ b/src/main/java/com/example/solidconnection/chat/config/StompHandler.java @@ -8,6 +8,8 @@ import com.example.solidconnection.security.authentication.TokenAuthentication; import com.example.solidconnection.security.userdetails.SiteUserDetails; import java.security.Principal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -20,6 +22,7 @@ @RequiredArgsConstructor public class StompHandler implements ChannelInterceptor { + private static final Pattern ROOM_ID_PATTERN = Pattern.compile("^/topic/chat/(\\d+)$"); private final ChatService chatService; @Override @@ -55,10 +58,12 @@ private String extractRoomId(String destination) { if (destination == null) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - String[] parts = destination.split("/"); - if (parts.length < 4 || !parts[1].equals("topic") || !parts[2].equals("chat")) { + + Matcher matcher = ROOM_ID_PATTERN.matcher(destination); + if (!matcher.matches()) { throw new CustomException(ErrorCode.INVALID_ROOM_ID); } - return parts[3]; + + return matcher.group(1); } } diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index 1673e503c..a7e158224 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -4,6 +4,7 @@ import com.example.solidconnection.chat.service.ChatService; import com.example.solidconnection.security.authentication.TokenAuthentication; import com.example.solidconnection.security.userdetails.SiteUserDetails; +import jakarta.validation.Valid; import java.security.Principal; import lombok.RequiredArgsConstructor; import org.springframework.messaging.handler.annotation.DestinationVariable; @@ -20,7 +21,7 @@ public class ChatMessageController { @MessageMapping("/chat/{roomId}") public void sendChatMessage( @DestinationVariable Long roomId, - @Payload ChatMessageSendRequest chatMessageSendRequest, + @Valid @Payload ChatMessageSendRequest chatMessageSendRequest, Principal principal ) { TokenAuthentication tokenAuthentication = (TokenAuthentication) principal; diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java index 623053268..22d652a35 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendRequest.java @@ -1,6 +1,11 @@ package com.example.solidconnection.chat.dto; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + public record ChatMessageSendRequest( + @NotNull(message = "메시지를 입력해주세요.") + @Size(max = 500, message = "메시지는 500자를 초과할 수 없습니다") String content ) { diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 56bba8306..fadd284fe 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -29,7 +29,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,20 +42,20 @@ public class ChatService { private final ChatReadStatusRepository chatReadStatusRepository; private final SiteUserRepository siteUserRepository; - private final SimpMessagingTemplate simpMessagingTemplate; + private final SimpMessageSendingOperations simpMessageSendingOperations; public ChatService(ChatRoomRepository chatRoomRepository, ChatMessageRepository chatMessageRepository, ChatParticipantRepository chatParticipantRepository, ChatReadStatusRepository chatReadStatusRepository, SiteUserRepository siteUserRepository, - @Lazy SimpMessagingTemplate simpMessagingTemplate) { + @Lazy SimpMessageSendingOperations simpMessageSendingOperations) { this.chatRoomRepository = chatRoomRepository; this.chatMessageRepository = chatMessageRepository; this.chatParticipantRepository = chatParticipantRepository; this.chatReadStatusRepository = chatReadStatusRepository; this.siteUserRepository = siteUserRepository; - this.simpMessagingTemplate = simpMessagingTemplate; + this.simpMessageSendingOperations = simpMessageSendingOperations; } @Transactional(readOnly = true) @@ -107,6 +107,13 @@ public SliceResponse getChatMessages(long siteUserId, long return SliceResponse.of(content, chatMessages); } + public void validateChatRoomParticipant(long siteUserId, long roomId) { + boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); + if (!isParticipant) { + throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); + } + } + private ChatMessageResponse toChatMessageResponse(ChatMessage message) { List attachments = message.getChatAttachments().stream() .map(attachment -> ChatAttachmentResponse.of( @@ -138,11 +145,13 @@ public void markChatMessagesAsRead(long siteUserId, long roomId) { @Transactional public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long siteUserId, long roomId) { - validateChatRoomParticipant(siteUserId, roomId); + long senderId = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId) + .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND)) + .getId(); ChatMessage chatMessage = new ChatMessage( chatMessageSendRequest.content(), - siteUserId, + senderId, chatRoomRepository.findById(roomId) .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)) ); @@ -151,13 +160,6 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); - simpMessagingTemplate.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); - } - - public void validateChatRoomParticipant(long siteUserId, long roomId) { - boolean isParticipant = chatParticipantRepository.existsByChatRoomIdAndSiteUserId(roomId, siteUserId); - if (!isParticipant) { - throw new CustomException(CHAT_PARTICIPANT_NOT_FOUND); - } + simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } } From c92e336e214f8c601d78df9f9f74a104c90e3e99 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Sun, 24 Aug 2025 20:29:14 +0900 Subject: [PATCH 13/21] =?UTF-8?q?feat:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20S3?= =?UTF-8?q?=20Controller=20=EB=B0=8F=20Service=20=EC=B6=94=EA=B0=80,=20Img?= =?UTF-8?q?Type=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/s3/controller/S3Controller.java | 9 +++++++++ .../com/example/solidconnection/s3/domain/ImgType.java | 2 +- .../example/solidconnection/s3/service/S3Service.java | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java index 1bd978627..98b0574f9 100644 --- a/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/controller/S3Controller.java @@ -5,6 +5,7 @@ import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.dto.urlPrefixResponse; import com.example.solidconnection.s3.service.S3Service; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -68,6 +69,14 @@ public ResponseEntity uploadLanguageImage( return ResponseEntity.ok(profileImageUrl); } + @PostMapping("/chat") + public ResponseEntity> uploadChatImage( + @RequestParam("files") List imageFiles + ) { + List chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT); + return ResponseEntity.ok(chatImageUrls); + } + @GetMapping("/s3-url-prefix") public ResponseEntity getS3UrlPrefix() { return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); diff --git a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java b/src/main/java/com/example/solidconnection/s3/domain/ImgType.java index 7efedb1a5..50ac78b30 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java +++ b/src/main/java/com/example/solidconnection/s3/domain/ImgType.java @@ -4,7 +4,7 @@ @Getter public enum ImgType { - PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"); + PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"), CHAT("chat"); private final String type; diff --git a/src/main/java/com/example/solidconnection/s3/service/S3Service.java b/src/main/java/com/example/solidconnection/s3/service/S3Service.java index 11b66a499..4c4110693 100644 --- a/src/main/java/com/example/solidconnection/s3/service/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/service/S3Service.java @@ -35,7 +35,7 @@ public class S3Service { private static final Logger log = LoggerFactory.getLogger(S3Service.class); - private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 3; + private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5; private final AmazonS3Client amazonS3; private final SiteUserRepository siteUserRepository; @@ -52,8 +52,8 @@ public class S3Service { * - 파일에 대한 메타 데이터를 생성한다. * - 임의의 랜덤한 문자열로 파일 이름을 생성한다. * - S3에 파일을 업로드한다. - * - 3mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다. - * - 3mb 미만의 파일은 바로 업로드한다. + * - 5mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다. + * - 5mb 미만의 파일은 바로 업로드한다. * */ public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) { // 파일 검증 From 078a28bd6eca733d2a7c8be1fa6a15b6bd4bbd65 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Sun, 24 Aug 2025 20:41:45 +0900 Subject: [PATCH 14/21] =?UTF-8?q?feat:=20DTO=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20MessageType=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/MessageType.java | 6 +++++ .../chat/dto/ChatImageSendRequest.java | 13 ++++++++++ .../chat/dto/ChatMessageSendResponse.java | 25 ++++++++++++++++--- 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/chat/domain/MessageType.java create mode 100644 src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java diff --git a/src/main/java/com/example/solidconnection/chat/domain/MessageType.java b/src/main/java/com/example/solidconnection/chat/domain/MessageType.java new file mode 100644 index 000000000..6e2750f38 --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/domain/MessageType.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.chat.domain; + +public enum MessageType { + TEXT, + IMAGE, +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java new file mode 100644 index 000000000..01e0c6c5c --- /dev/null +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.chat.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record ChatImageSendRequest( + @NotNull(message = "이미지 URL 목록은 필수입니다") + @Size(min = 1, max = 10, message = "이미지는 1~10개까지 가능합니다") + List<@NotBlank(message = "이미지 URL은 필수입니다") String> imageUrls +) { +} diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java index 065c7ba1c..8e976148a 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java @@ -1,19 +1,38 @@ package com.example.solidconnection.chat.dto; import com.example.solidconnection.chat.domain.ChatMessage; +import com.example.solidconnection.chat.domain.MessageType; +import java.util.List; public record ChatMessageSendResponse( long messageId, String content, - long senderId + long senderId, + MessageType messageType, + List attachments ) { public static ChatMessageSendResponse from(ChatMessage chatMessage) { + MessageType messageType = chatMessage.getChatAttachments().isEmpty() + ? MessageType.TEXT + : MessageType.IMAGE; + + List attachments = chatMessage.getChatAttachments().stream() + .map(attachment -> ChatAttachmentResponse.of( + attachment.getId(), + attachment.getIsImage(), + attachment.getUrl(), + attachment.getThumbnailUrl(), + attachment.getCreatedAt() + )) + .toList(); + return new ChatMessageSendResponse( chatMessage.getId(), chatMessage.getContent(), - chatMessage.getSenderId() + chatMessage.getSenderId(), + messageType, + attachments ); } - } From ac2ad8c1258e87d86a30d8814d6460a08abd2ca4 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Sun, 24 Aug 2025 20:42:03 +0900 Subject: [PATCH 15/21] =?UTF-8?q?feat:=20Controller,=20Service=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ChatMessageController.java | 13 +++++ .../chat/service/ChatService.java | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java index a7e158224..47bf9939a 100644 --- a/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java +++ b/src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java @@ -1,5 +1,6 @@ package com.example.solidconnection.chat.controller; +import com.example.solidconnection.chat.dto.ChatImageSendRequest; import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.service.ChatService; import com.example.solidconnection.security.authentication.TokenAuthentication; @@ -29,4 +30,16 @@ public void sendChatMessage( chatService.sendChatMessage(chatMessageSendRequest, siteUserDetails.getSiteUser().getId(), roomId); } + + @MessageMapping("/chat/{roomId}/image") + public void sendChatImage( + @DestinationVariable Long roomId, + @Valid @Payload ChatImageSendRequest chatImageSendRequest, + Principal principal + ) { + TokenAuthentication tokenAuthentication = (TokenAuthentication) principal; + SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal(); + + chatService.sendChatImage(chatImageSendRequest, siteUserDetails.getSiteUser().getId(), roomId); + } } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 874d71bd5..92e759f4c 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -5,10 +5,12 @@ import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; +import com.example.solidconnection.chat.domain.ChatAttachment; import com.example.solidconnection.chat.domain.ChatMessage; import com.example.solidconnection.chat.domain.ChatParticipant; import com.example.solidconnection.chat.domain.ChatRoom; import com.example.solidconnection.chat.dto.ChatAttachmentResponse; +import com.example.solidconnection.chat.dto.ChatImageSendRequest; import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.dto.ChatMessageSendResponse; @@ -173,6 +175,38 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } + @Transactional + public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUserId, long roomId) { + long senderId = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId) + .orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND)) + .getId(); + + ChatRoom chatRoom = chatRoomRepository.findById(roomId) + .orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE)); + + ChatMessage chatMessage = new ChatMessage( + "", + senderId, + chatRoom + ); + + for (String imageUrl : chatImageSendRequest.imageUrls()) { + String thumbnailUrl = generateThumbnailUrl(imageUrl); + + new ChatAttachment( + true, + imageUrl, + thumbnailUrl, + chatMessage + ); + } + + chatMessageRepository.save(chatMessage); + + ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); + simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); + } + @Transactional public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) { if (chatRoomRepository.existsByMentoringId(mentoringId)) { @@ -185,4 +219,23 @@ public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long mentee ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom); chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); } + + private String generateThumbnailUrl(String originalUrl) { + try { + String fileName = originalUrl.substring(originalUrl.lastIndexOf('/') + 1); + + String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); + String extension = fileName.substring(fileName.lastIndexOf('.')); + + String thumbnailFileName = nameWithoutExt + "_thumb" + extension; + + String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/") + .replace(fileName, thumbnailFileName); + + return thumbnailUrl; + + } catch (Exception e) { + return originalUrl; + } + } } From 73b3a0ec2d930c2bacd6f2e3af0257d4f15b8d05 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Sun, 24 Aug 2025 20:42:14 +0900 Subject: [PATCH 16/21] =?UTF-8?q?feat:=20Test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatServiceTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 9f3c1f017..c1f2178cc 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -11,6 +11,8 @@ import com.example.solidconnection.chat.domain.ChatParticipant; import com.example.solidconnection.chat.domain.ChatReadStatus; import com.example.solidconnection.chat.domain.ChatRoom; +import com.example.solidconnection.chat.domain.MessageType; +import com.example.solidconnection.chat.dto.ChatImageSendRequest; import com.example.solidconnection.chat.dto.ChatMessageResponse; import com.example.solidconnection.chat.dto.ChatMessageSendRequest; import com.example.solidconnection.chat.dto.ChatMessageSendResponse; @@ -28,6 +30,7 @@ import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import java.time.ZonedDateTime; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -444,4 +447,112 @@ void setUp() { .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); } } + + @Nested + class 채팅_이미지를_전송한다 { + + private SiteUser sender; + private ChatParticipant senderParticipant; + private ChatRoom chatRoom; + + @BeforeEach + void setUp() { + sender = siteUserFixture.사용자(111, "sender"); + chatRoom = chatRoomFixture.채팅방(false); + senderParticipant = chatParticipantFixture.참여자(sender.getId(), chatRoom); + } + + @Test + void 채팅방_참여자는_이미지_메시지를_전송할_수_있다() { + // given + final List imageUrls = List.of( + "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test1.jpg", + "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test2.jpg" + ); + ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); + + // when + chatService.sendChatImage(request, sender.getId(), chatRoom.getId()); + + // then + ArgumentCaptor destinationCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); + + BDDMockito.verify(simpMessagingTemplate).convertAndSend(destinationCaptor.capture(), payloadCaptor.capture()); + + ChatMessageSendResponse response = payloadCaptor.getValue(); + + assertAll( + () -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()), + () -> assertThat(response.attachments()).hasSize(imageUrls.size()), + () -> assertThat(response.attachments().get(0).url()).isEqualTo(imageUrls.get(0)), + () -> assertThat(response.attachments().get(1).url()).isEqualTo(imageUrls.get(1)), + () -> assertThat(response.messageType()).isEqualTo(MessageType.IMAGE), + () -> assertThat(response.senderId()).isEqualTo(senderParticipant.getId()), + () -> assertThat(response.content()).isEmpty() // 이미지 메시지는 빈 content + ); + } + + @Test + void 단일_이미지_메시지가_정상_전송된다() { + // given + final List imageUrls = List.of( + "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/single.jpg" + ); + ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); + + // when + chatService.sendChatImage(request, sender.getId(), chatRoom.getId()); + + // then + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); + BDDMockito.verify(simpMessagingTemplate).convertAndSend(BDDMockito.anyString(), payloadCaptor.capture()); + + ChatMessageSendResponse response = payloadCaptor.getValue(); + + assertAll( + () -> assertThat(response.attachments()).hasSize(1), + () -> assertThat(response.attachments().get(0).url()).isEqualTo(imageUrls.get(0)), + () -> assertThat(response.messageType()).isEqualTo(MessageType.IMAGE) + ); + } + + @Test + void 채팅_참여자가_아니면_이미지_메시지를_전송할_수_없다() { + // given + SiteUser nonParticipant = siteUserFixture.사용자(333, "nonParticipant"); + List imageUrls = List.of("https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test.jpg"); + ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); + + // when & then + assertThatCode(() -> chatService.sendChatImage(request, nonParticipant.getId(), chatRoom.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); + } + + @Test + void 썸네일_URL이_정상적으로_생성된다() { + // given + final List imageUrls = List.of( + "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example.jpg" + ); + ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); + + // when + chatService.sendChatImage(request, sender.getId(), chatRoom.getId()); + + // then + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); + BDDMockito.verify(simpMessagingTemplate).convertAndSend(BDDMockito.anyString(), payloadCaptor.capture()); + + ChatMessageSendResponse response = payloadCaptor.getValue(); + + assertAll( + () -> assertThat(response.attachments().get(0).url()).isEqualTo(imageUrls.get(0)), + () -> assertThat(response.attachments().get(0).thumbnailUrl()).isEqualTo( + "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/thumbnails/example_thumb.jpg" + ) + ); + } + } } From 6069a6e9ae2c20986567733d188149229b0f7426 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Sun, 24 Aug 2025 22:00:32 +0900 Subject: [PATCH 17/21] =?UTF-8?q?fix:=20=EC=84=9C=EB=B8=8C=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=BB=A4=EB=B0=8B=ED=95=B4=EC=8B=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/secret | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index e592f6d36..bb3bf0f41 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit e592f6d36f57185c8d92a7838c0e3039603b2c57 +Subproject commit bb3bf0f4122d10ddacab279a368cf9f06d6f6dbd From 361e97130ad74d432aa2cfc159d689ff2888b623 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Tue, 26 Aug 2025 17:04:38 +0900 Subject: [PATCH 18/21] =?UTF-8?q?refactor:=20addAttachment=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/ChatAttachment.java | 4 +++ .../chat/domain/ChatMessage.java | 7 +++- .../chat/service/ChatService.java | 34 ++++++++----------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java index def9263c8..a7c472d1f 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java @@ -42,4 +42,8 @@ public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMess chatMessage.getChatAttachments().add(this); } } + + protected void setChatMessage(ChatMessage chatMessage) { + this.chatMessage = chatMessage; + } } diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java index 07fc99131..170a93f05 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java @@ -33,7 +33,7 @@ public class ChatMessage extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) private ChatRoom chatRoom; - @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL) + @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL, orphanRemoval = true) private final List chatAttachments = new ArrayList<>(); public ChatMessage(String content, long senderId, ChatRoom chatRoom) { @@ -44,4 +44,9 @@ public ChatMessage(String content, long senderId, ChatRoom chatRoom) { chatRoom.getChatMessages().add(this); } } + + public void addAttachment(ChatAttachment attachment) { + this.chatAttachments.add(attachment); + attachment.setChatMessage(this); + } } diff --git a/src/main/java/com/example/solidconnection/chat/service/ChatService.java b/src/main/java/com/example/solidconnection/chat/service/ChatService.java index 92e759f4c..46abd1d11 100644 --- a/src/main/java/com/example/solidconnection/chat/service/ChatService.java +++ b/src/main/java/com/example/solidconnection/chat/service/ChatService.java @@ -193,12 +193,8 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs for (String imageUrl : chatImageSendRequest.imageUrls()) { String thumbnailUrl = generateThumbnailUrl(imageUrl); - new ChatAttachment( - true, - imageUrl, - thumbnailUrl, - chatMessage - ); + ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null); + chatMessage.addAttachment(attachment); } chatMessageRepository.save(chatMessage); @@ -207,19 +203,6 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); } - @Transactional - public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) { - if (chatRoomRepository.existsByMentoringId(mentoringId)) { - return; - } - - ChatRoom chatRoom = new ChatRoom(mentoringId, false); - chatRoom = chatRoomRepository.save(chatRoom); - ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom); - ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom); - chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); - } - private String generateThumbnailUrl(String originalUrl) { try { String fileName = originalUrl.substring(originalUrl.lastIndexOf('/') + 1); @@ -238,4 +221,17 @@ private String generateThumbnailUrl(String originalUrl) { return originalUrl; } } + + @Transactional + public void createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) { + if (chatRoomRepository.existsByMentoringId(mentoringId)) { + return; + } + + ChatRoom chatRoom = new ChatRoom(mentoringId, false); + chatRoom = chatRoomRepository.save(chatRoom); + ChatParticipant mentorParticipant = new ChatParticipant(mentorId, chatRoom); + ChatParticipant menteeParticipant = new ChatParticipant(menteeId, chatRoom); + chatParticipantRepository.saveAll(List.of(mentorParticipant, menteeParticipant)); + } } From e9fadc2884e48426767f21e0b8cf99b3675d3ed2 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Tue, 26 Aug 2025 17:05:44 +0900 Subject: [PATCH 19/21] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/dto/ChatImageSendRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java b/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java index 01e0c6c5c..e32be3633 100644 --- a/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java +++ b/src/main/java/com/example/solidconnection/chat/dto/ChatImageSendRequest.java @@ -10,4 +10,5 @@ public record ChatImageSendRequest( @Size(min = 1, max = 10, message = "이미지는 1~10개까지 가능합니다") List<@NotBlank(message = "이미지 URL은 필수입니다") String> imageUrls ) { + } From 18a3b4dd75216c450d72ab9dac301ef704da33ae Mon Sep 17 00:00:00 2001 From: Yeonri Date: Tue, 26 Aug 2025 17:40:57 +0900 Subject: [PATCH 20/21] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatServiceTest.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index c1f2178cc..f5ec202bb 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -436,7 +436,7 @@ void setUp() { } @Test - void 채팅_참여자가_아니면_메시지를_전송할_수_없다() { + void 채팅_참여자가_아니면_예외가_발생한다() { // given SiteUser nonParticipant = siteUserFixture.사용자(333, "nonParticipant"); ChatMessageSendRequest request = new ChatMessageSendRequest("안녕하세요"); @@ -454,6 +454,9 @@ class 채팅_이미지를_전송한다 { private SiteUser sender; private ChatParticipant senderParticipant; private ChatRoom chatRoom; + private static final String TEST_IMAGE_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example.jpg"; + private static final String TEST_IMAGE_URL2 = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example2.jpg"; + private static final String EXPECTED_THUMBNAIL_URL = "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/thumbnails/example_thumb.jpg"; @BeforeEach void setUp() { @@ -466,8 +469,8 @@ void setUp() { void 채팅방_참여자는_이미지_메시지를_전송할_수_있다() { // given final List imageUrls = List.of( - "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test1.jpg", - "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test2.jpg" + TEST_IMAGE_URL, + TEST_IMAGE_URL2 ); ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); @@ -489,7 +492,7 @@ void setUp() { () -> assertThat(response.attachments().get(1).url()).isEqualTo(imageUrls.get(1)), () -> assertThat(response.messageType()).isEqualTo(MessageType.IMAGE), () -> assertThat(response.senderId()).isEqualTo(senderParticipant.getId()), - () -> assertThat(response.content()).isEmpty() // 이미지 메시지는 빈 content + () -> assertThat(response.content()).isEmpty() ); } @@ -497,7 +500,7 @@ void setUp() { void 단일_이미지_메시지가_정상_전송된다() { // given final List imageUrls = List.of( - "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/single.jpg" + TEST_IMAGE_URL ); ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); @@ -518,10 +521,10 @@ void setUp() { } @Test - void 채팅_참여자가_아니면_이미지_메시지를_전송할_수_없다() { + void 채팅_참여자가_아니면_예외가_발생한다() { // given SiteUser nonParticipant = siteUserFixture.사용자(333, "nonParticipant"); - List imageUrls = List.of("https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/test.jpg"); + List imageUrls = List.of(TEST_IMAGE_URL); ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); // when & then @@ -534,7 +537,7 @@ void setUp() { void 썸네일_URL이_정상적으로_생성된다() { // given final List imageUrls = List.of( - "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/images/example.jpg" + TEST_IMAGE_URL ); ChatImageSendRequest request = new ChatImageSendRequest(imageUrls); @@ -550,7 +553,7 @@ void setUp() { assertAll( () -> assertThat(response.attachments().get(0).url()).isEqualTo(imageUrls.get(0)), () -> assertThat(response.attachments().get(0).thumbnailUrl()).isEqualTo( - "https://bucket.s3.ap-northeast-2.amazonaws.com/chat/thumbnails/example_thumb.jpg" + EXPECTED_THUMBNAIL_URL ) ); } From 55160b18fafd98cea6ae9a6d656536bd160de1a9 Mon Sep 17 00:00:00 2001 From: Yeonri Date: Tue, 26 Aug 2025 17:56:35 +0900 Subject: [PATCH 21/21] =?UTF-8?q?refactor:=20setChatMessage=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/chat/domain/ChatAttachment.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java index a7c472d1f..da9c917ba 100644 --- a/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java +++ b/src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java @@ -7,6 +7,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.Getter; @@ -30,7 +31,7 @@ public class ChatAttachment extends BaseEntity { @Column(length = 500) private String thumbnailUrl; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.LAZY, optional = false) private ChatMessage chatMessage; public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMessage chatMessage) { @@ -44,6 +45,15 @@ public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMess } protected void setChatMessage(ChatMessage chatMessage) { + if (this.chatMessage == chatMessage) return; + + if (this.chatMessage != null) { + this.chatMessage.getChatAttachments().remove(this); + } + this.chatMessage = chatMessage; + if (chatMessage != null && !chatMessage.getChatAttachments().contains(this)) { + chatMessage.getChatAttachments().add(this); + } } }