Skip to content

Commit 6da42bd

Browse files
lsy1307whqtker
andauthored
feat: 채팅 이미지 전송 구현 (#475)
* chore: 토픽 주소 변경 - topic/{roomId} -> topic/chat/{roomId} - 의미적 명확성을 위해 * feat: 메시지 전송 DTO 작성 * feat: 메시지 전송 Service 작성 * feat: 메시지 전송 Controller 작성 * chore: 메시지 전송에 대한 컨트롤러 어노테이션을 RestController에서 Controller로 변경 * chore: WebSocket 초기 연결을 위한 HTTP 핸드셰이크에서 인증을 수행하도록 * fix: 핸드셰이크 후 Principal을 WebSocket 세션에 전달하도록 수정 - 이에 컨트롤러 인자로 siteUserId를 받도록 하고, DTO에 senderId를 삭제한다. * fix: 컨트롤러 파라미터 인자로 Principal를 받고, 이후 SiteUserDetails에서 siteUserId를 추출하도록 변경 * fix: DTO를 통해 순환참조 문제 해결 * chore: 실제 구독 권한 TODO 구현 - 검증 로직이 핸들러에서 사용됨에 따라 발생하는 순환 참조를 막기 위해 Lazy 어노테이션을 사용한 생성자를 직접 작성 * chore: 코드 리포매팅 * chore: 미사용 SiteUserPrincipal 제거 외 - 정규표현식을 사용하여 채팅방 ID 추출 - DTO 검증 추가 - 구체화 클래스가 아닌 인터페이스 사용하도록 (DIP) - senderId가 siteUserId가 아니라 chatParticipantId로 설정되도록 변경 * feat: 이미지 업로드를 위한 S3 Controller 및 Service 추가, ImgType수정 * feat: DTO 추가, 수정 및 MessageType 추가 * feat: Controller, Service 구현 * feat: Test 코드 추가 * fix: 서브 모듈 커밋해시 수정 * refactor: addAttachment 메서드 추가 * refactor: 코드 포매팅 * refactor: 테스트 코드 컨벤션 수정 * refactor: setChatMessage 메서드 수정 --------- Co-authored-by: seonghyeok <[email protected]>
1 parent 2eab140 commit 6da42bd

File tree

12 files changed

+254
-11
lines changed

12 files changed

+254
-11
lines changed

src/main/java/com/example/solidconnection/chat/controller/ChatMessageController.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.solidconnection.chat.controller;
22

3+
import com.example.solidconnection.chat.dto.ChatImageSendRequest;
34
import com.example.solidconnection.chat.dto.ChatMessageSendRequest;
45
import com.example.solidconnection.chat.service.ChatService;
56
import com.example.solidconnection.security.authentication.TokenAuthentication;
@@ -29,4 +30,16 @@ public void sendChatMessage(
2930

3031
chatService.sendChatMessage(chatMessageSendRequest, siteUserDetails.getSiteUser().getId(), roomId);
3132
}
33+
34+
@MessageMapping("/chat/{roomId}/image")
35+
public void sendChatImage(
36+
@DestinationVariable Long roomId,
37+
@Valid @Payload ChatImageSendRequest chatImageSendRequest,
38+
Principal principal
39+
) {
40+
TokenAuthentication tokenAuthentication = (TokenAuthentication) principal;
41+
SiteUserDetails siteUserDetails = (SiteUserDetails) tokenAuthentication.getPrincipal();
42+
43+
chatService.sendChatImage(chatImageSendRequest, siteUserDetails.getSiteUser().getId(), roomId);
44+
}
3245
}

