Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b5c8884
chore: 토픽 주소 변경
whqtker Aug 1, 2025
678ef9d
feat: 메시지 전송 DTO 작성
whqtker Aug 1, 2025
ecf92e3
feat: 메시지 전송 Service 작성
whqtker Aug 1, 2025
4928e0c
feat: 메시지 전송 Controller 작성
whqtker Aug 1, 2025
2e1db36
chore: 메시지 전송에 대한 컨트롤러 어노테이션을 RestController에서 Controller로 변경
whqtker Aug 1, 2025
500b664
chore: WebSocket 초기 연결을 위한 HTTP 핸드셰이크에서 인증을 수행하도록
whqtker Aug 2, 2025
560e13c
fix: 핸드셰이크 후 Principal을 WebSocket 세션에 전달하도록 수정
whqtker Aug 2, 2025
482b594
fix: 컨트롤러 파라미터 인자로 Principal를 받고, 이후 SiteUserDetails에서 siteUserId를 추출…
whqtker Aug 2, 2025
db8f107
fix: DTO를 통해 순환참조 문제 해결
whqtker Aug 2, 2025
5b1304d
chore: 실제 구독 권한 TODO 구현
whqtker Aug 2, 2025
865e520
chore: 코드 리포매팅
whqtker Aug 2, 2025
ab6422f
chore: 미사용 SiteUserPrincipal 제거 외
whqtker Aug 4, 2025
189b987
Merge branch 'develop' into feat/428-chat-with-socket-image
lsy1307 Aug 19, 2025
25c6f42
Merge branch 'develop' of https://github.com/lsy1307/solid-connect-se…
lsy1307 Aug 23, 2025
c92e336
feat: 이미지 업로드를 위한 S3 Controller 및 Service 추가, ImgType수정
lsy1307 Aug 24, 2025
078a28b
feat: DTO 추가, 수정 및 MessageType 추가
lsy1307 Aug 24, 2025
ac2ad8c
feat: Controller, Service 구현
lsy1307 Aug 24, 2025
73b3a0e
feat: Test 코드 추가
lsy1307 Aug 24, 2025
6069a6e
fix: 서브 모듈 커밋해시 수정
lsy1307 Aug 24, 2025
361e971
refactor: addAttachment 메서드 추가
lsy1307 Aug 26, 2025
e9fadc2
refactor: 코드 포매팅
lsy1307 Aug 26, 2025
18a3b4d
refactor: 테스트 코드 컨벤션 수정
lsy1307 Aug 26, 2025
55160b1
refactor: setChatMessage 메서드 수정
lsy1307 Aug 26, 2025
74b40dc
fix: conflict 해결 및 dev merge
lsy1307 Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -42,4 +43,17 @@ public ChatAttachment(boolean isImage, String url, String thumbnailUrl, ChatMess
chatMessage.getChatAttachments().add(this);
}
}

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatAttachment> chatAttachments = new ArrayList<>();

public ChatMessage(String content, long senderId, ChatRoom chatRoom) {
Expand All @@ -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);
}
}
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,
}
Comment on lines +3 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 추가된 히스토리가 어떤거였나요? 제가 기억을 못하는 것일 수도 있습니다..

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디코에 올려놓겠습니다 ! 회의할 때 나왔던 내용인데, 수연 님과 DM으로 결정했었습니다

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL 검증 있으면 좋을 거 같네요 !

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link

@coderabbitai coderabbitai bot Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

메시지 타입 산정 시 ‘첨부 존재 여부’ 대신 ‘이미지 첨부 포함 여부’를 사용해 주세요.

  1. 파일(비이미지) 첨부도 있는 경우, 현재 로직은 IMAGE로 잘못 분류될 수 있습니다.
  2. isImage 플래그를 활용해 타입을 판정해야 확장 시에도 안전합니다.

