Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
29c9cc9
feat ( #53 ) : PathList
coehgns Sep 26, 2025
df33741
feat ( #53 ) : ImageFileConverter
coehgns Sep 26, 2025
3a52655
feat ( #53 ) : PhotoJpaEntity
coehgns Sep 26, 2025
506701f
feat ( #53 ) : PhotoJpaRepository
coehgns Sep 26, 2025
e9f4176
feat ( #53 ) : UploadFilePort
coehgns Sep 26, 2025
f6f2007
feat ( #53 ) : WebException
coehgns Sep 26, 2025
e98bae4
feat ( #53 ) : WebFileExceptions
coehgns Sep 26, 2025
6654be6
feat ( #53 ) : FileConverter
coehgns Sep 26, 2025
cf82228
feat ( #53 ) : FileExtensions
coehgns Sep 26, 2025
6008d21
feat ( #53 ) : FileUploadUseCase
coehgns Sep 26, 2025
5694188
feat ( #53 ) : GenerateFileUrlPort
coehgns Sep 26, 2025
bad8c73
feat ( #53 ) : FileController
coehgns Sep 26, 2025
7474b35
feat ( #53 ) : AwsS3Adapter
coehgns Sep 26, 2025
dcfee81
feat ( #53 ) : AwsS3Config
coehgns Sep 26, 2025
242ed5e
feat ( #53 ) : AwsProperties
coehgns Sep 26, 2025
53d8b99
feat ( #53 ) : AwsCredentialsProperties
coehgns Sep 26, 2025
e42db3c
refactor ( #53 ) : ApplicationDetailResponse photo path 추가
coehgns Sep 26, 2025
ddb5e8c
refactor ( #53 ) : ApplicationJpaEntity
coehgns Sep 26, 2025
923f64c
refactor ( #53 ) : ApplicationQueryUseCase
coehgns Sep 26, 2025
1087db0
build ( #53 ) : aws s3 의존성 추가
coehgns Sep 26, 2025
dd44240
refactor ( #53 ) : FileController 파일 구조 수정
coehgns Sep 26, 2025
e44014f
feat ( #53 ) : AwsRegionProperties
coehgns Sep 26, 2025
4ef5128
refactor ( #53 ) : AwsProperties
coehgns Sep 26, 2025
3811e3a
refactor ( #53 ) : AwsS3Config
coehgns Sep 26, 2025
1003f70
refactor ( #53 ) : PhotoJpaRepository
coehgns Sep 26, 2025
db82271
refactor ( #53 ) : FileUploadUseCase
coehgns Sep 26, 2025
84b479d
refactor ( #53 ) : AwsS3Adapter
coehgns Sep 26, 2025
15d4e6d
refactor ( #53 ) : FileController
coehgns Sep 26, 2025
f1823b9
refactor ( #53 ) : BusinessException
coehgns Sep 26, 2025
f25fdab
refactor ( #53 ) : FileExceptions
coehgns Sep 26, 2025
ce24d72
refactor ( #53 ) : SecurityConfig
coehgns Sep 26, 2025
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
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 BusinessException(
open val status: Int,
override val message: String,
) : RuntimeException()
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()
3 changes: 3 additions & 0 deletions casper-application-infrastructure/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ dependencies {

// swagger
implementation(Dependencies.SWAGGER)

// aws s3
implementation("com.amazonaws:aws-java-sdk-s3:1.12.767")
}

allOpen {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand Down
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, UUID> {

fun findByUserId(userId: UUID): PhotoJpaEntity?
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ data class ApplicationDetailResponse(
val reviewedAt: LocalDateTime?,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
val photoPath: String?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,13 +29,19 @@ 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)
val application =
applicationRepository.findById(uuid)
.orElseThrow { IllegalArgumentException("원서를 찾을 수 없습니다: $applicationId") }

val user = securityAdapter.getCurrentUserId()
val photoPath = photoJpaRepository.findByUserId(user)?.photo

return ApplicationDetailResponse(
success = true,
data =
Expand All @@ -51,6 +61,7 @@ class ApplicationQueryUseCase(
reviewedAt = application.reviewedAt,
createdAt = application.createdAt,
updatedAt = application.updatedAt,
photoPath = generateFileUrlPort.generateFileUrl(photoPath!!, PathList.PHOTO)
),
)
}
Expand Down
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
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
}
}
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> {
val photoUrl = fileUploadUseCase.execute(
file.let(ImageFileConverter::transferTo)
)
return ResponseEntity.ok(mapOf("fileName" to 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)
}
}
}
}
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 = "파일 입출력 처리가 중단되었습니다."
}
}
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,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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
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,8 @@
package hs.kr.entrydsm.application.global.storage

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("cloud.aws.s3")
class AwsProperties(
val bucket: String
)
Original file line number Diff line number Diff line change
@@ -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
)
Loading