src/main/java/com/example/solidconnection/chat/domain/ChatAttachment.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import jakarta.persistence.GeneratedValue;
88
import jakarta.persistence.GenerationType;
99
import jakarta.persistence.Id;
10+
import jakarta.persistence.JoinColumn;
1011
import jakarta.persistence.ManyToOne;
1112
import lombok.AccessLevel;
1213
import lombok.Getter;
@@ -30,7 +31,7 @@ public class ChatAttachment extends BaseEntity {
3031
@Column(length = 500)
3132
private String thumbnailUrl;
3233

33-
@ManyToOne(fetch = FetchType.LAZY)
34+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
3435
private ChatMessage chatMessage;
3536

3637
public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMessage chatMessage) {
@@ -42,4 +43,17 @@ public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMess
4243
chatMessage.getChatAttachments().add(this);
4344
}
4445
}
46+
47+
protected void setChatMessage(ChatMessage chatMessage) {
48+
if (this.chatMessage == chatMessage) return;
49+
50+
if (this.chatMessage != null) {
51+
this.chatMessage.getChatAttachments().remove(this);
52+
}
53+
54+
this.chatMessage = chatMessage;
55+
if (chatMessage != null && !chatMessage.getChatAttachments().contains(this)) {
56+
chatMessage.getChatAttachments().add(this);
57+
}
58+
}
4559
}

src/main/java/com/example/solidconnection/chat/domain/ChatMessage.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public class ChatMessage extends BaseEntity {
3333
@ManyToOne(fetch = FetchType.LAZY)
3434
private ChatRoom chatRoom;
3535

36-
@OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL)
36+
@OneToMany(mappedBy = "chatMessage", cascade = CascadeType.ALL, orphanRemoval = true)
3737
private final List<ChatAttachment> chatAttachments = new ArrayList<>();
3838

3939
public ChatMessage(String content, long senderId, ChatRoom chatRoom) {
@@ -44,4 +44,9 @@ public ChatMessage(String content, long senderId, ChatRoom chatRoom) {
4444
chatRoom.getChatMessages().add(this);
4545
}
4646
}
47+
48+
public void addAttachment(ChatAttachment attachment) {
49+
this.chatAttachments.add(attachment);
50+
attachment.setChatMessage(this);
51+
}
4752
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.example.solidconnection.chat.domain;
2+
3+
public enum MessageType {
4+
TEXT,
5+
IMAGE,
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.example.solidconnection.chat.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.Size;
6+
import java.util.List;
7+
8+
public record ChatImageSendRequest(
9+
@NotNull(message = "이미지 URL 목록은 필수입니다")
10+
@Size(min = 1, max = 10, message = "이미지는 1~10개까지 가능합니다")
11+
List<@NotBlank(message = "이미지 URL은 필수입니다") String> imageUrls
12+
) {
13+
14+
}
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
11
package com.example.solidconnection.chat.dto;
22

33
import com.example.solidconnection.chat.domain.ChatMessage;
4+
import com.example.solidconnection.chat.domain.MessageType;
5+
import java.util.List;
46

57
public record ChatMessageSendResponse(
68
long messageId,
79
String content,
8-
long senderId
10+
long senderId,
11+
MessageType messageType,
12+
List<ChatAttachmentResponse> attachments
913
) {
1014

1115
public static ChatMessageSendResponse from(ChatMessage chatMessage) {
16+
MessageType messageType = chatMessage.getChatAttachments().isEmpty()
17+
? MessageType.TEXT
18+
: MessageType.IMAGE;
19+
20+
List<ChatAttachmentResponse> attachments = chatMessage.getChatAttachments().stream()
21+
.map(attachment -> ChatAttachmentResponse.of(
22+
attachment.getId(),
23+
attachment.getIsImage(),
24+
attachment.getUrl(),
25+
attachment.getThumbnailUrl(),
26+
attachment.getCreatedAt()
27+
))
28+
.toList();
29+
1230
return new ChatMessageSendResponse(
1331
chatMessage.getId(),
1432
chatMessage.getContent(),
15-
chatMessage.getSenderId()
33+
chatMessage.getSenderId(),
34+
messageType,
35+
attachments
1636
);
1737
}
18-
1938
}

src/main/java/com/example/solidconnection/chat/service/ChatService.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
import static com.example.solidconnection.common.exception.ErrorCode.INVALID_CHAT_ROOM_STATE;
66
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
77

8+
import com.example.solidconnection.chat.domain.ChatAttachment;
89
import com.example.solidconnection.chat.domain.ChatMessage;
910
import com.example.solidconnection.chat.domain.ChatParticipant;
1011
import com.example.solidconnection.chat.domain.ChatRoom;
1112
import com.example.solidconnection.chat.dto.ChatAttachmentResponse;
13+
import com.example.solidconnection.chat.dto.ChatImageSendRequest;
1214
import com.example.solidconnection.chat.dto.ChatMessageResponse;
1315
import com.example.solidconnection.chat.dto.ChatMessageSendRequest;
1416
import com.example.solidconnection.chat.dto.ChatMessageSendResponse;
@@ -195,6 +197,53 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long
195197
simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
196198
}
197199