아래처럼 변경을 제안드립니다(추가 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/chat/dto/ChatMessageSendResponse.java
around lines 16 to 19, the code currently sets MessageType based only on whether
any attachments exist, which misclassifies messages that have non-image
attachments; update the logic to check if any attachment has the isImage flag
true (e.g., use a stream anyMatch on chatMessage.getChatAttachments() to detect
image attachments) and set MessageType.IMAGE only when an image attachment is
present, otherwise fallback to MessageType.TEXT; keep imports unchanged by using
a lambda.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기억이 가물가물한데 현재 저희는 일단 이미지만 작업하는 거 맞나요 ? 그렇다면 이건 추후에 반영해도 될 거 같습니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are existing non-image attachment implementations
echo "=== Checking ChatAttachment entity structure ==="
fd -e java -x grep -l "ChatAttachment" {} \; | head -5 | xargs cat

echo -e "\n=== Checking for existing file upload types beyond images ==="
rg -n "isImage" --type java -C 3

echo -e "\n=== Checking ImgType enum for file type support ==="
cat src/main/java/com/example/solidconnection/s3/domain/ImgType.java

echo -e "\n=== Checking if there are existing file/attachment upload endpoints ==="
rg -n "upload.*file" --type java -C 2

Length of output: 21605


@whqtker 맞습니다! 현재 작업 스코프는 이미지 전송에 집중되어 있네요.

코드를 확인해보니:

  1. 현재 PR 스코프: ChatImageSendRequest를 통한 이미지 전송만 구현
  2. 인프라는 준비됨: ChatAttachmentisImage 필드가 있어서 이미지/비이미지 구분 가능
  3. 현재 구현: sendChatImage에서 isImage=true인 첨부만 생성

PR 설명에서도 "이미지를 제외한 기타 첨부파일 관련 로직은 정상 동작을 확인했고, 추가 작업 예정"이라고 하셨으니, 지금은 이미지만 다루고 있는 게 맞는 것 같습니다.

따라서 제안드린 개선사항은 추후에 비이미지 첨부파일이 실제로 추가될 때 함께 반영하는 게 더 실용적일 것 같아요. 현재로서는 단순히 isEmpty() 체크만으로도 충분하겠네요!

ᕦ(◕‿◕)ᕤ

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
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메시지 전송 실패해도 DB에 트랜잭션 반영이 되는 거 같은데 이건 추후에 롤백 기능 넣어야겠네요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 확인했습니당

Comment on lines +200 to +226
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

1) 트랜잭션 커밋 이후에만 브로드캐스트하도록 변경 + 첨부 생성 시 null 제거 제안

    1. 메시지 브로드캐스트는 커밋 이후(AFTER_COMMIT)에 발생해야 일관성이 보장됩니다. 지금은 @transactional 내부에서 즉시 전송되어 롤백 시 “유령 메시지”가 나갈 수 있습니다.
    1. ChatAttachment 생성자에 chatMessage를 null로 전달한 뒤 addAttachment로 다시 세팅하고 있습니다. optional=false 매핑일 경우 중간 상태가 불안정합니다. 바로 chatMessage를 넘기세요.
    1. 입력 URL에 대해 도메인/프리픽스(chat/images/) 검증과 개수 제한(DoS 방지)을 권장합니다. DTO Bean Validation으로도 가능합니다.
    1. messageType=IMAGE 설정/응답 생성 로직이 어디서 결정되는지 재확인해 주세요. from(chatMessage)에서 첨부 유무로 추론하는지, 혹은 엔티티 필드가 있는지요.

제안 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


반영해야 할 변경 사항

  1. 트랜잭션 커밋 이후에만 메시지를 브로드캐스트하도록 TransactionSynchronizationManager.registerSynchronization 또는 @TransactionalEventListener(phase = AFTER_COMMIT)을 사용해 롤백 시 유령 메시지가 전송되지 않도록 보장해주세요.
  2. ChatAttachment 생성자에 null 대신 chatMessage를 직접 전달해 ChatAttachment(true, imageUrl, thumbnailUrl, chatMessage) 형태로 생성하고, 이후 chatMessage.addAttachment(attachment)로 양방향 연관관계를 설정해 optional=false 매핑의 불안정성을 해소해주세요.
  3. chatImageSendRequest.imageUrls에 도메인·경로 프리픽스(chat/images/) 검증 및 최대 개수 제한을 DTO Bean Validation으로 추가해 URL 무결성과 DoS 방지를 강화해주세요.
  4. ChatMessageSendResponse.from에서 첨부 여부로 MessageType을 추론하는 로직이 의도한 방식인지 재확인해 엔티티 필드로 직접 설정해야 하는지 검토해주세요.


