-
Notifications
You must be signed in to change notification settings - Fork 0
upload file #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
upload file #54
Changes from 20 commits
29c9cc9
df33741
3a52655
506701f
e9f4176
f6f2007
e98bae4
6654be6
cf82228
6008d21
5694188
bad8c73
7474b35
dcfee81
242ed5e
53d8b99
e42db3c
ddb5e8c
923f64c
1087db0
dd44240
e44014f
4ef5128
3811e3a
1003f70
db82271
84b479d
15d4e6d
f1823b9
f25fdab
ce24d72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package hs.kr.entrydsm.domain.file.`object` | ||
|
|
||
| object PathList { | ||
| const val PHOTO = "entry_photo/" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package hs.kr.entrydsm.domain.file.spi | ||
|
|
||
| interface GenerateFileUrlPort { | ||
| fun generateFileUrl(fileName: String, path: String): String | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package hs.kr.entrydsm.domain.file.spi | ||
|
|
||
| import java.io.File | ||
|
|
||
| interface UploadFilePort { | ||
| fun upload(file: File, path: String): String | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package hs.kr.entrydsm.global.exception | ||
|
|
||
| abstract class WebException( | ||
| open val status: Int, | ||
| override val message: String, | ||
| ) : RuntimeException() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<PhotoJpaEntity, Long> { | ||
|
|
||
| fun findByUserId(userId: UUID): PhotoJpaEntity? | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package hs.kr.entrydsm.application.domain.application.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<Map<String, String>> { | ||
| val photoUrl = fileUploadUseCase.execute( | ||
| file.let(ImageFileConverter::transferTo) | ||
| ) | ||
| return ResponseEntity.ok(mapOf("photo_url" to photoUrl)) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
coehgns marked this conversation as resolved.
|
||
| 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 photoUrl = uploadFilePort.upload(file, PathList.PHOTO) | ||
|
|
||
| photoJpaRepository.findByUserId(userId)?.apply { | ||
| photo = photoUrl | ||
| photoJpaRepository.save(this) | ||
| } ?: photoJpaRepository.save( | ||
| PhotoJpaEntity( | ||
| userId = userId, | ||
| photo = photoUrl | ||
| ) | ||
| ) | ||
|
|
||
| return photoUrl | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
| } | ||
| } | ||
| } | ||
|
coehgns marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } | ||
|
coehgns marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = "확장자가 유효하지 않습니다." | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| 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 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 awsProperties: AwsProperties, | ||
| private val awsCredentialsProperties: AwsCredentialsProperties | ||
| ) { | ||
|
|
||
| @Bean | ||
| fun amazonS3Client(): AmazonS3Client { | ||
| val credentials = BasicAWSCredentials(awsCredentialsProperties.accessKey, awsCredentialsProperties.secretKey) | ||
|
|
||
| return AmazonS3ClientBuilder.standard() | ||
| .withRegion(awsProperties.region) | ||
| .withCredentials(AWSStaticCredentialsProvider(credentials)) | ||
| .build() as AmazonS3Client | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package hs.kr.entrydsm.application.global.storage | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties | ||
|
|
||
| @ConfigurationProperties("cloud.aws.s3") | ||
| class AwsProperties( | ||
| val bucket: String, | ||
| val region: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.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(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 업로드 예외가 삼켜져 성공으로 처리됨 runCatching 결과를 무시하고 항상 URL을 반환합니다. 업로드 실패 시 실패를 반환/전파해야 합니다. 또한 파일 삭제는 finally에서 보장하세요. - override fun upload(file: File, path: String): String {
- runCatching { inputS3(file, path) }
- .also { file.delete() }
-
- return getS3Url(path)
- }
+ override fun upload(file: File, path: String): String {
+ return try {
+ inputS3(file, path)
+ getS3Url(path)
+ } catch (e: Exception) {
+ throw IllegalStateException("S3 업로드 실패: ${e.message}", e)
+ } finally {
+ file.delete()
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun inputS3(file: File, path: String) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val inputStream = file.inputStream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val objectMetadata = ObjectMetadata().apply { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentLength = file.length() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentType = Mimetypes.getInstance().getMimetype(file) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+35
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. InputStream 누수 가능성과 예외 타입 구체화
- private fun inputS3(file: File, path: String) {
- try {
- val inputStream = file.inputStream()
- val objectMetadata = ObjectMetadata().apply {
- contentLength = file.length()
- contentType = Mimetypes.getInstance().getMimetype(file)
- }
+ private fun inputS3(file: File, path: String) {
+ try {
+ file.inputStream().use { 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 IllegalArgumentException("File Exception")
+ amazonS3Client.putObject(
+ PutObjectRequest(
+ awsProperties.bucket,
+ path,
+ inputStream,
+ objectMetadata
+ ).withCannedAcl(CannedAccessControlList.PublicRead)
+ )
+ }
+ } catch (e: Exception) {
+ throw IllegalStateException("S3 업로드 처리 중 예외 발생: ${e.message}", e)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| amazonS3Client.putObject( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PutObjectRequest( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| awsProperties.bucket, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputStream, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| objectMetadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).withCannedAcl(CannedAccessControlList.PublicRead) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e: IOException) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw IllegalArgumentException("File Exception") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private fun getS3Url(path: String): String { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return amazonS3Client.getUrl(awsProperties.bucket, path).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() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.