Skip to content

Commit

Permalink
✨ 프로필 이미지 업로드 API
Browse files Browse the repository at this point in the history
  • Loading branch information
waterfogSW committed Dec 1, 2024
1 parent 5cca8aa commit c25e2f5
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.threedays.application.auth.config

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

@ConfigurationProperties(prefix = "user")
data class UserProperties(
val profileImage: ProfileImageProperties
) {
data class ProfileImageProperties(
val maxContentLength: Long,
val uploadExpiresIn: Long
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.threedays.application.user.port.inbound

import com.threedays.domain.user.entity.User
import com.threedays.domain.user.entity.UserProfileImage

interface CompleteUserProfileImageUpload {

fun invoke(command: Command)

data class Command(
val userId: User.Id,
val imageId: UserProfileImage.Id,
val extension: UserProfileImage.Extension,
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.threedays.application.user.port.inbound

import com.threedays.domain.user.entity.UserProfileImage
import java.net.URL
import java.util.UUID

interface GetUserProfileImageUploadUrl {

fun invoke(command: Command): Result

data class Command(val extension: UserProfileImage.Extension)

data class Result(
val imageId: UUID,
val extension: UserProfileImage.Extension,
val url: URL,
val uploadExpiresIn: Long
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.threedays.application.user.port.outbound

import com.threedays.domain.user.entity.UserProfileImage
import java.net.URL
import java.util.UUID

interface UserProfileImagePort {

fun getUploadUrl(
id: UUID,
extension: UserProfileImage.Extension,
expiresIn: Long, // seconds
maxContentLength: Long, // bytes
): URL

fun findImageUrlByIdAndExtension(
id: UserProfileImage.Id,
extension: UserProfileImage.Extension,
): URL

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.threedays.application.user.service

import com.threedays.application.auth.config.AuthProperties
import com.threedays.application.auth.config.UserProperties
import com.threedays.application.auth.port.inbound.IssueLoginTokens
import com.threedays.application.user.port.inbound.CompleteUserProfileImageUpload
import com.threedays.application.user.port.inbound.DeleteProfileWidget
import com.threedays.application.user.port.inbound.GetUserProfileImageUploadUrl
import com.threedays.application.user.port.inbound.PutProfileWidget
import com.threedays.application.user.port.inbound.RegisterUser
import com.threedays.application.user.port.inbound.UpdateDesiredPartner
import com.threedays.application.user.port.inbound.UpdateUserInfo
import com.threedays.application.user.port.outbound.UserProfileImagePort
import com.threedays.domain.user.entity.Company
import com.threedays.domain.user.entity.Location
import com.threedays.domain.user.entity.User
Expand All @@ -15,19 +19,20 @@ import com.threedays.domain.user.repository.LocationQueryRepository
import com.threedays.domain.user.repository.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.net.URL
import java.util.*

@Service
class UserService(
private val userRepository: UserRepository,
private val locationQueryRepository: LocationQueryRepository,
private val companyQueryRepository: CompanyQueryRepository,
private val issueLoginTokens: IssueLoginTokens,
private val userProfileImagePort: UserProfileImagePort,
private val authProperties: AuthProperties,
) : RegisterUser,
PutProfileWidget,
DeleteProfileWidget,
UpdateUserInfo,
UpdateDesiredPartner {
private val userProperties: UserProperties,
) : RegisterUser, PutProfileWidget, DeleteProfileWidget, UpdateUserInfo, UpdateDesiredPartner,
GetUserProfileImageUploadUrl, CompleteUserProfileImageUpload {

@Transactional
override fun invoke(command: RegisterUser.Command): RegisterUser.Result {
Expand Down Expand Up @@ -93,7 +98,6 @@ class UserService(
.also { userRepository.save(it) }
}

@Transactional
override fun invoke(command: UpdateDesiredPartner.Command): User {
return userRepository
.get(command.userId)
Expand All @@ -105,4 +109,31 @@ class UserService(
.also { userRepository.save(it) }
}

override fun invoke(command: GetUserProfileImageUploadUrl.Command): GetUserProfileImageUploadUrl.Result {
val imageId: UUID = UUID.randomUUID()
val uploadUrl: URL = userProfileImagePort.getUploadUrl(
id = imageId,
extension = command.extension,
expiresIn = userProperties.profileImage.uploadExpiresIn,
maxContentLength = userProperties.profileImage.maxContentLength,
)

return GetUserProfileImageUploadUrl.Result(
imageId = imageId,
extension = command.extension,
url = uploadUrl,
uploadExpiresIn = userProperties.profileImage.uploadExpiresIn,
)
}

override fun invoke(command: CompleteUserProfileImageUpload.Command) {
userRepository
.get(command.userId)
.updateUserProfileImage(
extension = command.extension,
getProfileImageUrlAction = userProfileImagePort::findImageUrlByIdAndExtension,
)
.let { userRepository.save(it) }
}

}
4 changes: 4 additions & 0 deletions application/src/main/resources/application-application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ auth:
access-token-expiration-seconds: ${ACCESS_TOKEN_EXPIRATION_SECONDS:7200} # 2 hours
refresh-token-expiration-seconds: ${REFRESH_TOKEN_EXPIRATION_SECONDS:604800} # 1 week
register-token-expiration-seconds: ${REGISTER_TOKEN_EXPIRATION_SECONDS:86400} # 1 day
user:
profile-image:
upload-expires-in: ${PROFILE_IMAGE_UPLOAD_EXPIRES_IN:300} # 5 minutes
max-content-length: ${PROFILE_IMAGE_MAX_CONTENT_LENGTH:5242880} # 5MB
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.threedays.bootstrap.api.user

import com.threedays.application.user.port.inbound.CompleteUserProfileImageUpload
import com.threedays.application.user.port.inbound.DeleteProfileWidget
import com.threedays.application.user.port.inbound.GetUserProfileImageUploadUrl
import com.threedays.application.user.port.inbound.PutProfileWidget
import com.threedays.application.user.port.inbound.RegisterUser
import com.threedays.application.user.port.inbound.UpdateDesiredPartner
Expand All @@ -10,16 +12,20 @@ import com.threedays.bootstrap.api.support.security.withUserAuthentication
import com.threedays.domain.auth.vo.PhoneNumber
import com.threedays.domain.user.entity.User
import com.threedays.domain.user.entity.UserDesiredPartner
import com.threedays.domain.user.entity.UserProfileImage
import com.threedays.domain.user.repository.UserRepository
import com.threedays.domain.user.vo.BirthYearRange
import com.threedays.domain.user.vo.Gender
import com.threedays.domain.user.vo.JobOccupation
import com.threedays.oas.api.UsersApi
import com.threedays.oas.model.CompanyDisplayInfo
import com.threedays.oas.model.CompleteProfileImageUploadRequest
import com.threedays.oas.model.GetMyUserInfoResponse
import com.threedays.oas.model.GetProfileImageUploadUrlResponse
import com.threedays.oas.model.JobOccupationDisplayInfo
import com.threedays.oas.model.LocationDisplayInfo
import com.threedays.oas.model.PreferDistance
import com.threedays.oas.model.ProfileImageExtension
import com.threedays.oas.model.ProfileWidget
import com.threedays.oas.model.ProfileWidgetType
import com.threedays.oas.model.RegisterUserRequest
Expand All @@ -45,6 +51,8 @@ class UserController(
private val deleteProfileWidget: DeleteProfileWidget,
private val updateUserInfo: UpdateUserInfo,
private val updateDesiredPartner: UpdateDesiredPartner,
private val getUserProfileImageUploadUrl: GetUserProfileImageUploadUrl,
private val completeUserProfileImageUpload: CompleteUserProfileImageUpload,
) : UsersApi {

override fun registerUser(
Expand Down Expand Up @@ -245,4 +253,31 @@ class UserController(
).let { ResponseEntity.ok(it) }

}

override fun completeProfileImageUpload(completeProfileImageUploadRequest: CompleteProfileImageUploadRequest): ResponseEntity<Unit> = withUserAuthentication { authentication ->
val command = CompleteUserProfileImageUpload.Command(
userId = authentication.userId,
imageId = UUIDTypeId.from(completeProfileImageUploadRequest.imageId),
extension = UserProfileImage.Extension.valueOf(completeProfileImageUploadRequest.extension.name)
)
completeUserProfileImageUpload.invoke(command)
ResponseEntity.ok().build()
}

override fun getProfileImageUploadUrl(extension: ProfileImageExtension): ResponseEntity<GetProfileImageUploadUrlResponse> {
val command: GetUserProfileImageUploadUrl.Command = GetUserProfileImageUploadUrl.Command(
extension = UserProfileImage.Extension.valueOf(extension.name)
)

val result: GetUserProfileImageUploadUrl.Result = getUserProfileImageUploadUrl.invoke(command)

return GetProfileImageUploadUrlResponse(
imageId = result.imageId,
url = result.url.toString(),
extension = ProfileImageExtension.valueOf(result.extension.name),
uploadExpiresIn = result.uploadExpiresIn.toInt(),
).let {
ResponseEntity.ok(it)
}
}
}
13 changes: 13 additions & 0 deletions domain/src/main/kotlin/com/threedays/domain/user/entity/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package com.threedays.domain.user.entity

import com.threedays.domain.auth.vo.PhoneNumber
import com.threedays.domain.user.entity.UserDesiredPartner.PreferDistance
import com.threedays.domain.user.entity.UserProfileImage.Extension
import com.threedays.domain.user.repository.CompanyQueryRepository
import com.threedays.domain.user.repository.LocationQueryRepository
import com.threedays.domain.user.vo.BirthYearRange
import com.threedays.domain.user.vo.Gender
import com.threedays.domain.user.vo.JobOccupation
import com.threedays.support.common.base.domain.AggregateRoot
import com.threedays.support.common.base.domain.UUIDTypeId
import java.net.URL
import java.time.Year
import java.util.*

Expand All @@ -17,6 +19,7 @@ data class User(
override val id: Id,
val name: Name,
val phoneNumber: PhoneNumber,
val profileImages: List<UserProfileImage> = emptyList(),
val profile: UserProfile,
val desiredPartner: UserDesiredPartner,
) : AggregateRoot<User, User.Id>() {
Expand All @@ -25,6 +28,7 @@ data class User(

@JvmInline
value class Name(val value: String) {

init {
require(value.isNotBlank()) { "이름은 공백일 수 없습니다." }
}
Expand Down Expand Up @@ -135,4 +139,13 @@ data class User(
)
}

// TODO: 이미지 여러개 업로드 가능하도록 수정 필요
fun updateUserProfileImage(
extension: Extension,
getProfileImageUrlAction: (UserProfileImage.Id, Extension) -> URL,
): User {
val newProfileImage = UserProfileImage.create(extension, getProfileImageUrlAction)
return copy(profileImages = listOf(newProfileImage))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class UserProfileImage(
extension: Extension,
getProfileImageUrlAction: (Id, Extension) -> URL,
): UserProfileImage {
val imageId = Id(UUID.randomUUID())
val imageId: Id = UUIDTypeId.random<Id>()
val imageUrl: URL = try {
getProfileImageUrlAction(imageId, extension)
} catch (e: Exception) {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ java = "21"
spring = "6.1.10"
spring-boot = "3.3.6"
spring-boot-dependency-management = "1.1.6"
kotlin = "2.0.0"
kotlin = "2.1.0"
sonar-cloud = "4.4.1.3373"
openapi-generator = "7.5.0"
jib = "3.4.3"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.threedays.aws.s3.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import software.amazon.awssdk.services.s3.presigner.S3Presigner

@Configuration
class S3Config {

@Bean
fun s3Presigner(): S3Presigner {
return S3Presigner.create()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.threedays.aws.s3.config

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

@ConfigurationProperties(prefix = "aws.s3")
class S3Properties(
val userProfileImage: UserProfileImage,
) {

data class UserProfileImage(
val bucketName: String,
val keyPrefix: String,
)

}
Loading

0 comments on commit c25e2f5

Please sign in to comment.