Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ dependencies {
testImplementation 'org.awaitility:awaitility:4.2.0'

// Etc
implementation platform('software.amazon.awssdk:bom:2.41.4')
implementation 'software.amazon.awssdk:s3'
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.example.solidconnection.mentor.repository.MentorRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
Expand All @@ -39,6 +40,7 @@
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
public class ChatService {
Expand Down Expand Up @@ -240,16 +242,19 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs
ChatRoom chatRoom = chatRoomRepository.findById(roomId)
.orElseThrow(() -> new CustomException(INVALID_CHAT_ROOM_STATE));

ChatMessage chatMessage = new ChatMessage(
"",
senderId,
chatRoom
);
ChatMessage chatMessage = new ChatMessage("", senderId, chatRoom);

// 이미지 판별을 위한 확장자 리스트
List<String> imageExtensions = Arrays.asList("jpg", "jpeg", "png", "webp");
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
Contributor

Choose a reason for hiding this comment

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

이 부분은 enum으로 관리하는 건 어떻게 생각하시나요?


for (String imageUrl : chatImageSendRequest.imageUrls()) {
String thumbnailUrl = generateThumbnailUrl(imageUrl);
String extension = StringUtils.getFilenameExtension(imageUrl);

ChatAttachment attachment = new ChatAttachment(true, imageUrl, thumbnailUrl, null);
boolean isImage = extension != null && imageExtensions.contains(extension.toLowerCase());

String thumbnailUrl = isImage ? generateThumbnailUrl(imageUrl) : null;

ChatAttachment attachment = new ChatAttachment(isImage, imageUrl, thumbnailUrl, null);
Copy link
Contributor

Choose a reason for hiding this comment

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

thumbnailUrl이 null인 경우에 대해 방어 로직이 존재하나요?? 이번 pr 상에서는 안보여서...

(개인적으로 gif나 avif 이미지 파일 확장자도 잘 쓰인다고 생각해서 확인 한 번 해주시면 감사드리겠습니다!)

chatMessage.addAttachment(attachment);
}

Expand All @@ -268,11 +273,9 @@ private String generateThumbnailUrl(String originalUrl) {

String thumbnailFileName = nameWithoutExt + "_thumb" + extension;

String thumbnailUrl = originalUrl.replace("chat/images/", "chat/thumbnails/")
return originalUrl.replace("chat/files/", "chat/thumbnails/")
.replace(fileName, thumbnailFileName);

return thumbnailUrl;

} catch (Exception e) {
return originalUrl;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.example.solidconnection.community.post.dto.PostUpdateRequest;
import com.example.solidconnection.community.post.dto.PostUpdateResponse;
import com.example.solidconnection.community.post.repository.PostRepository;
import com.example.solidconnection.s3.domain.ImgType;
import com.example.solidconnection.s3.domain.UploadType;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
import com.example.solidconnection.s3.service.S3Service;
import com.example.solidconnection.siteuser.domain.SiteUser;
Expand Down Expand Up @@ -88,7 +88,7 @@ private void savePostImages(List<MultipartFile> imageFile, Post post) {
if (imageFile.isEmpty()) {
return;
}
List<UploadedFileUrlResponse> uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, ImgType.COMMUNITY);
List<UploadedFileUrlResponse> uploadedFileUrlResponseList = s3Service.uploadFiles(imageFile, UploadType.COMMUNITY);
for (UploadedFileUrlResponse uploadedFileUrlResponse : uploadedFileUrlResponseList) {
PostImage postImage = new PostImage(uploadedFileUrlResponse.fileUrl());
postImage.setPost(post);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
import com.example.solidconnection.mentor.dto.MentorApplicationRequest;
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
import com.example.solidconnection.s3.domain.ImgType;
import com.example.solidconnection.s3.domain.UploadType;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
import com.example.solidconnection.s3.service.S3Service;
import com.example.solidconnection.siteuser.domain.SiteUser;
Expand Down Expand Up @@ -45,7 +45,7 @@ public void submitMentorApplication(
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
Term term = termRepository.findByName(mentorApplicationRequest.term())
.orElseThrow(() -> new CustomException(TERM_NOT_FOUND));
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF);
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, UploadType.MENTOR_PROOF);
MentorApplication mentorApplication = new MentorApplication(
siteUser.getId(),
mentorApplicationRequest.country(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import com.example.solidconnection.news.dto.NewsCreateRequest;
import com.example.solidconnection.news.dto.NewsUpdateRequest;
import com.example.solidconnection.news.repository.NewsRepository;
import com.example.solidconnection.s3.domain.ImgType;
import com.example.solidconnection.s3.domain.UploadType;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
import com.example.solidconnection.s3.service.S3Service;
import com.example.solidconnection.siteuser.domain.Role;
Expand Down Expand Up @@ -41,7 +41,7 @@ public NewsCommandResponse createNews(long siteUserId, NewsCreateRequest newsCre

private String getImageUrl(MultipartFile imageFile) {
if (imageFile != null && !imageFile.isEmpty()) {
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS);
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadType.NEWS);
return uploadedFile.fileUrl();
}
return newsProperties.defaultThumbnailUrl();
Expand Down Expand Up @@ -73,7 +73,7 @@ private void updateThumbnail(News news, MultipartFile imageFile, Boolean resetTo
deleteCustomImage(news.getThumbnailUrl());
news.updateThumbnailUrl(newsProperties.defaultThumbnailUrl());
} else if (imageFile != null && !imageFile.isEmpty()) {
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, ImgType.NEWS);
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(imageFile, UploadType.NEWS);
deleteCustomImage(news.getThumbnailUrl());
news.updateThumbnailUrl(uploadedFile.fileUrl());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.example.solidconnection.s3.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class AmazonS3Config {
Expand All @@ -21,12 +21,12 @@ public class AmazonS3Config {
private String region;

@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder
.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);

return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.example.solidconnection.s3.controller;

import com.example.solidconnection.common.resolver.AuthorizedUser;
import com.example.solidconnection.s3.domain.ImgType;
import com.example.solidconnection.s3.domain.UploadType;
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
import com.example.solidconnection.s3.dto.urlPrefixResponse;
import com.example.solidconnection.s3.service.S3Service;
Expand Down Expand Up @@ -39,7 +39,7 @@ public class S3Controller {
public ResponseEntity<UploadedFileUrlResponse> uploadPreProfileImage(
@RequestParam("file") MultipartFile imageFile
) {
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE);
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.PROFILE);
return ResponseEntity.ok(profileImageUrl);
}

Expand All @@ -48,7 +48,7 @@ public ResponseEntity<UploadedFileUrlResponse> uploadPostProfileImage(
@AuthorizedUser long siteUserId,
@RequestParam("file") MultipartFile imageFile
) {
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE);
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.PROFILE);
s3Service.deleteExProfile(siteUserId);
return ResponseEntity.ok(profileImageUrl);
}
Expand All @@ -57,23 +57,23 @@ public ResponseEntity<UploadedFileUrlResponse> uploadPostProfileImage(
public ResponseEntity<UploadedFileUrlResponse> uploadGpaImage(
@RequestParam("file") MultipartFile imageFile
) {
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA);
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.GPA);
return ResponseEntity.ok(profileImageUrl);
}

@PostMapping("/language-test")
public ResponseEntity<UploadedFileUrlResponse> uploadLanguageImage(
@RequestParam("file") MultipartFile imageFile
) {
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST);
UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, UploadType.LANGUAGE_TEST);
return ResponseEntity.ok(profileImageUrl);
}

