Skip to content

Commit 4353d00

Browse files
hodoonysw789
authored andcommitted
refactor: 이미지 저장 로직 수정(DASOMBE-19)
1 parent ead2ebc commit 4353d00

File tree

9 files changed

+140
-41
lines changed

9 files changed

+140
-41
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ dependencies {
5757
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
5858
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
5959
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
60+
61+
implementation 'software.amazon.awssdk:s3:2.28.23'
6062
}
6163

6264
tasks.named('test') {

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public enum ErrorCode {
3434
SLOT_FULL(400, "C025", "해당 슬롯이 가득 찼습니다."),
3535
RESERVATION_NOT_FOUND(400, "C026", "예약을 찾을 수 없습니다."),
3636
SLOT_NOT_ACTIVE(400, "C027", "해당 슬롯이 비활성화 되었습니다."),
37-
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다."),
37+
FILE_UPLOAD_FAIL(400, "C028", "파일 업로드에 실패하였습니다."),
3838
RECRUITMENT_NOT_ACTIVE(400, "C029", "모집 기간이 아닙니다."),
3939
NOT_FOUND_PARTICIPANT(400, "C030", "참가자를 찾을 수 없습니다."),
4040
EXECUTIVE_NOT_FOUND(400, "C031", "임원진을 찾을 수 없습니다."),
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package dmu.dasom.api.global.R2.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
7+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
8+
import software.amazon.awssdk.regions.Region;
9+
import software.amazon.awssdk.services.s3.S3Client;
10+
11+
import java.net.URI;
12+
13+
@Configuration
14+
public class R2Config {
15+
16+
@Value("${cloudflare.r2.endpoint}")
17+
private String endPoint;
18+
19+
@Value("${cloudflare.r2.access-key-id}")
20+
private String accessKeyId;
21+
22+
@Value("${cloudflare.r2.secret-access-key}")
23+
private String secretAccessKey;
24+
25+
@Bean
26+
public S3Client s3Client() {
27+
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
28+
29+
return S3Client.builder()
30+
.region(Region.of("auto")) // R2 does not require a specific region
31+
.endpointOverride(URI.create(endPoint))
32+
.credentialsProvider(StaticCredentialsProvider.create(credentials))
33+
.build();
34+
}
35+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package dmu.dasom.api.global.R2.service;
2+
3+
import dmu.dasom.api.domain.common.exception.CustomException;
4+
import dmu.dasom.api.domain.common.exception.ErrorCode;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.web.multipart.MultipartFile;
8+
import software.amazon.awssdk.core.sync.RequestBody;
9+
import software.amazon.awssdk.services.s3.S3Client;
10+
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
11+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
12+
13+
import java.io.IOException;
14+
import java.util.UUID;
15+
16+
@Service
17+
public class R2Service {
18+
19+
private final S3Client s3Client;
20+
21+
@Value("${cloudflare.r2.bucket}")
22+
private String bucket;
23+
24+
@Value("${cloudflare.r2.public-url}")
25+
private String publicUrl;
26+
27+
public R2Service(S3Client s3Client) {
28+
this.s3Client = s3Client;
29+
}
30+
31+
public String uploadFile(MultipartFile file) {
32+
String key = UUID.randomUUID() + "_" + file.getOriginalFilename();
33+
try {
34+
PutObjectRequest request = PutObjectRequest.builder()
35+
.bucket(bucket)
36+
.key(key)
37+
.contentType(file.getContentType())
38+
.build();
39+
40+
s3Client.putObject(request,
41+
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
42+
} catch (IOException e) {
43+
throw new CustomException(ErrorCode.FILE_UPLOAD_FAIL);
44+
}
45+
return publicUrl + "/" + key;
46+
}
47+
48+
public void deleteFile(String fileUrl) {
49+
String key = fileUrl.replace(publicUrl + "/", "");
50+
DeleteObjectRequest request = DeleteObjectRequest.builder()
51+
.bucket(bucket)
52+
.key(key)
53+
.build();
54+
s3Client.deleteObject(request);
55+
}
56+
}

src/main/java/dmu/dasom/api/global/admin/controller/AdminFileController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import io.swagger.v3.oas.annotations.tags.Tag;
1313
import jakarta.validation.constraints.Min;
1414
import lombok.RequiredArgsConstructor;
15+
import org.springframework.http.HttpStatus;
1516
import org.springframework.http.ResponseEntity;
1617
import org.springframework.web.bind.annotation.*;
1718
import org.springframework.web.multipart.MultipartFile;
@@ -40,14 +41,14 @@ public class AdminFileController {
4041
))
4142
})
4243
@PostMapping(value = "/upload", consumes = {"multipart/form-data"})
43-
public ResponseEntity<Void> uploadFiles(
44+
public ResponseEntity<List<FileResponseDto>> uploadFiles(
4445
@RequestParam("files") List<MultipartFile> files,
4546
@RequestParam("fileType") FileType fileType,
4647
@RequestParam("targetId") @Min(1) Long targetId
4748
) {
48-
fileService.uploadFiles(files, fileType, targetId);
49-
return ResponseEntity.ok()
50-
.build();
49+
List<FileResponseDto> fileResponseDtos = fileService.uploadFiles(files, fileType, targetId);
50+
return ResponseEntity.status(HttpStatus.CREATED)
51+
.body(fileResponseDtos);
5152
}
5253

5354
@ApiResponses(value = {

src/main/java/dmu/dasom/api/global/file/dto/FileResponseDto.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ public class FileResponseDto {
1515
@NotNull
1616
private Long id;
1717

18-
@Schema(description = "파일 형식", example = "image/png")
18+
@Schema(description = "파일 URL", example = "url")
1919
@NotNull
20-
private String fileFormat;
20+
private String encodedData; // r2에 저장된 파일의 URL
2121

22-
@Schema(description = "인코딩 된 파일", example = "base64encoded")
22+
@Schema(description = "파일 형식", example = "image/png")
2323
@NotNull
24-
private String encodedData; // Base64 인코딩 데이터
25-
24+
private String fileFormat;
2625
}

src/main/java/dmu/dasom/api/global/file/entity/FileEntity.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public class FileEntity {
2121
private String originalName;
2222

2323
@Lob
24-
@Column(name = "ENCODED_DATA", nullable = false, columnDefinition = "CLOB")
25-
private String encodedData;
24+
@Column(name = "FILE_URL", nullable = false, columnDefinition = "CLOB")
25+
private String fileUrl;
2626

2727
@Column(name = "FILE_FORMAT", nullable = false)
2828
private String fileFormat;
@@ -41,7 +41,7 @@ public FileResponseDto toResponseDto() {
4141
return FileResponseDto.builder()
4242
.id(id)
4343
.fileFormat(fileFormat)
44-
.encodedData(encodedData)
44+
.encodedData(fileUrl)
4545
.build();
4646
}
4747

src/main/java/dmu/dasom/api/global/file/service/FileService.java

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
import dmu.dasom.api.domain.common.exception.CustomException;
44
import dmu.dasom.api.domain.common.exception.ErrorCode;
55
import dmu.dasom.api.domain.news.entity.NewsEntity;
6+
import dmu.dasom.api.global.R2.service.R2Service;
67
import dmu.dasom.api.global.file.dto.FileResponseDto;
78
import dmu.dasom.api.global.file.entity.FileEntity;
89
import dmu.dasom.api.global.file.enums.FileType;
910
import dmu.dasom.api.global.file.repository.FileRepository;
1011
import lombok.RequiredArgsConstructor;
1112
import org.apache.commons.lang3.ObjectUtils;
1213
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
1315
import org.springframework.web.multipart.MultipartFile;
1416

15-
import java.io.IOException;
16-
import java.util.Base64;
1717
import java.util.List;
1818
import java.util.Map;
1919
import java.util.stream.Collectors;
@@ -23,21 +23,27 @@
2323
public class FileService {
2424

2525
private final FileRepository fileRepository;
26+
private final R2Service r2Service;
2627

2728
// 파일 업로드
28-
public void uploadFiles(List<MultipartFile> files, FileType fileType, Long targetId) {
29+
@Transactional
30+
public List<FileResponseDto> uploadFiles(List<MultipartFile> files, FileType fileType, Long targetId) {
2931
List<FileEntity> filesToEntity = files.stream()
30-
.map(file -> FileEntity.builder()
31-
.originalName(file.getOriginalFilename())
32-
.encodedData(encode(file))
33-
.fileFormat(file.getContentType())
34-
.fileSize(file.getSize())
35-
.fileType(fileType)
36-
.targetId(targetId)
37-
.build())
38-
.toList();
39-
40-
fileRepository.saveAllAndFlush(filesToEntity);
32+
.map(file -> {
33+
String fileUrl = r2Service.uploadFile(file);
34+
return FileEntity.builder()
35+
.originalName(file.getOriginalFilename())
36+
.fileUrl(fileUrl)
37+
.fileFormat(file.getContentType())
38+
.fileSize(file.getSize())
39+
.fileType(fileType)
40+
.targetId(targetId)
41+
.build();
42+
}).toList();
43+
List<FileEntity> savedFiles = fileRepository.saveAll(filesToEntity);
44+
return savedFiles.stream()
45+
.map(FileEntity::toResponseDto)
46+
.collect(Collectors.toList());
4147
}
4248

4349
// 단일 파일 조회
@@ -56,13 +62,17 @@ public List<FileResponseDto> getFilesByTypeAndTargetId(FileType fileType, Long t
5662
.toList();
5763
}
5864

65+
@Transactional
5966
public void deleteFilesByTypeAndTargetId(FileType fileType, Long targetId) {
6067
List<FileEntity> files = findByFileTypeAndTargetId(fileType, targetId);
6168

62-
if (ObjectUtils.isNotEmpty(files))
63-
fileRepository.deleteAll(files);
69+
if (ObjectUtils.isNotEmpty(files)) {
70+
files.forEach(file -> r2Service.deleteFile(file.getFileUrl()));
71+
}
72+
fileRepository.deleteAll(files);
6473
}
6574

75+
@Transactional
6676
public void deleteFilesById(NewsEntity news, List<Long> fileIds) {
6777
List<FileEntity> files = fileRepository.findAllById(fileIds);
6878

@@ -92,15 +102,4 @@ public Map<Long, FileResponseDto> getFirstFileByTypeAndTargetIds(FileType fileTy
92102
private List<FileEntity> findByFileTypeAndTargetId(FileType fileType, Long targetId) {
93103
return fileRepository.findByFileTypeAndTargetId(fileType, targetId);
94104
}
95-
96-
private String encode(MultipartFile file) {
97-
try {
98-
byte[] bytes = file.getBytes();
99-
return Base64.getEncoder()
100-
.encodeToString(bytes);
101-
} catch (IOException e) {
102-
throw new CustomException(ErrorCode.FILE_ENCODE_FAIL);
103-
}
104-
}
105-
106105
}

src/main/resources/application-credentials.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,11 @@ google:
3535
file:
3636
path: ${GOOGLE_CREDENTIALS_PATH}
3737
spreadsheet:
38-
id: ${GOOGLE_SPREADSHEET_ID}
38+
id: ${GOOGLE_SPREADSHEET_ID}
39+
cloudflare:
40+
r2:
41+
endpoint: ${CLOUDFLARE_R2_ENDPOINT}
42+
bucket: ${CLOUDFLARE_R2_BUCKET}
43+
access-key-id: ${CLOUDFLARE_R2_ACCESS_KEY_ID}
44+
secret-access-key: ${CLOUDFLARE_R2_SECRET_ACCESS_KEY}
45+
public-url: ${CLOUDFLARE_R2_PUBLIC_URL}

0 commit comments

Comments
 (0)