Skip to content

[DPMBE-68] 유저 프로필 이미지 업로드 기능을 만든다 #89

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

Merged
merged 5 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -27,4 +27,12 @@ class ImageController(
): ImageUrlResponse {
return getPresignedUrlUseCase.forPromise(promiseId, fileExtension)
}

@Operation(summary = "유저 프로필 이미지 업로드 Presigned URL 발급")
@GetMapping("/users/me/images")
fun getPresignedUrlOfUser(
@RequestParam fileExtension: ImageFileExtension,
): ImageUrlResponse {
return getPresignedUrlUseCase.forUser(fileExtension)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.depromeet.whatnow.annotation.UseCase
import com.depromeet.whatnow.api.image.dto.ImageUrlResponse
import com.depromeet.whatnow.config.s3.ImageFileExtension
import com.depromeet.whatnow.config.s3.S3UploadPresignedUrlService
import com.depromeet.whatnow.config.security.SecurityUtils

@UseCase
class GetPresignedUrlUseCase(
Expand All @@ -12,4 +13,9 @@ class GetPresignedUrlUseCase(
fun forPromise(promiseId: Long, fileExtension: ImageFileExtension): ImageUrlResponse {
return ImageUrlResponse.from(presignedUrlService.forPromise(promiseId, fileExtension))
}

fun forUser(fileExtension: ImageFileExtension): ImageUrlResponse {
val currentUserId = SecurityUtils.currentUserId
return ImageUrlResponse.from(presignedUrlService.forUser(currentUserId, fileExtension))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ class PictureController(

@Operation(summary = "약속 관련 이미지 업로드 성공 요청")
@PostMapping("/promises/{promiseId}/images/success/{imageKey}")
fun successUploadImage(@PathVariable promiseId: Long, @PathVariable imageKey: String, @RequestParam pictureCommentType: PictureCommentType) {
successUseCase.successUploadImage(promiseId, imageKey, pictureCommentType)
fun promiseUploadImageSuccess(@PathVariable promiseId: Long, @PathVariable imageKey: String, @RequestParam pictureCommentType: PictureCommentType) {
successUseCase.promiseUploadImageSuccess(promiseId, imageKey, pictureCommentType)
}

@Operation(summary = "유저 프로필 이미지 업로드 성공 요청")
@PostMapping("/users/me/images/success/{imageKey}")
fun userUploadImageSuccess(@PathVariable imageKey: String) {
successUseCase.userUploadImageSuccess(imageKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import com.depromeet.whatnow.domains.picture.service.PictureDomainService
class PictureUploadSuccessUseCase(
val pictureDomainService: PictureDomainService,
) {
fun successUploadImage(promiseId: Long, imageKey: String, pictureCommentType: PictureCommentType) {
fun promiseUploadImageSuccess(promiseId: Long, imageKey: String, pictureCommentType: PictureCommentType) {
val currentUserId: Long = SecurityUtils.currentUserId
pictureDomainService.successUploadImage(currentUserId, promiseId, imageKey, pictureCommentType)
pictureDomainService.promiseUploadImageSuccess(currentUserId, promiseId, imageKey, pictureCommentType)
}

fun userUploadImageSuccess(imageKey: String) {
val currentUserId: Long = SecurityUtils.currentUserId
pictureDomainService.userUploadImageSuccess(currentUserId, imageKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ImageControllerTest {
lateinit var mockMvc: MockMvc

@Test
fun `presignedUrl 요청에 성공하면 200을 응답한다`() {
fun `약속 이미지 presignedUrl 요청에 성공하면 200을 응답한다`() {
// given
val promiseId = 1L
val fileExtension = ImageFileExtension.JPEG.name
Expand All @@ -36,4 +36,18 @@ class ImageControllerTest {
.andExpect(status().isOk)
.andDo { print(it) }
}

@Test
fun `유저 프로필 presignedUrl 요청에 성공하면 200을 응답한다`() {
// given
val fileExtension = ImageFileExtension.JPEG.name

// when, then
mockMvc.perform(
get("/v1/users/me/images")
.param("fileExtension", fileExtension),
)
.andExpect(status().isOk)
.andDo { print(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import com.depromeet.whatnow.config.s3.ImageFileExtension
import com.depromeet.whatnow.config.s3.ImageUrlDto
import com.depromeet.whatnow.config.s3.S3UploadPresignedUrlService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.given
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.test.context.support.WithMockUser

@ExtendWith(MockitoExtension::class)
class GetPresignedUrlUseCaseTest {
Expand All @@ -19,8 +24,16 @@ class GetPresignedUrlUseCaseTest {
@InjectMocks
lateinit var getPresignedUrlUseCase: GetPresignedUrlUseCase

@BeforeEach
fun setup() {
val securityContext = SecurityContextHolder.createEmptyContext()
val authentication = UsernamePasswordAuthenticationToken("1", null, setOf(SimpleGrantedAuthority("ROLE_USER")))
securityContext.authentication = authentication
SecurityContextHolder.setContext(securityContext)
}

@Test
fun `PresignUrl 을 요청하면 url 을 반환한다`() {
fun `약속 이미지 PresignUrl 을 요청하면 url 을 반환한다`() {
// given
given(presignedUrlService.forPromise(1, ImageFileExtension.JPEG)).willReturn(
ImageUrlDto(
Expand All @@ -35,4 +48,22 @@ class GetPresignedUrlUseCaseTest {
assertEquals("https://whatnow.kr/1.jpg", imageUrlResponse.presignedUrl)
assertEquals("1.jpg", imageUrlResponse.key)
}

@Test
@WithMockUser(username = "1")
fun `유저 프로필 PresignUrl 을 요청하면 url 을 반환한다`() {
// given
given(presignedUrlService.forUser(1, ImageFileExtension.JPEG)).willReturn(
ImageUrlDto(
url = "https://whatnow.kr/1.jpg",
key = "1.jpg",
),
)
// when
val imageUrlResponse = getPresignedUrlUseCase.forUser(ImageFileExtension.JPEG)

// then
assertEquals("https://whatnow.kr/1.jpg", imageUrlResponse.presignedUrl)
assertEquals("1.jpg", imageUrlResponse.key)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PictureControllerTest {
lateinit var mockMvc: MockMvc

@Test
fun `이미지 업로드 성공 요청에 정상적으로 200을 반환한다`() {
fun `약속 이미지 업로드 성공 요청에 정상적으로 200을 반환한다`() {
// given
val promiseId = 1
val imageKey = "imageKey"
Expand All @@ -37,4 +37,17 @@ class PictureControllerTest {
.andExpect(status().isOk)
.andDo { print(it) }
}

@Test
fun `유저 프로필 업로드 성공 요청에 정상적으로 200을 반환한다`() {
// given
val imageKey = "imageKey"

// when, then
mockMvc.perform(
post("/v1/users/me/images/success/{imageKey}", imageKey),
)
.andExpect(status().isOk)
.andDo { print(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,26 @@ class PictureUploadSuccessUseCaseTest {
}

@Test
fun `이미지 업로드 성공 요청시 정상적이라면 에러가 발생하지 않는다`() {
fun `약속 이미지 업로드 성공 요청시 정상적이라면 에러가 발생하지 않는다`() {
// given

// when

// then
assertThatCode {
pictureUploadSuccessUseCase.successUploadImage(1, "1", PictureCommentType.SORRY_LATE)
pictureUploadSuccessUseCase.promiseUploadImageSuccess(1, "imageKey", PictureCommentType.SORRY_LATE)
}.doesNotThrowAnyException()
}

@Test
fun `유저 프로필 업로드 성공 요청시 정상적이라면 에러가 발생하지 않는다`() {
// given

// when

// then
assertThatCode {
pictureUploadSuccessUseCase.userUploadImageSuccess("imageKey")
}.doesNotThrowAnyException()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import com.depromeet.whatnow.domains.picture.repository.PictureRepository
class PictureAdapter(
val pictureRepository: PictureRepository,
) {
fun save(userId: Long, promiseId: Long, imageUrl: String, imageKey: String, pictureCommentType: PictureCommentType) {
val picture = Picture(userId, promiseId, imageUrl, imageKey, pictureCommentType)
pictureRepository.save(picture)
fun saveForPromise(userId: Long, promiseId: Long, imageUrl: String, imageKey: String, pictureCommentType: PictureCommentType): Picture {
val picture = Picture.createForPromise(userId, promiseId, imageUrl, imageKey, pictureCommentType)
return pictureRepository.save(picture)
}

fun saveForUser(userId: Long, imageUrl: String, imageKey: String): Picture {
val picture = Picture.createForUser(userId, imageUrl, imageKey)
return pictureRepository.save(picture)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,40 @@ class Picture(
var uuid: String,

@Enumerated(EnumType.STRING)
var pictureCommentType: PictureCommentType,
var pictureType: PictureType,

@Enumerated(EnumType.STRING)
var pictureCommentType: PictureCommentType = PictureCommentType.NONE,

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "picture_id")
val id: Long? = null,
) : BaseTimeEntity() {
companion object {
fun createForPromise(userId: Long, promiseId: Long, url: String, uuid: String, pictureCommentType: PictureCommentType): Picture {
return Picture(
userId = userId,
promiseId = promiseId,
url = url,
uuid = uuid,
pictureType = PictureType.PROMISE,
pictureCommentType = pictureCommentType,
)
}

fun createForUser(userId: Long, url: String, uuid: String): Picture {
return Picture(
userId = userId,
promiseId = 0,
url = url,
uuid = uuid,
pictureType = PictureType.USER,
pictureCommentType = PictureCommentType.NONE,
)
}
}

Comment on lines +38 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동행객체로 따로 빼는군요 왜 그런건가요? 궁금

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동행객체가 뭔가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static 코틀린에서는 못만들어서 class 안에서 선언할 떄 저렇게 선언하는건데용
Picture 생성하는거 펙토리 메소드로 뺀거죠?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 맞습니다잇~!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM~

@PostPersist
fun createPictureEvent() {
Events.raise(PictureRegisterEvent(userId, promiseId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.depromeet.whatnow.domains.picture.domain

import com.depromeet.whatnow.domains.promiseuser.domain.PromiseUserType

enum class PictureCommentType(val value: String, val promiseUserType: PromiseUserType) {
enum class PictureCommentType(val value: String, val promiseUserType: PromiseUserType?) {
NONE("NONE", null),

// Can LATE
RUNNING("달려가는 중️", PromiseUserType.LATE),
GASPING("헐레벌떡", PromiseUserType.LATE),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.depromeet.whatnow.domains.picture.domain

enum class PictureType {
PROMISE, USER
}
Comment on lines +3 to +5
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 인터랙션은 프론트에서 따로 하니까 미디어로 2가지로만 한거죠?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네넵

Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,30 @@ import com.depromeet.whatnow.domains.picture.exception.InvalidCommentTypeExcepti
import com.depromeet.whatnow.domains.picture.exception.UploadBeforeTrackingException
import com.depromeet.whatnow.domains.promiseuser.adaptor.PromiseUserAdaptor
import com.depromeet.whatnow.domains.promiseuser.domain.PromiseUserType
import com.depromeet.whatnow.domains.user.adapter.UserAdapter
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class PictureDomainService(
val pictureAdapter: PictureAdapter,
val promiseUserAdapter: PromiseUserAdaptor,
val userAdapter: UserAdapter,
) {
@Transactional
fun successUploadImage(userId: Long, promiseId: Long, imageKey: String, pictureCommentType: PictureCommentType) {
fun promiseUploadImageSuccess(userId: Long, promiseId: Long, imageKey: String, pictureCommentType: PictureCommentType) {
val promiseUser = promiseUserAdapter.findByPromiseIdAndUserId(promiseId, userId)
validatePromiseUserType(promiseUser.promiseUserType!!, pictureCommentType)

val imageUrl = IMAGE_DOMAIN + "promise/$promiseId/$imageKey"
pictureAdapter.save(userId, promiseId, imageUrl, imageKey, pictureCommentType)
pictureAdapter.saveForPromise(userId, promiseId, imageUrl, imageKey, pictureCommentType)
}

fun userUploadImageSuccess(userId: Long, imageKey: String) {
val user = userAdapter.queryUser(userId)
val imageUrl = IMAGE_DOMAIN + "user/$userId/$imageKey"
pictureAdapter.saveForUser(userId, imageUrl, imageKey)
user.updateProfileImg(imageUrl)
}

private fun validatePromiseUserType(promiseUserType: PromiseUserType, pictureCommentType: PictureCommentType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ class User(
profileImg = profileImage
nickname = username
}

fun updateProfileImg(imageUrl: String) {
if (profileImg != imageUrl) {
isDefaultImg = false
}
profileImg = imageUrl
}

fun toUserInfoVo(): UserInfoVo {
return UserInfoVo.from(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package com.depromeet.whatnow.domains.picture.adapter

import com.depromeet.whatnow.domains.picture.domain.Picture
import com.depromeet.whatnow.domains.picture.domain.PictureCommentType
import com.depromeet.whatnow.domains.picture.domain.PictureType
import com.depromeet.whatnow.domains.picture.repository.PictureRepository
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.ArgumentCaptor
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.kotlin.then
import org.mockito.kotlin.given
import kotlin.test.assertEquals

@ExtendWith(MockitoExtension::class)
class PictureAdapterTest {
Expand All @@ -21,13 +22,36 @@ class PictureAdapterTest {
lateinit var pictureAdapter: PictureAdapter

@Test
fun `Picture 저장 시 정상적으로 저장된다`() {
val captor: ArgumentCaptor<Picture> = ArgumentCaptor.forClass(Picture::class.java)
fun `약속 이미지 Picture 저장 시 정상적으로 저장된다`() {
given(pictureRepository.save(Mockito.any(Picture::class.java)))
.willReturn(Picture.createForPromise(1, 1, "imageUrl", "imageKey", PictureCommentType.RUNNING))

// when
pictureAdapter.save(1, 1, "imageUrl", "imageKey", PictureCommentType.RUNNING)
val picture = pictureAdapter.saveForPromise(1, 1, "imageUrl", "imageKey", PictureCommentType.RUNNING)

// then
then(pictureRepository).should(Mockito.times(1)).save(captor.capture())
assertEquals(picture.userId, 1)
assertEquals(picture.promiseId, 1)
assertEquals(picture.url, "imageUrl")
assertEquals(picture.uuid, "imageKey")
assertEquals(picture.pictureType, PictureType.PROMISE)
assertEquals(picture.pictureCommentType, PictureCommentType.RUNNING)
}

@Test
fun `유저 프로필 Picture 저장 시 정상적으로 저장된다`() {
given(pictureRepository.save(Mockito.any(Picture::class.java)))
.willReturn(Picture.createForUser(1, "imageUrl", "imageKey"))

// when
val picture = pictureAdapter.saveForUser(1, "imageUrl", "imageKey")

// then
assertEquals(picture.userId, 1)
assertEquals(picture.promiseId, 0)
assertEquals(picture.url, "imageUrl")
assertEquals(picture.uuid, "imageKey")
assertEquals(picture.pictureType, PictureType.USER)
assertEquals(picture.pictureCommentType, PictureCommentType.NONE)
}
}
Loading