-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 채팅 이미지 전송 구현 #475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 채팅 이미지 전송 구현 #475
Changes from all commits
b5c8884
678ef9d
ecf92e3
4928e0c
2e1db36
500b664
560e13c
482b594
db8f107
5b1304d
865e520
ab6422f
189b987
25c6f42
c92e336
078a28b
ac2ad8c
73b3a0e
6069a6e
361e971
e9fadc2
18a3b4d
55160b1
74b40dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.example.solidconnection.chat.domain; | ||
|
|
||
| public enum MessageType { | ||
| TEXT, | ||
| IMAGE, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL 검증 있으면 좋을 거 같네요 !
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 검증 로직은 곧 추가할 예정입니다! |
||
| ) { | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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<ChatAttachmentResponse> attachments | ||||||||||||||||
| ) { | ||||||||||||||||
|
|
||||||||||||||||
| public static ChatMessageSendResponse from(ChatMessage chatMessage) { | ||||||||||||||||
| MessageType messageType = chatMessage.getChatAttachments().isEmpty() | ||||||||||||||||
| ? MessageType.TEXT | ||||||||||||||||
| : MessageType.IMAGE; | ||||||||||||||||
|
|
||||||||||||||||
|
Comment on lines
+16
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 메시지 타입 산정 시 ‘첨부 존재 여부’ 대신 ‘이미지 첨부 포함 여부’를 사용해 주세요.
아래처럼 변경을 제안드립니다(추가 import 없이 람다 사용). - MessageType messageType = chatMessage.getChatAttachments().isEmpty()
- ? MessageType.TEXT
- : MessageType.IMAGE;
+ MessageType messageType = chatMessage.getChatAttachments().stream()
+ .anyMatch(att -> Boolean.TRUE.equals(att.getIsImage()))
+ ? MessageType.IMAGE
+ : MessageType.TEXT;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기억이 가물가물한데 현재 저희는 일단 이미지만 작업하는 거 맞나요 ? 그렇다면 이건 추후에 반영해도 될 거 같습니다 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Length of output: 21605 @whqtker 맞습니다! 현재 작업 스코프는 이미지 전송에 집중되어 있네요. 코드를 확인해보니:
PR 설명에서도 "이미지를 제외한 기타 첨부파일 관련 로직은 정상 동작을 확인했고, 추가 작업 예정"이라고 하셨으니, 지금은 이미지만 다루고 있는 게 맞는 것 같습니다. 따라서 제안드린 개선사항은 추후에 비이미지 첨부파일이 실제로 추가될 때 함께 반영하는 게 더 실용적일 것 같아요. 현재로서는 단순히 ᕦ(◕‿◕)ᕤ |
||||||||||||||||
| List<ChatAttachmentResponse> 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 | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -195,6 +197,53 @@ 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); | ||
|
|
||
| ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null); | ||
| chatMessage.addAttachment(attachment); | ||
| } | ||
|
|
||
| chatMessageRepository.save(chatMessage); | ||
|
|
||
| ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage); | ||
| simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메시지 전송 실패해도 DB에 트랜잭션 반영이 되는 거 같은데 이건 추후에 롤백 기능 넣어야겠네요
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 확인했습니당
Comment on lines
+200
to
+226
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain1) 트랜잭션 커밋 이후에만 브로드캐스트하도록 변경 + 첨부 생성 시 null 제거 제안
제안 diff: @@
for (String imageUrl : chatImageSendRequest.imageUrls()) {
String thumbnailUrl = generateThumbnailUrl(imageUrl);
- ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null);
+ ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, chatMessage);
chatMessage.addAttachment(attachment);
}
chatMessageRepository.save(chatMessage);
- ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage);
- simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
+ final ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.from(chatMessage);
+ org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(
+ new org.springframework.transaction.support.TransactionSynchronization() {
+ @Override
+ public void afterCommit() {
+ simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
+ }
+ }
+ );추가 import(파일 상단): import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;🏁 Script executed: #!/bin/bash
echo "1) ChatAttachment 클래스 위치 및 정의 검색:"
rg -nP --type=java 'class\s+ChatAttachment' -C5
echo "2) ChatAttachment의 @ManyToOne 매핑 확인:"
rg -n '@ManyToOne' -C3 --glob '*ChatAttachment.java'
echo "3) ChatAttachment 생성자 시그니처 검사:"
rg -n 'ChatAttachment\(' -C3 --glob '*ChatAttachment.java'
echo "4) ChatMessageSendResponse 클래스 정의 검색:"
rg -nP --type=java 'class\s+ChatMessageSendResponse' -C5
echo "5) ChatMessageSendResponse.from() 메서드 구현 확인:"
rg -n 'from\s*\(' -C3 --glob '*ChatMessageSendResponse.java'Length of output: 6951 반영해야 할 변경 사항
|
||
|
|
||
| private String generateThumbnailUrl(String originalUrl) { | ||
| try { | ||
| String fileName = originalUrl.substring(originalUrl.lastIndexOf('/') + 1); | ||
|
|
||
|
Comment on lines
+230
to
+231
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확장자 검증이 따로 없는 거 같은데 상관없나요? 사진만 받기로 했던 거 같아서요
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확장자 검증은 S3업로드 함수에서 합니다! 이후 인증로직 추가되면 URL자체를 검증하기 때문에 따로 확장자 검증은 필요없을 것 같습니당 |
||
| 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; | ||
| } | ||
| } | ||
|
Comment on lines
+228
to
+245
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용하는 함수 바로 밑으로 옮겨주세요~
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반영하였습니다! |
||
|
|
||
| @Transactional | ||
| public Long createMentoringChatRoom(Long mentoringId, Long mentorId, Long menteeId) { | ||
| ChatRoom existingChatRoom = chatRoomRepository.findByMentoringId(mentoringId); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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<UploadedFileUrlResponse> uploadLanguageImage( | |||||||||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(profileImageUrl); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| @PostMapping("/chat") | ||||||||||||||||||||||||||||||||||||||||||||
| public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage( | ||||||||||||||||||||||||||||||||||||||||||||
| @RequestParam("files") List<MultipartFile> imageFiles | ||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||
| List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT); | ||||||||||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(chatImageUrls); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain채팅 이미지 다중 업로드 엔드포인트에 인증·밸리데이션을 추가해 주세요.
선택 범위 내에서 적용 가능한 최소 diff입니다(추가 import: org.springframework.http.MediaType, jakarta.validation.constraints.Size, 클래스/메서드에 @validated 필요). - @PostMapping("/chat")
- public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
- @RequestParam("files") List<MultipartFile> imageFiles
- ) {
- List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
+ @PostMapping(value = "/chat", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
+ public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
+ @AuthorizedUser long siteUserId,
+ @RequestParam("files") @jakarta.validation.constraints.Size(min = 1, max = 10) List<MultipartFile> imageFiles
+ ) {
+ // siteUserId는 감사/추적용으로 로깅하거나 업로드 정책에 활용 가능
+ List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
return ResponseEntity.ok(chatImageUrls);
}아래 스크립트로 보안 설정에서 /file/** 경로의 접근 정책을 확인해 주세요(무권한 업로드 방지). 🏁 Script executed: #!/bin/bash
# SecurityFilterChain 내 /file/** 접근 정책 점검
rg -n -C3 -P '(SecurityFilterChain|WebSecurityConfigurerAdapter|@EnableWebSecurity)' --glob '!**/build/**' --glob '!**/out/**'
rg -n -C2 -P '(/file/\*\*|/file/|S3Controller)' --glob '!**/build/**' --glob '!**/out/**'
rg -n -C2 -P '(permitAll|hasRole|authenticated)\(' --glob '!**/build/**' --glob '!**/out/**'Length of output: 4590 인증·밸리데이션 및 보안 설정 강화 제안 아래 4가지 변경 사항을 적용해 주세요:
- @PostMapping("/chat")
- public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
- @RequestParam("files") List<MultipartFile> imageFiles
- ) {
- List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
+ @PostMapping(
+ value = "/chat",
+ consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE
+ )
+ public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
+ @AuthorizedUser long siteUserId,
+ @RequestParam("files")
+ @jakarta.validation.constraints.Size(min = 1, max = 10)
+ List<MultipartFile> imageFiles
+ ) {
+ // siteUserId는 감사/추적 및 업로드 정책에 활용할 수 있습니다.
+ List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
return ResponseEntity.ok(chatImageUrls);
}— 형태로 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이건 비단 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||
| @GetMapping("/s3-url-prefix") | ||||||||||||||||||||||||||||||||||||||||||||
| public ResponseEntity<urlPrefixResponse> getS3UrlPrefix() { | ||||||||||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이게 추가된 히스토리가 어떤거였나요? 제가 기억을 못하는 것일 수도 있습니다..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
디코에 올려놓겠습니다 ! 회의할 때 나왔던 내용인데, 수연 님과 DM으로 결정했었습니다