@PostMapping("/chat")
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatImage(
@RequestParam("files") List<MultipartFile> imageFiles
public ResponseEntity<List<UploadedFileUrlResponse>> uploadChatFile(
@RequestParam("files") List<MultipartFile> files
) {
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(imageFiles, ImgType.CHAT);
List<UploadedFileUrlResponse> chatImageUrls = s3Service.uploadFiles(files, UploadType.CHAT);
return ResponseEntity.ok(chatImageUrls);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import lombok.Getter;

@Getter
public enum ImgType {
public enum UploadType {
Copy link
Contributor

Choose a reason for hiding this comment

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

저에게는 UploadType이 직관적으로는 업로드한 파일이 어떤 타입인지, 즉 파일 확장자를 구별하는 걸로 느껴집니다..!
해당 enum의 목적이 어떤 기능(도메인)에서 사용되는 기능인지를 구분하고, 저장하는 파일의 basePath를 결정지어야 하는 책임이 있다고 생각해서 다른 네이밍이 좋을 것 같다는 조심스런 의견을 드립니다 ㅎㅎ

(추신 : 제미나이가 추천한 이름은 UploadPath, UploadCategory였습니다..!)

PROFILE("profile"),
GPA("gpa"),
LANGUAGE_TEST("language"),
COMMUNITY("community"),
NEWS("news"),
CHAT("chat"),
CHAT("chat/files"),
Copy link
Member

Choose a reason for hiding this comment

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

이미 chat 을 통해 업로드된 파일이 있나요 ? 그렇다면 관련해서 데이터 마이그레이션이 필요한 것 같습니다 !

MENTOR_PROOF("mentor-proof"),
;

private final String type;

ImgType(String type) {
UploadType(String type) {
this.type = type;
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,52 @@
package com.example.solidconnection.s3.service;
package com.example.solidconnection.s3.service;
Copy link
Member

Choose a reason for hiding this comment

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

FileUploadService가 전부 한 칸씩 들여쓰기 되었어요 ㅠㅠ


import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.S3_CLIENT_EXCEPTION;
import static com.example.solidconnection.common.exception.ErrorCode.S3_SERVICE_EXCEPTION;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.example.solidconnection.common.exception.CustomException;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import com.example.solidconnection.common.exception.CustomException;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.ObjectCannedACL;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;

@Component
@EnableAsync
@Slf4j
public class FileUploadService {
@Component
@Slf4j
@RequiredArgsConstructor
public class FileUploadService {

private final AmazonS3Client amazonS3;
private final S3Client s3Client;

public FileUploadService(AmazonS3Client amazonS3) {
this.amazonS3 = amazonS3;
}
@Async
public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) {
Comment on lines +27 to +28
Copy link

@coderabbitai coderabbitai bot Jan 14, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# `@EnableAsync` 설정 확인
rg -n "@EnableAsync" --type java

Repository: solid-connection/solid-connect-server

Length of output: 287


🏁 Script executed:

#!/bin/bash
# 두 파일의 내용 확인 - `@EnableAsync` 주변 컨텍스트
echo "=== UpdateViewCountScheduler.java ===" 
sed -n '1,30p' src/main/java/com/example/solidconnection/scheduler/UpdateViewCountScheduler.java

echo -e "\n=== UpdateViewCountService.java ===" 
sed -n '1,30p' src/main/java/com/example/solidconnection/community/post/service/UpdateViewCountService.java

Repository: solid-connection/solid-connect-server

Length of output: 2450


@EnableAsync는 선언되어 있지만, 설정 위치를 개선하세요.

다음과 같은 사항을 확인했습니다:

  1. 현재 상태

    • @EnableAsync가 코드베이스에 존재하므로 uploadFile 메서드의 @Async 애노테이션은 비동기로 작동합니다.
  2. 개선 필요 사항

    • @EnableAsyncUpdateViewCountScheduler (@Component) 와 UpdateViewCountService (@Service) 등 여러 클래스에 분산되어 있습니다.
    • 모범 사례는 하나의 전용 @Configuration 클래스에만 @EnableAsync를 선언하는 것입니다.
  3. 권장 조치

    • 중앙 집중식 설정 클래스를 만들고 그곳에 @EnableAsync를 옮기세요. (예: AsyncConfig.java)
    • 불필요한 중복 선언을 제거하세요.
🤖 Prompt for AI Agents
In `@src/main/java/com/example/solidconnection/s3/service/FileUploadService.java`
around lines 27 - 28, You currently have `@EnableAsync` scattered across
components (e.g., UpdateViewCountScheduler, UpdateViewCountService) while
methods like uploadFile(String bucket, String fileName, MultipartFile
multipartFile) rely on `@Async`; create a single `@Configuration` class (e.g.,
AsyncConfig) and place `@EnableAsync` there, remove all other `@EnableAsync`
annotations from components/services, ensure Spring picks up the config via
component scanning or `@Import` so `@Async` on uploadFile continues to work.

Copy link
Contributor Author

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.

@lsy1307, 알겠습니다! @EnableAsync 설정 통합은 나중에 일괄적으로 정리하는 것이 좋겠네요.

필요하시다면 추적을 위한 GitHub 이슈를 생성해 드릴까요? 이슈에 현재 상황과 개선 방향을 정리해 두면 나중에 작업하실 때 도움이 될 것 같습니다.

try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(fileName)
.contentType(multipartFile.getContentType())
.contentLength(multipartFile.getSize())
.build();

@Async
public void uploadFile(String bucket, String fileName, MultipartFile multipartFile) {
// 메타데이터 생성
String contentType = multipartFile.getContentType();
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
metadata.setContentLength(multipartFile.getSize());
s3Client.putObject(putObjectRequest,
RequestBody.fromInputStream(multipartFile.getInputStream(), multipartFile.getSize()));

try {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
log.info("이미지 업로드 정상적 완료 thread: {}", Thread.currentThread().getName());
} catch (AmazonServiceException e) {
log.error("이미지 업로드 중 s3 서비스 예외 발생 : {}", e.getMessage());
throw new CustomException(S3_SERVICE_EXCEPTION);
} catch (SdkClientException | IOException e) {
log.error("이미지 업로드 중 s3 클라이언트 예외 발생 : {}", e.getMessage());
throw new CustomException(S3_CLIENT_EXCEPTION);
log.info("파일 업로드 정상 완료 thread: {}", Thread.currentThread().getName());
} catch (S3Exception e) {
String errorMessage = (e.awsErrorDetails() != null)
? e.awsErrorDetails().errorMessage()
: e.getMessage();
log.error("S3 서비스 예외 발생 : {}", errorMessage);
throw new CustomException(S3_SERVICE_EXCEPTION);
} catch (SdkException | IOException e) {
log.error("S3 클라이언트 또는 IO 예외 발생 : {}", e.getMessage());
throw new CustomException(S3_CLIENT_EXCEPTION);
}
}
}
}
Loading
Loading