Skip to content

Commit 1ffe461

Browse files
authored
Merge pull request #314 from prgrms-web-devcourse-final-project/feature/EA3-210-aws-s3
[EA3-210] feature: aws s3 파일 업로드 기능 구현
2 parents 6291fe9 + a69a209 commit 1ffe461

File tree

10 files changed

+96
-59
lines changed

10 files changed

+96
-59
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM openjdk:21-ea-oraclelinux8
1+
FROM openjdk:21-jdk-slim
22

33
# 타임존 설정
44
RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ dependencies {
9494

9595
// 이메일 전송 의존성
9696
implementation 'org.springframework.boot:spring-boot-starter-mail'
97+
98+
// s3
99+
implementation 'software.amazon.awssdk:s3:2.25.61'
97100
}
98101

99102
tasks.named('test') {

src/main/java/grep/neogulcoder/domain/timevote/repository/TimeVoteStatRepository.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public interface TimeVoteStatRepository extends JpaRepository<TimeVoteStat, Long
1515
@Query("UPDATE TimeVoteStat s SET s.activated = false WHERE s.period.studyId = :studyId")
1616
void deactivateAllByPeriod_StudyId(@Param("studyId") Long studyId);
1717

18-
1918
@Modifying
2019
@Query("UPDATE TimeVoteStat s SET s.activated = false WHERE s.period = :period")
2120
void softDeleteByPeriod(@Param("period") TimeVotePeriod period);
@@ -25,12 +24,10 @@ public interface TimeVoteStatRepository extends JpaRepository<TimeVoteStat, Long
2524

2625
@Modifying(clearAutomatically = true)
2726
@Query(value = """
28-
2927
INSERT INTO time_vote_stat (period_id, time_slot, vote_count, activated, created_date, modified_date)
3028
VALUES (:periodId, :timeSlot, :count, true, now(), now())
31-
ON CONFLICT (period_id, time_slot)
32-
DO UPDATE SET
33-
vote_count = time_vote_stat.vote_count + EXCLUDED.vote_count,
29+
ON DUPLICATE KEY UPDATE
30+
vote_count = vote_count + VALUES(vote_count),
3431
modified_date = now(),
3532
activated = true
3633
""", nativeQuery = true)
@@ -44,9 +41,8 @@ void upsertVoteStat(
4441
@Query(value = """
4542
INSERT INTO time_vote_stat (period_id, time_slot, vote_count, activated, created_date, modified_date)
4643
VALUES (:periodId, :timeSlot, :voteCount, true, now(), now())
47-
ON CONFLICT (period_id, time_slot)
48-
DO UPDATE SET
49-
vote_count = EXCLUDED.vote_count,
44+
ON DUPLICATE KEY UPDATE
45+
vote_count = VALUES(vote_count),
5046
modified_date = now(),
5147
activated = true
5248
""", nativeQuery = true)

src/main/java/grep/neogulcoder/domain/users/service/UserService.java

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,27 @@
1212
import grep.neogulcoder.domain.users.controller.dto.response.AllUserResponse;
1313
import grep.neogulcoder.domain.users.controller.dto.response.UserResponse;
1414
import grep.neogulcoder.domain.users.entity.User;
15-
import grep.neogulcoder.domain.users.exception.EmailDuplicationException;
16-
import grep.neogulcoder.domain.users.exception.NicknameDuplicatedException;
17-
import grep.neogulcoder.domain.users.exception.NotVerifiedEmailException;
18-
import grep.neogulcoder.domain.users.exception.PasswordNotMatchException;
19-
import grep.neogulcoder.domain.users.exception.UserNotFoundException;
15+
import grep.neogulcoder.domain.users.exception.*;
2016
import grep.neogulcoder.domain.users.exception.code.UserErrorCode;
2117
import grep.neogulcoder.domain.users.repository.UserRepository;
18+
import grep.neogulcoder.global.utils.upload.AbstractFileManager;
2219
import grep.neogulcoder.global.utils.upload.FileUploadResponse;
2320
import grep.neogulcoder.global.utils.upload.FileUsageType;
24-
import grep.neogulcoder.global.utils.upload.uploader.GcpFileUploader;
25-
import grep.neogulcoder.global.utils.upload.uploader.LocalFileUploader;
26-
import java.io.IOException;
27-
import java.util.List;
2821
import lombok.RequiredArgsConstructor;
29-
import org.springframework.beans.factory.annotation.Autowired;
30-
import org.springframework.core.env.Environment;
3122
import org.springframework.security.crypto.password.PasswordEncoder;
3223
import org.springframework.stereotype.Service;
3324
import org.springframework.transaction.annotation.Transactional;
3425
import org.springframework.web.multipart.MultipartFile;
3526

27+
import java.io.IOException;
28+
import java.util.List;
29+
3630
@Transactional
3731
@Service
3832
@RequiredArgsConstructor
3933
public class UserService {
4034

35+
private final AbstractFileManager fileUploader;
4136
private final UserRepository userRepository;
4237
private final PasswordEncoder passwordEncoder;
4338
private final PrTemplateRepository prTemplateRepository;
@@ -48,16 +43,6 @@ public class UserService {
4843
private final BuddyEnergyService buddyEnergyService;
4944
private final EmailVerificationService verificationService;
5045

51-
@Autowired(required = false)
52-
private GcpFileUploader gcpFileUploader;
53-
54-
@Autowired(required = false)
55-
private LocalFileUploader localFileUploader;
56-
57-
@Autowired
58-
private Environment environment;
59-
60-
6146
@Transactional(readOnly = true)
6247
public User get(Long id) {
6348
return findUserById(id);
@@ -94,18 +79,9 @@ public void updateProfile(Long userId, String nickname, MultipartFile profileIma
9479
throws IOException {
9580

9681
User user = findUserById(userId);
97-
9882
String validatedNickname = validateUpdateNickname(user, nickname);
9983

100-
String uploadedImageUrl;
101-
if (isProfileImgExists(profileImage)) {
102-
FileUploadResponse response = isProductionEnvironment()
103-
? gcpFileUploader.upload(profileImage, userId, FileUsageType.PROFILE, userId)
104-
: localFileUploader.upload(profileImage, userId, FileUsageType.PROFILE, userId);
105-
uploadedImageUrl = response.getFileUrl();
106-
} else {
107-
uploadedImageUrl = user.getProfileImageUrl();
108-
}
84+
String uploadedImageUrl = updateImageUrl(userId, profileImage, user.getProfileImageUrl());
10985

11086
user.updateProfile(validatedNickname, uploadedImageUrl);
11187
}
@@ -252,20 +228,15 @@ private boolean isNotMatchPasswordCheck(String password, String passwordCheck) {
252228
return !password.equals(passwordCheck);
253229
}
254230

255-
private boolean isProductionEnvironment() {
256-
for (String profile : environment.getActiveProfiles()) {
257-
if ("prod".equals(profile)) {
258-
return true;
259-
}
231+
private String updateImageUrl(Long userId, MultipartFile image, String originalImageUrl) throws IOException {
232+
if (isImgExists(image)) {
233+
FileUploadResponse response = fileUploader.upload(image, userId, FileUsageType.PROFILE, userId);
234+
return response.getFileUrl();
260235
}
261-
return false;
236+
return originalImageUrl;
262237
}
263238

264-
private boolean isProfileImgExists(MultipartFile profileImage) {
265-
return profileImage != null && !profileImage.isEmpty();
239+
private boolean isImgExists(MultipartFile image) {
240+
return image != null && !image.isEmpty();
266241
}
267242
}
268-
269-
270-
271-

src/main/java/grep/neogulcoder/global/config/WebConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) {
3333
@Override
3434
public void addCorsMappings(CorsRegistry registry) {
3535
registry.addMapping("/**")
36-
.allowedOriginPatterns(frontServer, "http://localhost:3000", "https://wibby.cedartodo.uk")
36+
.allowedOriginPatterns(frontServer, "http://localhost:3000", "https://wibby.cedartodo.uk", "https://www.wibby.o-r.kr")
3737
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
3838
.allowedHeaders("*")
3939
.allowCredentials(true)

src/main/java/grep/neogulcoder/global/utils/upload/AbstractFileManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ protected String generateRenameFileName(String originFileName) {
9191

9292
// 전체 저장 경로(폴더+파일명)를 반환
9393
protected String buildFullPath(String savePath, String renameFileName) {
94-
return savePath + "/" + renameFileName;
94+
return savePath + renameFileName;
9595
}
9696

9797
// MIME 타입 허용 목록 (이미지 파일만 허용)

src/main/java/grep/neogulcoder/global/utils/upload/exception/FileUploadErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum FileUploadErrorCode implements ErrorCode {
1414
FILE_NOT_FOUND("UPLOAD_005", HttpStatus.NOT_FOUND, "파일을 찾을 수 없습니다."),
1515

1616
GCP_UPLOAD_FAIL("UPLOAD_GCP_001", HttpStatus.INTERNAL_SERVER_ERROR, "GCP 업로드 중 오류가 발생했습니다."),
17+
AWS_UPLOAD_FAIL("UPLOAD_AWS_001", HttpStatus.INTERNAL_SERVER_ERROR, "AWS 업로드 중 오류가 발생했습니다."),
1718
LOCAL_UPLOAD_FAIL("UPLOAD_LOCAL_001", HttpStatus.INTERNAL_SERVER_ERROR, "로컬 업로드 중 오류가 발생했습니다.");
1819

1920
private final String code;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package grep.neogulcoder.global.utils.upload.uploader;
2+
3+
import grep.neogulcoder.global.utils.upload.AbstractFileManager;
4+
import grep.neogulcoder.global.utils.upload.exception.FileUploadErrorCode;
5+
import grep.neogulcoder.global.utils.upload.exception.FileUploadException;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.context.annotation.Profile;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.multipart.MultipartFile;
11+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
12+
import software.amazon.awssdk.core.sync.RequestBody;
13+
import software.amazon.awssdk.regions.Region;
14+
import software.amazon.awssdk.services.s3.S3Client;
15+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
16+
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
20+
@Slf4j
21+
@Component
22+
@Profile("prod-aws")
23+
public class AwsFileUploader extends AbstractFileManager {
24+
25+
@Value("${cloud.aws.s3.bucket}")
26+
private String bucket;
27+
28+
@Value("${cloud.aws.region}")
29+
private String region;
30+
31+
private static final String STORAGE_BASE_URL = "https://%s.s3.%s.amazonaws.com/%s";
32+
33+
private S3Client createS3Client() {
34+
return S3Client.builder()
35+
.region(Region.of(region))
36+
.credentialsProvider(DefaultCredentialsProvider.create())
37+
.build();
38+
}
39+
40+
@Override
41+
protected void uploadFile(MultipartFile file, String fullPath) {
42+
try (S3Client s3 = createS3Client();
43+
InputStream inputStream = file.getInputStream()) {
44+
45+
PutObjectRequest putRequest = PutObjectRequest.builder()
46+
.bucket(bucket)
47+
.key(fullPath)
48+
.contentType(file.getContentType())
49+
.build();
50+
51+
s3.putObject(putRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
52+
53+
} catch (IOException e) {
54+
log.error("AWS S3 업로드 실패 - 원본 파일명: {}", file.getOriginalFilename(), e);
55+
throw new FileUploadException(FileUploadErrorCode.AWS_UPLOAD_FAIL, e);
56+
}
57+
}
58+
59+
@Override
60+
protected String generateFileUrl(String savePath, String renameFileName) {
61+
String key = buildFullPath(savePath, renameFileName);
62+
return String.format(STORAGE_BASE_URL, bucket, region, key);
63+
}
64+
}

src/main/java/grep/neogulcoder/global/utils/upload/uploader/GcpFileUploader.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import org.springframework.web.multipart.MultipartFile;
1717

1818
@Component
19-
@Profile("prod")
19+
@Profile("prod-gcp")
2020
@Slf4j
2121
public class GcpFileUploader extends AbstractFileManager {
2222

@@ -27,7 +27,7 @@ public class GcpFileUploader extends AbstractFileManager {
2727

2828
// GCP Cloud Storage 에 파일을 업로드
2929
@Override
30-
protected void uploadFile(MultipartFile file, String fullPath) throws IOException {
30+
protected void uploadFile(MultipartFile file, String fullPath) {
3131
try (InputStream inputStream = file.getInputStream()) {
3232
Storage storage = StorageOptions.getDefaultInstance().getService();
3333
BlobId blobId = BlobId.of(bucket, fullPath);
@@ -45,6 +45,7 @@ protected void uploadFile(MultipartFile file, String fullPath) throws IOExceptio
4545
// 전체 파일 URL (https://storage.googleapis.com/bucket/경로/파일명)
4646
@Override
4747
public String generateFileUrl(String savePath, String renameFileName) {
48-
return STORAGE_BASE_URL + bucket + "/" + savePath + "/" + renameFileName;
48+
String key = buildFullPath(savePath, renameFileName);
49+
return STORAGE_BASE_URL + bucket + key;
4950
}
5051
}

src/main/java/grep/neogulcoder/global/utils/upload/uploader/LocalFileUploader.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class LocalFileUploader extends AbstractFileManager {
1919

2020
// 로컬 파일 시스템에 파일을 업로드
2121
@Override
22-
protected void uploadFile(MultipartFile file, String fullPath) throws IOException {
22+
protected void uploadFile(MultipartFile file, String fullPath) {
2323
File targetFile = new File(filePath + fullPath);
2424
File parentDir = targetFile.getParentFile();
2525

@@ -37,6 +37,7 @@ protected void uploadFile(MultipartFile file, String fullPath) throws IOExceptio
3737
// 업로드된 파일의 접근 URL 을 생성
3838
@Override
3939
public String generateFileUrl(String savePath, String renameFileName) {
40-
return "/upload/" + savePath + renameFileName;
40+
String key = buildFullPath(savePath, renameFileName);
41+
return "/upload/" + key;
4142
}
4243
}

0 commit comments

Comments
 (0)