Skip to content
Merged
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
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

implementation 'software.amazon.awssdk:s3:2.28.23'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public enum ErrorCode {
SLOT_FULL(400, "C025", "해당 슬롯이 가득 찼습니다."),
RESERVATION_NOT_FOUND(400, "C026", "예약을 찾을 수 없습니다."),
SLOT_NOT_ACTIVE(400, "C027", "해당 슬롯이 비활성화 되었습니다."),
FILE_ENCODE_FAIL(400, "C028", "파일 인코딩에 실패하였습니다."),
FILE_UPLOAD_FAIL(400, "C028", "파일 업로드에 실패하였습니다."),
RECRUITMENT_NOT_ACTIVE(400, "C029", "모집 기간이 아닙니다."),
NOT_FOUND_PARTICIPANT(400, "C030", "참가자를 찾을 수 없습니다.")
;
Expand Down
35 changes: 35 additions & 0 deletions src/main/java/dmu/dasom/api/global/R2/config/R2Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dmu.dasom.api.global.R2.config;

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;

import java.net.URI;

@Configuration
public class R2Config {

@Value("${cloudflare.r2.endpoint}")
private String endPoint;

@Value("${cloudflare.r2.access-key-id}")
private String accessKeyId;

@Value("${cloudflare.r2.secret-access-key}")
private String secretAccessKey;

@Bean
public S3Client s3Client() {
AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);

return S3Client.builder()
.region(Region.of("auto")) // R2 does not require a specific region
.endpointOverride(URI.create(endPoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
}
}
56 changes: 56 additions & 0 deletions src/main/java/dmu/dasom/api/global/R2/service/R2Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dmu.dasom.api.global.R2.service;

import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.util.UUID;

@Service
public class R2Service {

private final S3Client s3Client;

@Value("${cloudflare.r2.bucket}")
private String bucket;

@Value("${cloudflare.r2.public-url}")
private String publicUrl;

public R2Service(S3Client s3Client) {
this.s3Client = s3Client;
}

public String uploadFile(MultipartFile file) {
String key = UUID.randomUUID() + "_" + file.getOriginalFilename();
try {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(key)
.contentType(file.getContentType())
.build();

s3Client.putObject(request,
RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
} catch (IOException e) {
throw new CustomException(ErrorCode.FILE_UPLOAD_FAIL);
}
return publicUrl + "/" + key;
}

public void deleteFile(String fileUrl) {
String key = fileUrl.replace(publicUrl + "/", "");
DeleteObjectRequest request = DeleteObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
s3Client.deleteObject(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -40,14 +41,14 @@ public class AdminFileController {
))
})
@PostMapping(value = "/upload", consumes = {"multipart/form-data"})
public ResponseEntity<Void> uploadFiles(
public ResponseEntity<List<FileResponseDto>> uploadFiles(
@RequestParam("files") List<MultipartFile> files,
@RequestParam("fileType") FileType fileType,
@RequestParam("targetId") @Min(1) Long targetId
) {
fileService.uploadFiles(files, fileType, targetId);
return ResponseEntity.ok()
.build();
List<FileResponseDto> fileResponseDtos = fileService.uploadFiles(files, fileType, targetId);
return ResponseEntity.status(HttpStatus.CREATED)
.body(fileResponseDtos);
}

@ApiResponses(value = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ public class FileResponseDto {
@NotNull
private Long id;

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

@Schema(description = "인코딩 된 파일", example = "base64encoded")
@Schema(description = "파일 형식", example = "image/png")
@NotNull
private String encodedData; // Base64 인코딩 데이터

private String fileFormat;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public class FileEntity {
private String originalName;

@Lob
@Column(name = "ENCODED_DATA", nullable = false, columnDefinition = "CLOB")
private String encodedData;
@Column(name = "FILE_URL", nullable = false, columnDefinition = "CLOB")
private String fileUrl;

@Column(name = "FILE_FORMAT", nullable = false)
private String fileFormat;
Expand All @@ -41,7 +41,7 @@ public FileResponseDto toResponseDto() {
return FileResponseDto.builder()
.id(id)
.fileFormat(fileFormat)
.encodedData(encodedData)
.encodedData(fileUrl)
.build();
}

Expand Down
53 changes: 26 additions & 27 deletions src/main/java/dmu/dasom/api/global/file/service/FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import dmu.dasom.api.domain.news.entity.NewsEntity;
import dmu.dasom.api.global.R2.service.R2Service;
import dmu.dasom.api.global.file.dto.FileResponseDto;
import dmu.dasom.api.global.file.entity.FileEntity;
import dmu.dasom.api.global.file.enums.FileType;
import dmu.dasom.api.global.file.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand All @@ -23,21 +23,27 @@
public class FileService {

private final FileRepository fileRepository;
private final R2Service r2Service;

// 파일 업로드
public void uploadFiles(List<MultipartFile> files, FileType fileType, Long targetId) {
@Transactional
public List<FileResponseDto> uploadFiles(List<MultipartFile> files, FileType fileType, Long targetId) {
List<FileEntity> filesToEntity = files.stream()
.map(file -> FileEntity.builder()
.originalName(file.getOriginalFilename())
.encodedData(encode(file))
.fileFormat(file.getContentType())
.fileSize(file.getSize())
.fileType(fileType)
.targetId(targetId)
.build())
.toList();

fileRepository.saveAllAndFlush(filesToEntity);
.map(file -> {
String fileUrl = r2Service.uploadFile(file);
return FileEntity.builder()
.originalName(file.getOriginalFilename())
.fileUrl(fileUrl)
.fileFormat(file.getContentType())
.fileSize(file.getSize())
.fileType(fileType)
.targetId(targetId)
.build();
}).toList();
List<FileEntity> savedFiles = fileRepository.saveAll(filesToEntity);
return savedFiles.stream()
.map(FileEntity::toResponseDto)
.collect(Collectors.toList());
}

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

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

if (ObjectUtils.isNotEmpty(files))
fileRepository.deleteAll(files);
if (ObjectUtils.isNotEmpty(files)) {
files.forEach(file -> r2Service.deleteFile(file.getFileUrl()));
}
fileRepository.deleteAll(files);
}

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

Expand Down Expand Up @@ -92,15 +102,4 @@ public Map<Long, FileResponseDto> getFirstFileByTypeAndTargetIds(FileType fileTy
private List<FileEntity> findByFileTypeAndTargetId(FileType fileType, Long targetId) {
return fileRepository.findByFileTypeAndTargetId(fileType, targetId);
}

private String encode(MultipartFile file) {
try {
byte[] bytes = file.getBytes();
return Base64.getEncoder()
.encodeToString(bytes);
} catch (IOException e) {
throw new CustomException(ErrorCode.FILE_ENCODE_FAIL);
}
}

}
9 changes: 8 additions & 1 deletion src/main/resources/application-credentials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,11 @@ google:
file:
path: ${GOOGLE_CREDENTIALS_PATH}
spreadsheet:
id: ${GOOGLE_SPREADSHEET_ID}
id: ${GOOGLE_SPREADSHEET_ID}
cloudflare:
r2:
endpoint: ${CLOUDFLARE_R2_ENDPOINT}
bucket: ${CLOUDFLARE_R2_BUCKET}
access-key-id: ${CLOUDFLARE_R2_ACCESS_KEY_ID}
secret-access-key: ${CLOUDFLARE_R2_SECRET_ACCESS_KEY}
public-url: ${CLOUDFLARE_R2_PUBLIC_URL}
Loading