200+
@Transactional
201+
public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUserId, long roomId) {
202+
long senderId = chatParticipantRepository.findByChatRoomIdAndSiteUserId(roomId, siteUserId)
203+
.orElseThrow(() -> new CustomException(CHAT_PARTICIPANT_NOT_FOUND))
204+
.getId();
205+
206+
ChatRoom chatRoom = chatRoomRepository.findById(roomId)
207+
.orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE));
208+
209+
ChatMessage chatMessage = new ChatMessage(
210+
"",
211+
senderId,
212+
chatRoom
213+
);
214+
215+
for (String imageUrl : chatImageSendRequest.imageUrls()) {
216+
String thumbnailUrl = generateThumbnailUrl(imageUrl);
217+
218+
ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null);
219+
chatMessage.addAttachment(attachment);
220+
}
221+
222+
chatMessageRepository.save(chatMessage);
223+
224+
ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage);
225+
simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
226+
}
227+
228+
private String generateThumbnailUrl(String originalUrl) {
229+
try {
230+
String fileName = originalUrl.substring(originalUrl.lastIndexOf('/') + 1);
231+
232+
String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
233+
String extension = fileName.substring(fileName.lastIndexOf('.'));
234+
235+
String thumbnailFileName = nameWithoutExt + "_thumb" + extension;
236+
237+
String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/")
238+
.replace(fileName, thumbnailFileName);
239+
240+
return thumbnailUrl;
241+
242+
} catch (Exception e) {
243+
return originalUrl;
244+
}
245+
}
246+
198247
@Transactional
199248
public Long createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) {
200249
ChatRoom existingChatRoom = chatRoomRepository.findByMentoringId(mentoringId);

src/main/java/com/example/solidconnection/s3/controller/S3Controller.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
66
import com.example.solidconnection.s3.dto.urlPrefixResponse;
77
import com.example.solidconnection.s3.service.S3Service;
8+
import java.util.List;
89
import lombok.RequiredArgsConstructor;
910
import org.springframework.beans.factory.annotation.Value;
1011
import org.springframework.http.ResponseEntity;
@@ -68,6 +69,14 @@ public ResponseEntity<UploadedFileUrlResponse> uploadLanguageImage(
6869
return ResponseEntity.ok(profileImageUrl);
6970
}
7071

72+
@PostMapping("/chat")
73+
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
74+
@RequestParam("files") List<MultipartFile> imageFiles
75+
) {
76+
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
77+
return ResponseEntity.ok(chatImageUrls);
78+
}
79+
7180
@GetMapping("/s3-url-prefix")
7281
public ResponseEntity<urlPrefixResponse> getS3UrlPrefix() {
7382
return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded));

src/main/java/com/example/solidconnection/s3/domain/ImgType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
@Getter
66
public enum ImgType {
7-
PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news");
7+
PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"), CHAT("chat");
88

99
private final String type;
1010

src/main/java/com/example/solidconnection/s3/service/S3Service.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
public class S3Service {
3636

3737
private static final Logger log = LoggerFactory.getLogger(S3Service.class);
38-
private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 3;
38+
private static final long MAX_FILE_SIZE_MB = 1024 * 1024 * 5;
3939

4040
private final AmazonS3Client amazonS3;
4141
private final SiteUserRepository siteUserRepository;
@@ -52,8 +52,8 @@ public class S3Service {
5252
* - 파일에 대한 메타 데이터를 생성한다.
5353
* - 임의의 랜덤한 문자열로 파일 이름을 생성한다.
5454
* - S3에 파일을 업로드한다.
55-
* - 3mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다.
56-
* - 3mb 미만의 파일은 바로 업로드한다.
55+
* - 5mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다.
56+
* - 5mb 미만의 파일은 바로 업로드한다.
5757
* */
5858
public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) {
5959
// 파일 검증

0 commit comments

Comments
 (0)