private String generateThumbnailUrl(String originalUrl) {
try {
String fileName = originalUrl.substring(originalUrl.lastIndexOf('/') + 1);

Comment on lines +230 to +231
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확장자 검증이 따로 없는 거 같은데 상관없나요? 사진만 받기로 했던 거 같아서요

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하는 함수 바로 밑으로 옮겨주세요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Copy link

@coderabbitai coderabbitai bot Aug 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

채팅 이미지 다중 업로드 엔드포인트에 인증·밸리데이션을 추가해 주세요.

  1. 인증: 기존 /file/* 엔드포인트의 보안 규칙을 재사용하더라도, 채팅 기능 특성상 업로더 식별이 중요합니다. @Authorizeduser long siteUserId 파라미터를 추가해 호출 주체를 명시적으로 확보하는 것을 권장합니다.
  2. 밸리데이션: 프런트 제약과 맞춰 서버에서도 파일 개수(1~10) 제약을 선언적으로 검증(@SiZe)하고, 비어 있거나 null인 경우 400을 반환해 주세요.
  3. 미디어 타입: consumes = multipart/form-data를 명시하면 클라이언트/게이트웨이 간 협업 오류를 줄일 수 있습니다.

선택 범위 내에서 적용 가능한 최소 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가지 변경 사항을 적용해 주세요:

  1. 인증(Authenticated User)
      1.1 메서드 시그니처에 @AuthorizedUser long siteUserId 파라미터를 추가해 호출 주체를 명시적으로 확보합니다.
      1.2 SecurityConfiguration 내 SecurityFilterChain 설정에서 /file/** 경로에도 .authenticated()를 적용해, 현재 .anyRequest().permitAll()로 노출된 업로드 엔드포인트를 보호합니다.
  2. 요청 검증(Validation)
      2.1 컨트롤러 클래스 또는 메서드에 @Validated를 선언해 밸리데이션을 활성화합니다.
      2.2 @RequestParam("files") 파라미터에 @Size(min = 1, max = 10) (및 필요 시 @NotEmpty)를 붙여, 1~10개 파일만 허용하도록 선언적으로 검증합니다.
  3. 미디어 타입 명시(Media Type)
      3.1 @PostMappingconsumes = MediaType.MULTIPART_FORM_DATA_VALUE를 지정해 클라이언트·게이트웨이 간 오해를 줄입니다.
  4. 최소 적용 예시(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);
     }


스크립트 결과: SecurityFilterChain 설정이

.requestMatchers("/connect/**").authenticated()
.requestMatchers("/admin/**").hasRole(ADMIN.name())
.anyRequest().permitAll()

형태로 /file/** 경로는 인증 없이 허용되고 있습니다. 따라서 메서드 레벨 인증과 보안 필터체인 수정을 함께 적용해 주세요.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@PostMapping("/chat")
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
@RequestParam("files") List<MultipartFile> imageFiles
) {
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
return ResponseEntity.ok(chatImageUrls);
}
@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);
}
🤖 Prompt for AI Agents
In src/main/java/com/example/solidconnection/s3/controller/S3Controller.java
around lines 72-79, the uploadChatImage endpoint is exposed without validation
or authenticated user context; update the controller to require an authenticated
caller and validate inputs by adding an @AuthorizedUser long siteUserId
parameter to the method signature, annotate the controller (or method) with
@Validated, add @Size(min = 1, max = 10) (and @NotEmpty if desired) to the
@RequestParam("files") List<MultipartFile> imageFiles parameter, and specify
consumes = MediaType.MULTIPART_FORM_DATA_VALUE on the @PostMapping;
additionally, modify your SecurityConfiguration SecurityFilterChain to require
authentication for the upload path (e.g., add
.requestMatchers("/file/**").authenticated() and remove/adjust the
.anyRequest().permitAll() exposure); also add necessary imports for MediaType
and validation annotations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 비단 chat 엔드포인트에만 해당하는 건 아니네요 ... 추후 구현하는 것이 좋을 것 같습니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@GetMapping("/s3-url-prefix")
public ResponseEntity<urlPrefixResponse> getS3UrlPrefix() {
return ResponseEntity.ok(new urlPrefixResponse(s3Default, s3Uploaded, cloudFrontDefault, cloudFrontUploaded));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -52,8 +52,8 @@ public class S3Service {
* - 파일에 대한 메타 데이터를 생성한다.
* - 임의의 랜덤한 문자열로 파일 이름을 생성한다.
* - S3에 파일을 업로드한다.
* - 3mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다.
* - 3mb 미만의 파일은 바로 업로드한다.
* - 5mb 이상의 파일은 /origin/ 경로로 업로드하여 lambda 함수로 리사이징 진행한다.
* - 5mb 미만의 파일은 바로 업로드한다.
* */
public UploadedFileUrlResponse uploadFile(MultipartFile multipartFile, ImgType imageFile) {
// 파일 검증
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/secret
Loading
Loading