diff --git a/jobis-application/src/main/java/team/retum/jobis/common/util/FileUtil.java b/jobis-application/src/main/java/team/retum/jobis/common/util/FileUtil.java new file mode 100644 index 000000000..12d77439a --- /dev/null +++ b/jobis-application/src/main/java/team/retum/jobis/common/util/FileUtil.java @@ -0,0 +1,27 @@ +package team.retum.jobis.common.util; + +import team.retum.jobis.domain.file.exception.InvalidExtensionException; +import team.retum.jobis.domain.file.model.FileType; + +import java.util.UUID; + +import static team.retum.jobis.domain.file.model.FileType.EXTENSION_FILE; +import static team.retum.jobis.domain.file.model.FileType.LOGO_IMAGE; + +public class FileUtil { + + public static String generateFullFileName(FileType fileType, String fileName) { + String extension = fileName.substring(fileName.lastIndexOf(".")); + + boolean isValid = switch (fileType) { + case LOGO_IMAGE -> LOGO_IMAGE.validExtensions.contains(extension); + case EXTENSION_FILE -> EXTENSION_FILE.validExtensions.contains(extension); + }; + + if (!isValid) { + throw InvalidExtensionException.EXCEPTION; + } + + return fileType.name() + "/" + UUID.randomUUID() + "-" + fileName; + } +} diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/application/usecase/QueryEmploymentCountUseCase.java b/jobis-application/src/main/java/team/retum/jobis/domain/application/usecase/QueryEmploymentCountUseCase.java index afc297bc6..2da00624d 100644 --- a/jobis-application/src/main/java/team/retum/jobis/domain/application/usecase/QueryEmploymentCountUseCase.java +++ b/jobis-application/src/main/java/team/retum/jobis/domain/application/usecase/QueryEmploymentCountUseCase.java @@ -3,8 +3,6 @@ import lombok.RequiredArgsConstructor; import team.retum.jobis.common.annotation.ReadOnlyUseCase; import team.retum.jobis.domain.application.dto.response.QueryEmploymentCountResponse; -import team.retum.jobis.domain.application.model.ApplicationStatus; -import team.retum.jobis.domain.application.spi.QueryApplicationPort; import team.retum.jobis.domain.student.spi.QueryStudentPort; import java.time.Year; diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/CreateFileUploadUrlRequest.java b/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/CreateFileUploadUrlRequest.java new file mode 100644 index 000000000..f4afdc469 --- /dev/null +++ b/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/CreateFileUploadUrlRequest.java @@ -0,0 +1,21 @@ +package team.retum.jobis.domain.file.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import team.retum.jobis.domain.file.model.FileType; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class CreateFileUploadUrlRequest { + + private final List files; + + @Getter + @AllArgsConstructor + public static class FileRequest { + private final FileType type; + private final String fileName; + } +} diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/response/CreateFileUploadUrlResponse.java b/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/response/CreateFileUploadUrlResponse.java new file mode 100644 index 000000000..d64191f14 --- /dev/null +++ b/jobis-application/src/main/java/team/retum/jobis/domain/file/dto/response/CreateFileUploadUrlResponse.java @@ -0,0 +1,19 @@ +package team.retum.jobis.domain.file.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class CreateFileUploadUrlResponse { + private final List urls; + + @Getter + @AllArgsConstructor + public static class UrlResponse { + private final String filePath; + private final String preSignedUrl; + } +} diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/file/spi/FilePort.java b/jobis-application/src/main/java/team/retum/jobis/domain/file/spi/FilePort.java index 851660332..b70efd0cf 100644 --- a/jobis-application/src/main/java/team/retum/jobis/domain/file/spi/FilePort.java +++ b/jobis-application/src/main/java/team/retum/jobis/domain/file/spi/FilePort.java @@ -1,9 +1,8 @@ package team.retum.jobis.domain.file.spi; -import team.retum.jobis.domain.file.model.FileType; - import java.io.File; public interface FilePort { - void uploadFile(File file, String fileName, FileType fileType); + void uploadFile(File file, String fileName); + String generateFileUploadUrl(String fullFileName); } diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/CreateFileUploadUrlUseCase.java b/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/CreateFileUploadUrlUseCase.java new file mode 100644 index 000000000..696b050ea --- /dev/null +++ b/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/CreateFileUploadUrlUseCase.java @@ -0,0 +1,32 @@ +package team.retum.jobis.domain.file.usecase; + +import lombok.RequiredArgsConstructor; +import team.retum.jobis.common.annotation.Service; +import team.retum.jobis.common.util.FileUtil; +import team.retum.jobis.domain.file.dto.CreateFileUploadUrlRequest; +import team.retum.jobis.domain.file.dto.response.CreateFileUploadUrlResponse; +import team.retum.jobis.domain.file.dto.response.CreateFileUploadUrlResponse.UrlResponse; +import team.retum.jobis.domain.file.spi.FilePort; + +@RequiredArgsConstructor +@Service +public class CreateFileUploadUrlUseCase { + + private final FilePort filePort; + + public CreateFileUploadUrlResponse execute(CreateFileUploadUrlRequest request) { + return new CreateFileUploadUrlResponse( + request.getFiles().stream() + .map( + file -> { + String fullFileName = FileUtil.generateFullFileName(file.getType(), file.getFileName()); + String url = filePort.generateFileUploadUrl(fullFileName); + return new UrlResponse( + fullFileName, + url + ); + } + ).toList() + ); + } +} diff --git a/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/FileUploadUseCase.java b/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/FileUploadUseCase.java index 0aaa5bf28..3868a5739 100644 --- a/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/FileUploadUseCase.java +++ b/jobis-application/src/main/java/team/retum/jobis/domain/file/usecase/FileUploadUseCase.java @@ -2,18 +2,14 @@ import lombok.RequiredArgsConstructor; import team.retum.jobis.common.annotation.UseCase; +import team.retum.jobis.common.util.FileUtil; import team.retum.jobis.domain.file.dto.response.FileUploadResponse; -import team.retum.jobis.domain.file.exception.InvalidExtensionException; import team.retum.jobis.domain.file.model.FileType; import team.retum.jobis.domain.file.spi.FilePort; import java.io.File; import java.util.List; import java.util.Objects; -import java.util.UUID; - -import static team.retum.jobis.domain.file.model.FileType.EXTENSION_FILE; -import static team.retum.jobis.domain.file.model.FileType.LOGO_IMAGE; @RequiredArgsConstructor @UseCase @@ -25,9 +21,8 @@ public FileUploadResponse execute(List files, FileType fileType) { List fileUrls = files.stream().filter(Objects::nonNull) .map( file -> { - String fileName = fileType + "/" + UUID.randomUUID() + "-" + file.getName(); - validateExtension(fileName, fileType); - filePort.uploadFile(file, fileName, fileType); + String fileName = FileUtil.generateFullFileName(fileType, file.getName()); + filePort.uploadFile(file, fileName); return fileName.replace(" ", "+"); } @@ -36,16 +31,5 @@ public FileUploadResponse execute(List files, FileType fileType) { return new FileUploadResponse(fileUrls); } - private void validateExtension(String fileName, FileType fileType) { - String extension = fileName.substring(fileName.lastIndexOf(".")); - - boolean isValid = switch (fileType) { - case LOGO_IMAGE -> LOGO_IMAGE.validExtensions.contains(extension); - case EXTENSION_FILE -> EXTENSION_FILE.validExtensions.contains(extension); - }; - if (!isValid) { - throw InvalidExtensionException.EXCEPTION; - } - } } diff --git a/jobis-infrastructure/src/main/java/team/retum/jobis/domain/application/presentation/ApplicationWebAdapter.java b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/application/presentation/ApplicationWebAdapter.java index c45ca56b2..d937f30d3 100644 --- a/jobis-infrastructure/src/main/java/team/retum/jobis/domain/application/presentation/ApplicationWebAdapter.java +++ b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/application/presentation/ApplicationWebAdapter.java @@ -1,7 +1,6 @@ package team.retum.jobis.domain.application.presentation; import jakarta.validation.Valid; -import jakarta.validation.constraints.Positive; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; @@ -18,9 +17,9 @@ import team.retum.jobis.common.dto.response.TotalPageCountResponse; import team.retum.jobis.domain.application.dto.response.CompanyQueryApplicationsResponse; import team.retum.jobis.domain.application.dto.response.QueryEmploymentCountResponse; +import team.retum.jobis.domain.application.dto.response.QueryMyApplicationsResponse; import team.retum.jobis.domain.application.dto.response.QueryPassedApplicationStudentsResponse; import team.retum.jobis.domain.application.dto.response.QueryRejectionReasonResponse; -import team.retum.jobis.domain.application.dto.response.QueryMyApplicationsResponse; import team.retum.jobis.domain.application.dto.response.TeacherQueryApplicationsResponse; import team.retum.jobis.domain.application.model.ApplicationStatus; import team.retum.jobis.domain.application.presentation.dto.request.ChangeApplicationsStatusWebRequest; @@ -33,9 +32,9 @@ import team.retum.jobis.domain.application.usecase.CreateApplicationUseCase; import team.retum.jobis.domain.application.usecase.DeleteApplicationUseCase; import team.retum.jobis.domain.application.usecase.QueryEmploymentCountUseCase; +import team.retum.jobis.domain.application.usecase.QueryMyApplicationsUseCase; import team.retum.jobis.domain.application.usecase.QueryPassedApplicationStudentsUseCase; import team.retum.jobis.domain.application.usecase.QueryRejectionReasonUseCase; -import team.retum.jobis.domain.application.usecase.QueryMyApplicationsUseCase; import team.retum.jobis.domain.application.usecase.ReapplyUseCase; import team.retum.jobis.domain.application.usecase.RejectApplicationUseCase; import team.retum.jobis.domain.application.usecase.TeacherQueryApplicationsUseCase; diff --git a/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/FileWebAdapter.java b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/FileWebAdapter.java index 56af81730..deab72e04 100644 --- a/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/FileWebAdapter.java +++ b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/FileWebAdapter.java @@ -1,17 +1,22 @@ package team.retum.jobis.domain.file.presentation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import team.retum.jobis.domain.file.dto.response.CreateFileUploadUrlResponse; import team.retum.jobis.domain.file.dto.response.FileUploadResponse; import team.retum.jobis.domain.file.exception.FileNotFoundException; import team.retum.jobis.domain.file.exception.FileUploadFailedException; import team.retum.jobis.domain.file.model.FileType; +import team.retum.jobis.domain.file.presentation.dto.CreatePreSignedUrlWebRequest; +import team.retum.jobis.domain.file.usecase.CreateFileUploadUrlUseCase; import team.retum.jobis.domain.file.usecase.FileUploadUseCase; import java.io.File; @@ -26,6 +31,7 @@ public class FileWebAdapter { private final FileUploadUseCase fileUploadUseCase; + private final CreateFileUploadUrlUseCase createFileUploadUrlUseCase; @ResponseStatus(HttpStatus.CREATED) @PostMapping @@ -52,4 +58,12 @@ public FileUploadResponse uploadFile( fileType ); } + + @ResponseStatus(HttpStatus.CREATED) + @PostMapping("/pre-signed") + public CreateFileUploadUrlResponse createPreSignedUrl( + @RequestBody @Valid CreatePreSignedUrlWebRequest request + ) { + return createFileUploadUrlUseCase.execute(request.toDomainRequest()); + } } diff --git a/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/dto/CreatePreSignedUrlWebRequest.java b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/dto/CreatePreSignedUrlWebRequest.java new file mode 100644 index 000000000..0976fdd07 --- /dev/null +++ b/jobis-infrastructure/src/main/java/team/retum/jobis/domain/file/presentation/dto/CreatePreSignedUrlWebRequest.java @@ -0,0 +1,41 @@ +package team.retum.jobis.domain.file.presentation.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.NoArgsConstructor; +import team.retum.jobis.domain.file.dto.CreateFileUploadUrlRequest; +import team.retum.jobis.domain.file.dto.CreateFileUploadUrlRequest.FileRequest; +import team.retum.jobis.domain.file.model.FileType; + +import java.util.List; + +@Getter +@NoArgsConstructor +public class CreatePreSignedUrlWebRequest { + + private List<@NotNull FileWebRequest> files; + + @Getter + @NoArgsConstructor + public static class FileWebRequest { + + @NotNull + private FileType type; + + @NotBlank + private String fileName; + } + + public CreateFileUploadUrlRequest toDomainRequest() { + return new CreateFileUploadUrlRequest( + files.stream() + .map(file -> + new FileRequest( + file.type, + file.fileName + ) + ).toList() + ); + } +} diff --git a/jobis-infrastructure/src/main/java/team/retum/jobis/global/security/SecurityConfig.java b/jobis-infrastructure/src/main/java/team/retum/jobis/global/security/SecurityConfig.java index fce403c40..c84b8f5e6 100644 --- a/jobis-infrastructure/src/main/java/team/retum/jobis/global/security/SecurityConfig.java +++ b/jobis-infrastructure/src/main/java/team/retum/jobis/global/security/SecurityConfig.java @@ -117,6 +117,7 @@ protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // files .requestMatchers(HttpMethod.POST, "/files").permitAll() + .requestMatchers(HttpMethod.POST, "/files/pre_signed").permitAll() .requestMatchers(HttpMethod.DELETE, "/files").permitAll() // code diff --git a/jobis-infrastructure/src/main/java/team/retum/jobis/thirdparty/s3/S3Adapter.java b/jobis-infrastructure/src/main/java/team/retum/jobis/thirdparty/s3/S3Adapter.java index c2f59b06d..88f671ff1 100644 --- a/jobis-infrastructure/src/main/java/team/retum/jobis/thirdparty/s3/S3Adapter.java +++ b/jobis-infrastructure/src/main/java/team/retum/jobis/thirdparty/s3/S3Adapter.java @@ -1,20 +1,25 @@ package team.retum.jobis.thirdparty.s3; +import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.Headers; import com.amazonaws.services.s3.internal.Mimetypes; import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import team.retum.jobis.domain.file.exception.FileUploadFailedException; -import team.retum.jobis.domain.file.model.FileType; import team.retum.jobis.domain.file.spi.FilePort; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Date; @Component @RequiredArgsConstructor @@ -25,7 +30,7 @@ public class S3Adapter implements FilePort { @Override @Async("asyncTaskExecutor") - public void uploadFile(File file, String fileName, FileType fileType) { + public void uploadFile(File file, String fileName) { try { InputStream inputStream = new FileInputStream(file); ObjectMetadata objectMetadata = new ObjectMetadata(); @@ -45,4 +50,30 @@ public void uploadFile(File file, String fileName, FileType fileType) { throw FileUploadFailedException.EXCEPTION; } } + + @Override + public String generateFileUploadUrl(String fullFileName) { + return URLDecoder.decode( + amazonS3.generatePresignedUrl(getPreSignedUrlRequest(fullFileName)).toString(), StandardCharsets.UTF_8 + ); + } + + private GeneratePresignedUrlRequest getPreSignedUrlRequest(String filename) { + GeneratePresignedUrlRequest request = + new GeneratePresignedUrlRequest(s3Properties.getBucket(), filename) + .withMethod(HttpMethod.PUT) + .withExpiration(getPreSignedUrlExpiration()); + request.addRequestParameter( + Headers.S3_CANNED_ACL, + CannedAccessControlList.PublicRead.toString() + ); + + return request; + } + + private Date getPreSignedUrlExpiration() { + Date expiration = new Date(); + expiration.setTime(expiration.getTime() + 1000 * 60 * 2); + return expiration; + } }