diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/object/PathList.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/object/PathList.kt new file mode 100644 index 00000000..dfb812aa --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/object/PathList.kt @@ -0,0 +1,5 @@ +package hs.kr.entrydsm.domain.file.`object` + +object PathList { + const val PHOTO = "entry_photo/" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/GenerateFileUrlPort.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/GenerateFileUrlPort.kt new file mode 100644 index 00000000..06febd57 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/GenerateFileUrlPort.kt @@ -0,0 +1,5 @@ +package hs.kr.entrydsm.domain.file.spi + +interface GenerateFileUrlPort { + fun generateFileUrl(fileName: String, path: String): String +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt new file mode 100644 index 00000000..9b57890f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/file/spi/UploadFilePort.kt @@ -0,0 +1,7 @@ +package hs.kr.entrydsm.domain.file.spi + +import java.io.File + +interface UploadFilePort { + fun upload(file: File, path: String): String +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessException.kt new file mode 100644 index 00000000..355afaeb --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessException.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.global.exception + +abstract class BusinessException( + open val status: Int, + override val message: String, +) : RuntimeException() \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/WebException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/WebException.kt new file mode 100644 index 00000000..5e4fcbb8 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/WebException.kt @@ -0,0 +1,6 @@ +package hs.kr.entrydsm.global.exception + +abstract class WebException( + open val status: Int, + override val message: String, +) : RuntimeException() \ No newline at end of file diff --git a/casper-application-infrastructure/build.gradle.kts b/casper-application-infrastructure/build.gradle.kts index 72f8ae4c..2eb3c1d9 100644 --- a/casper-application-infrastructure/build.gradle.kts +++ b/casper-application-infrastructure/build.gradle.kts @@ -134,6 +134,9 @@ dependencies { // swagger implementation(Dependencies.SWAGGER) + + // aws s3 + implementation("com.amazonaws:aws-java-sdk-s3:1.12.767") } allOpen { diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt index 0ce45b13..35f31b89 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/ApplicationJpaEntity.kt @@ -52,7 +52,7 @@ class ApplicationJpaEntity( @get:JvmName("getIsOutOfHeadcount") var isOutOfHeadcount: Boolean?, @Column(columnDefinition = "TEXT") - val photoPath: String?, + var photoPath: String?, val parentRelation: String?, val postalCode: String?, val detailAddress: String?, diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/PhotoJpaEntity.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/PhotoJpaEntity.kt new file mode 100644 index 00000000..77b2379a --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/entity/PhotoJpaEntity.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.application.domain.application.domain.entity + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.util.UUID + +@Entity +@Table(name = "tbl_photo") +class PhotoJpaEntity( + @Id + val userId: UUID, + + @Column(name = "photo_path", nullable = false) + var photo: String, +) diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/repository/PhotoJpaRepository.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/repository/PhotoJpaRepository.kt new file mode 100644 index 00000000..6ce92d04 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/domain/repository/PhotoJpaRepository.kt @@ -0,0 +1,10 @@ +package hs.kr.entrydsm.application.domain.application.domain.repository + +import hs.kr.entrydsm.application.domain.application.domain.entity.PhotoJpaEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.util.UUID + +interface PhotoJpaRepository : JpaRepository { + + fun findByUserId(userId: UUID): PhotoJpaEntity? +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt index fb1a995e..b0bebfe5 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/presentation/dto/response/ApplicationDetailResponse.kt @@ -22,5 +22,6 @@ data class ApplicationDetailResponse( val reviewedAt: LocalDateTime?, val createdAt: LocalDateTime, val updatedAt: LocalDateTime, + val photoPath: String?, ) } diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/ApplicationQueryUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/ApplicationQueryUseCase.kt index 46c48010..dc007b41 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/ApplicationQueryUseCase.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/ApplicationQueryUseCase.kt @@ -5,11 +5,15 @@ import hs.kr.entrydsm.application.domain.application.domain.repository.Applicati import hs.kr.entrydsm.application.domain.application.domain.repository.ApplicationScoreJpaRepository import hs.kr.entrydsm.application.domain.application.domain.repository.CalculationResultJpaRepository import hs.kr.entrydsm.application.domain.application.domain.repository.CalculationStepJpaRepository +import hs.kr.entrydsm.application.domain.application.domain.repository.PhotoJpaRepository import hs.kr.entrydsm.application.domain.application.presentation.dto.response.ApplicationDetailResponse import hs.kr.entrydsm.application.domain.application.presentation.dto.response.ApplicationListResponse import hs.kr.entrydsm.application.domain.application.presentation.dto.response.ApplicationScoresResponse import hs.kr.entrydsm.application.domain.application.presentation.dto.response.CalculationHistoryResponse import hs.kr.entrydsm.application.domain.application.presentation.dto.response.CalculationResponse +import hs.kr.entrydsm.application.global.security.SecurityAdapter +import hs.kr.entrydsm.domain.file.`object`.PathList +import hs.kr.entrydsm.domain.file.spi.GenerateFileUrlPort import org.springframework.data.domain.PageRequest import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort @@ -25,6 +29,9 @@ class ApplicationQueryUseCase( private val calculationResultRepository: CalculationResultJpaRepository, private val calculationStepRepository: CalculationStepJpaRepository, private val objectMapper: ObjectMapper, + private val photoJpaRepository: PhotoJpaRepository, + private val securityAdapter: SecurityAdapter, + private val generateFileUrlPort: GenerateFileUrlPort ) { fun getApplicationById(applicationId: String): ApplicationDetailResponse { val uuid = UUID.fromString(applicationId) @@ -32,6 +39,9 @@ class ApplicationQueryUseCase( applicationRepository.findById(uuid) .orElseThrow { IllegalArgumentException("원서를 찾을 수 없습니다: $applicationId") } + val user = securityAdapter.getCurrentUserId() + val photoPath = photoJpaRepository.findByUserId(user)?.photo + return ApplicationDetailResponse( success = true, data = @@ -51,6 +61,7 @@ class ApplicationQueryUseCase( reviewedAt = application.reviewedAt, createdAt = application.createdAt, updatedAt = application.updatedAt, + photoPath = generateFileUrlPort.generateFileUrl(photoPath!!, PathList.PHOTO) ), ) } diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/FileUploadUseCase.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/FileUploadUseCase.kt new file mode 100644 index 00000000..c334f48f --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/application/usecase/FileUploadUseCase.kt @@ -0,0 +1,35 @@ +package hs.kr.entrydsm.application.domain.application.usecase + +import hs.kr.entrydsm.application.domain.application.domain.entity.PhotoJpaEntity +import hs.kr.entrydsm.application.domain.application.domain.repository.PhotoJpaRepository +import hs.kr.entrydsm.application.global.security.SecurityAdapter +import hs.kr.entrydsm.domain.file.spi.UploadFilePort +import hs.kr.entrydsm.domain.file.`object`.PathList +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.io.File + +@Component +class FileUploadUseCase( + private val uploadFilePort: UploadFilePort, + private val photoJpaRepository: PhotoJpaRepository, + private val securityAdapter: SecurityAdapter, +) { + @Transactional + fun execute(file: File): String { + val userId = securityAdapter.getCurrentUserId() + val photo = uploadFilePort.upload(file, PathList.PHOTO) + + photoJpaRepository.findByUserId(userId)?.apply { + this.photo = photo + photoJpaRepository.save(this) + } ?: photoJpaRepository.save( + PhotoJpaEntity( + userId = userId, + photo = photo + ) + ) + + return photo + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/FileController.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/FileController.kt new file mode 100644 index 00000000..b561b027 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/FileController.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.application.domain.file.presentation + +import hs.kr.entrydsm.application.domain.application.usecase.FileUploadUseCase +import hs.kr.entrydsm.application.domain.file.presentation.converter.ImageFileConverter +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RequestMapping("/photo") +@RestController +class FileController( + private val fileUploadUseCase: FileUploadUseCase +) { + @PostMapping + fun uploadPhoto(@RequestPart(name = "image") file: MultipartFile): ResponseEntity> { + val photoUrl = fileUploadUseCase.execute( + file.let(ImageFileConverter::transferTo) + ) + return ResponseEntity.ok(mapOf("fileName" to photoUrl)) + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileConverter.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileConverter.kt new file mode 100644 index 00000000..6018767b --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileConverter.kt @@ -0,0 +1,31 @@ +package hs.kr.entrydsm.application.domain.file.presentation.converter + +import hs.kr.entrydsm.application.domain.file.presentation.exception.WebFileExceptions +import org.springframework.web.multipart.MultipartFile +import java.io.File +import java.io.FileOutputStream +import java.util.UUID + +interface FileConverter { + val MultipartFile.extension: String + get() = originalFilename?.substringAfterLast(".", "")?.uppercase() ?: "" + + fun isCorrectExtension(multipartFile: MultipartFile): Boolean + + fun transferTo(multipartFile: MultipartFile): File { + if (!isCorrectExtension(multipartFile)) { + throw WebFileExceptions.InvalidExtension() + } + + return transferFile(multipartFile) + } + + private fun transferFile(multipartFile: MultipartFile): File { + return File("${UUID.randomUUID()}_${multipartFile.originalFilename}") + .apply { + FileOutputStream(this).use { + it.write(multipartFile.bytes) + } + } + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileExtensions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileExtensions.kt new file mode 100644 index 00000000..e6317d5a --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/FileExtensions.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.domain.file.presentation.converter + +object FileExtensions { + const val JPG = "JPG" + const val JPEG = "JPEG" + const val PNG = "PNG" + const val HEIC = "HEIC" +} diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/ImageFileConverter.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/ImageFileConverter.kt new file mode 100644 index 00000000..080a0842 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/converter/ImageFileConverter.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.application.domain.file.presentation.converter + +import hs.kr.entrydsm.application.domain.file.presentation.converter.FileExtensions.HEIC +import hs.kr.entrydsm.application.domain.file.presentation.converter.FileExtensions.JPEG +import hs.kr.entrydsm.application.domain.file.presentation.converter.FileExtensions.JPG +import hs.kr.entrydsm.application.domain.file.presentation.converter.FileExtensions.PNG +import org.springframework.web.multipart.MultipartFile + +object ImageFileConverter : FileConverter { + override fun isCorrectExtension(multipartFile: MultipartFile): Boolean { + return when (multipartFile.extension) { + JPG, JPEG, PNG, HEIC -> true + else -> false + } + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/FileExceptions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/FileExceptions.kt new file mode 100644 index 00000000..78f9b397 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/FileExceptions.kt @@ -0,0 +1,23 @@ +package hs.kr.entrydsm.application.domain.file.presentation.exception + +import hs.kr.entrydsm.global.exception.BusinessException + +sealed class FileExceptions( + override val status: Int, + override val message: String, +) : BusinessException(status, message) { + // 400 + class NotValidContent(message: String = NOT_VALID_CONTENT) : FileExceptions(400, message) + + // 404 + class PathNotFound(message: String = PATH_NOT_FOUND) : FileExceptions(404, message) + + // 500 + class IOInterrupted(message: String = IO_INTERRUPTED) : FileExceptions(500, message) + + companion object { + private const val NOT_VALID_CONTENT = "파일의 내용이 올바르지 않습니다." + private const val PATH_NOT_FOUND = "경로를 찾을 수 없습니다." + private const val IO_INTERRUPTED = "파일 입출력 처리가 중단되었습니다." + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/WebFileExceptions.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/WebFileExceptions.kt new file mode 100644 index 00000000..f670b6c7 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/domain/file/presentation/exception/WebFileExceptions.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.application.domain.file.presentation.exception + +import hs.kr.entrydsm.global.exception.WebException + +sealed class WebFileExceptions( + override val status: Int, + override val message: String, +) : WebException(status, message) { + class InvalidExtension(message: String = INVALID_EXTENSION) : WebFileExceptions(400, message) + + companion object { + private const val INVALID_EXTENSION = "확장자가 유효하지 않습니다." + } +} \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/AwsS3Config.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/AwsS3Config.kt new file mode 100644 index 00000000..0dee148a --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/AwsS3Config.kt @@ -0,0 +1,30 @@ +package hs.kr.entrydsm.application.global.config + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import hs.kr.entrydsm.application.global.storage.AwsCredentialsProperties +import hs.kr.entrydsm.application.global.storage.AwsProperties +import hs.kr.entrydsm.application.global.storage.AwsRegionProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableConfigurationProperties(AwsProperties::class, AwsCredentialsProperties::class) +class AwsS3Config( + private val awsCredentialsProperties: AwsCredentialsProperties, + private val awsRegionProperties: AwsRegionProperties +) { + + @Bean + fun amazonS3Client(): AmazonS3Client { + val credentials = BasicAWSCredentials(awsCredentialsProperties.accessKey, awsCredentialsProperties.secretKey) + + return AmazonS3ClientBuilder.standard() + .withRegion(awsRegionProperties.static) + .withCredentials(AWSStaticCredentialsProvider(credentials)) + .build() as AmazonS3Client + } +} diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/SecurityConfig.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/SecurityConfig.kt index e5142001..31bc890a 100644 --- a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/SecurityConfig.kt +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/config/SecurityConfig.kt @@ -44,9 +44,10 @@ class SecurityConfig( .requestMatchers("/webjars/**").permitAll() .requestMatchers("/admin/**").hasRole(UserRole.ADMIN.name) .requestMatchers("/api/v1/applications/**").hasRole(UserRole.USER.name) + .requestMatchers("/photo").hasRole(UserRole.USER.name) .anyRequest().authenticated() } - .apply(filterConfig) + .with(filterConfig) { } return http.build() } diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsCredentialsProperties.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsCredentialsProperties.kt new file mode 100644 index 00000000..f9d1b46b --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsCredentialsProperties.kt @@ -0,0 +1,9 @@ +package hs.kr.entrydsm.application.global.storage + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("cloud.aws.credentials") +class AwsCredentialsProperties( + val accessKey: String, + val secretKey: String, +) \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsProperties.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsProperties.kt new file mode 100644 index 00000000..f43eef28 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsProperties.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.global.storage + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("cloud.aws.s3") +class AwsProperties( + val bucket: String +) \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsRegionProperties.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsRegionProperties.kt new file mode 100644 index 00000000..dd2de030 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsRegionProperties.kt @@ -0,0 +1,8 @@ +package hs.kr.entrydsm.application.global.storage + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("cloud.aws.region") +class AwsRegionProperties( + val static: String +) \ No newline at end of file diff --git a/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt new file mode 100644 index 00000000..6645de81 --- /dev/null +++ b/casper-application-infrastructure/src/main/kotlin/hs/kr/entrydsm/application/global/storage/AwsS3Adapter.kt @@ -0,0 +1,71 @@ +package hs.kr.entrydsm.application.global.storage + +import com.amazonaws.HttpMethod +import com.amazonaws.services.s3.AmazonS3Client +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 hs.kr.entrydsm.application.domain.file.presentation.exception.FileExceptions +import hs.kr.entrydsm.domain.file.spi.GenerateFileUrlPort +import hs.kr.entrydsm.domain.file.spi.UploadFilePort +import org.springframework.stereotype.Component +import java.io.File +import java.io.IOException +import java.util.Date + +@Component +class AwsS3Adapter( + private val amazonS3Client: AmazonS3Client, + private val awsProperties: AwsProperties +) : UploadFilePort, GenerateFileUrlPort { + + companion object { + const val EXP_TIME = 1000 * 60 * 2 + } + + override fun upload(file: File, path: String): String { + runCatching { inputS3(file, path) } + .also { file.delete() } + + return getS3Url(file.name) + } + + private fun inputS3(file: File, path: String) { + try { + val inputStream = file.inputStream() + val objectMetadata = ObjectMetadata().apply { + contentLength = file.length() + contentType = Mimetypes.getInstance().getMimetype(file) + } + + amazonS3Client.putObject( + PutObjectRequest( + awsProperties.bucket, + path, + inputStream, + objectMetadata + ).withCannedAcl(CannedAccessControlList.PublicRead) + ) + } catch (e: IOException) { + throw FileExceptions.IOInterrupted() + } + } + + private fun getS3Url(fileName: String): String { + return amazonS3Client.getUrl(awsProperties.bucket, fileName).toString() + } + + override fun generateFileUrl(fileName: String, path: String): String { + val expiration = Date().apply { + time += EXP_TIME + } + return amazonS3Client.generatePresignedUrl( + GeneratePresignedUrlRequest( + awsProperties.bucket, + "${path}$fileName" + ).withMethod(HttpMethod.GET).withExpiration(expiration) + ).toString() + } +} \ No newline at end of file