From 223496e2074509eee6581108da5b601642b85ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=9D=B8=EC=A4=80?= <54973090+dlswns2480@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:28:30 +0900 Subject: [PATCH] =?UTF-8?q?[feat=20#183]=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat : UserImage 엔티티, 도메인 * feat : 프로필 수정 API * feat : 프로필 수정 usecase 구현 * feat : 닉네임 중복 체크 쿼리(본인 제외한 중복) * feat : test 코드 의존 클래스 추가 --- .../kotlin/com/pokit/user/UserController.kt | 16 +++++++++--- .../user/dto/request/UpdateProfileRequest.kt | 16 ++++++++++++ .../out/persistence/user/impl/UserAdapter.kt | 4 +++ .../persistence/user/impl/UserImageAdapter.kt | 23 ++++++++++++++++ .../persistence/user/persist/UserEntity.kt | 12 ++++++--- .../user/persist/UserImageEntity.kt | 26 +++++++++++++++++++ .../user/persist/UserImageRepository.kt | 6 +++++ .../user/persist/UserRepository.kt | 2 ++ .../com/pokit/user/port/in/UserUseCase.kt | 3 +++ .../com/pokit/user/port/out/UserImagePort.kt | 9 +++++++ .../com/pokit/user/port/out/UserPort.kt | 2 ++ .../pokit/user/port/service/UserService.kt | 22 +++++++++++++++- .../user/port/service/UserServiceTest.kt | 8 ++++-- .../com/pokit/user/dto/request/UserCommand.kt | 6 +++++ .../com/pokit/user/exception/UserErrorCode.kt | 3 ++- .../main/kotlin/com/pokit/user/model/User.kt | 8 +++++- .../kotlin/com/pokit/user/model/UserImage.kt | 6 +++++ 17 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 adapters/in-web/src/main/kotlin/com/pokit/user/dto/request/UpdateProfileRequest.kt create mode 100644 adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserImageAdapter.kt create mode 100644 adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageEntity.kt create mode 100644 adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageRepository.kt create mode 100644 application/src/main/kotlin/com/pokit/user/port/out/UserImagePort.kt create mode 100644 domain/src/main/kotlin/com/pokit/user/dto/request/UserCommand.kt create mode 100644 domain/src/main/kotlin/com/pokit/user/model/UserImage.kt diff --git a/adapters/in-web/src/main/kotlin/com/pokit/user/UserController.kt b/adapters/in-web/src/main/kotlin/com/pokit/user/UserController.kt index 257c7a47..1d1e0e1d 100644 --- a/adapters/in-web/src/main/kotlin/com/pokit/user/UserController.kt +++ b/adapters/in-web/src/main/kotlin/com/pokit/user/UserController.kt @@ -4,10 +4,7 @@ import com.pokit.auth.config.ErrorOperation import com.pokit.auth.model.PrincipalUser import com.pokit.auth.model.toDomain import com.pokit.common.wrapper.ResponseWrapper.wrapOk -import com.pokit.user.dto.request.ApiCreateFcmTokenRequest -import com.pokit.user.dto.request.ApiSignUpRequest -import com.pokit.user.dto.request.ApiUpdateNicknameRequest -import com.pokit.user.dto.request.toDto +import com.pokit.user.dto.request.* import com.pokit.user.dto.response.CheckDuplicateNicknameResponse import com.pokit.user.dto.response.InterestTypeResponse import com.pokit.user.dto.response.UserResponse @@ -90,4 +87,15 @@ class UserController( .toResponse() .wrapOk() } + + @PutMapping + @Operation(summary = "유저 프로필 수정 API") + fun updateProfile( + @AuthenticationPrincipal user: PrincipalUser, + @RequestBody request: UpdateProfileRequest + ): ResponseEntity { + return userUseCase.updateProfile(user.id, request.toDto()) + .toResponse() + .wrapOk() + } } diff --git a/adapters/in-web/src/main/kotlin/com/pokit/user/dto/request/UpdateProfileRequest.kt b/adapters/in-web/src/main/kotlin/com/pokit/user/dto/request/UpdateProfileRequest.kt new file mode 100644 index 00000000..6ab2d0b9 --- /dev/null +++ b/adapters/in-web/src/main/kotlin/com/pokit/user/dto/request/UpdateProfileRequest.kt @@ -0,0 +1,16 @@ +package com.pokit.user.dto.request + +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + +data class UpdateProfileRequest( + val profileImageId: Int, + @field:NotBlank(message = "닉네임은 필수값입니다.") + @field:Size(max = 10, message = "닉네임은 10자 이하만 가능합니다.") + val nickname: String +) + +internal fun UpdateProfileRequest.toDto() = UserCommand( + profileImageId = this.profileImageId, + nickname = this.nickname +) diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserAdapter.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserAdapter.kt index bbf7fe89..c46be41f 100644 --- a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserAdapter.kt +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserAdapter.kt @@ -27,6 +27,10 @@ class UserAdapter( ?.run { toDomain() } override fun checkByNickname(nickname: String) = userRepository.existsByNickname(nickname) + + override fun checkByNickname(nickname: String, userId: Long) = + userRepository.existsByNicknameAndIdNot(nickname, userId) + override fun delete(user: User) { userRepository.findByIdOrNull(user.id) ?.delete() diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserImageAdapter.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserImageAdapter.kt new file mode 100644 index 00000000..898763dc --- /dev/null +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/impl/UserImageAdapter.kt @@ -0,0 +1,23 @@ +package com.pokit.out.persistence.user.impl + +import com.pokit.out.persistence.user.persist.UserImageRepository +import com.pokit.out.persistence.user.persist.toDomain +import com.pokit.user.model.UserImage +import com.pokit.user.port.out.UserImagePort +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Repository + +@Repository +class UserImageAdapter( + private val userImageRepository: UserImageRepository +) : UserImagePort { + override fun loadById(id: Int): UserImage? { + return userImageRepository.findByIdOrNull(id) + ?.toDomain() + } + + override fun loadAll(): List { + return userImageRepository.findAll() + .map { it.toDomain() } + } +} diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserEntity.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserEntity.kt index a34724b6..6e878dfa 100644 --- a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserEntity.kt +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserEntity.kt @@ -35,7 +35,11 @@ class UserEntity( var registered: Boolean, @Column(name = "sub") - var sub: String? + var sub: String?, + + @OneToOne + @JoinColumn(name = "image_id", foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)) + val image: UserImageEntity? ) : BaseEntity() { fun delete() { this.deleted = true @@ -50,7 +54,8 @@ class UserEntity( nickname = user.nickName, authPlatform = user.authPlatform, registered = user.registered, - sub = user.sub + sub = user.sub, + image = user.image?.let { UserImageEntity.of(it) } ) } } @@ -62,5 +67,6 @@ fun UserEntity.toDomain() = User( nickName = this.nickname, authPlatform = this.authPlatform, registered = this.registered, - sub = this.sub + sub = this.sub, + image = this.image?.toDomain() ) diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageEntity.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageEntity.kt new file mode 100644 index 00000000..aea35572 --- /dev/null +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageEntity.kt @@ -0,0 +1,26 @@ +package com.pokit.out.persistence.user.persist + +import com.pokit.user.model.UserImage +import jakarta.persistence.* + +@Table(name = "USER_IMAGE") +@Entity +class UserImageEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + val id: Int = 0, + + @Column(name = "url") + val url: String +) { + companion object { + fun of(userImage: UserImage) = + UserImageEntity( + id = userImage.id, + url = userImage.url + ) + } +} + +fun UserImageEntity.toDomain() = UserImage(this.id, this.url) diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageRepository.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageRepository.kt new file mode 100644 index 00000000..f95b6d4b --- /dev/null +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserImageRepository.kt @@ -0,0 +1,6 @@ +package com.pokit.out.persistence.user.persist + +import org.springframework.data.jpa.repository.JpaRepository + +interface UserImageRepository : JpaRepository { +} diff --git a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserRepository.kt b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserRepository.kt index 0ffddccf..ddf3bf38 100644 --- a/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserRepository.kt +++ b/adapters/out-persistence/src/main/kotlin/com/pokit/out/persistence/user/persist/UserRepository.kt @@ -8,6 +8,8 @@ interface UserRepository : JpaRepository { fun existsByNickname(nickname: String): Boolean + fun existsByNicknameAndIdNot(nickname: String, userId: Long): Boolean + fun findByIdAndDeleted(id: Long, deleted: Boolean): UserEntity? fun findByDeleted(deleted: Boolean): List diff --git a/application/src/main/kotlin/com/pokit/user/port/in/UserUseCase.kt b/application/src/main/kotlin/com/pokit/user/port/in/UserUseCase.kt index 347b2c2d..c5574c9d 100644 --- a/application/src/main/kotlin/com/pokit/user/port/in/UserUseCase.kt +++ b/application/src/main/kotlin/com/pokit/user/port/in/UserUseCase.kt @@ -3,6 +3,7 @@ package com.pokit.user.port.`in` import com.pokit.user.dto.request.CreateFcmTokenRequest import com.pokit.user.dto.request.SignUpRequest import com.pokit.user.dto.request.UpdateNicknameRequest +import com.pokit.user.dto.request.UserCommand import com.pokit.user.model.FcmToken import com.pokit.user.model.User @@ -18,4 +19,6 @@ interface UserUseCase { fun createFcmToken(userId: Long, request: CreateFcmTokenRequest): FcmToken fun getUserInfo(userId: Long): User + + fun updateProfile(userId: Long, command: UserCommand): User } diff --git a/application/src/main/kotlin/com/pokit/user/port/out/UserImagePort.kt b/application/src/main/kotlin/com/pokit/user/port/out/UserImagePort.kt new file mode 100644 index 00000000..b2e914ff --- /dev/null +++ b/application/src/main/kotlin/com/pokit/user/port/out/UserImagePort.kt @@ -0,0 +1,9 @@ +package com.pokit.user.port.out + +import com.pokit.user.model.UserImage + +interface UserImagePort { + fun loadById(id: Int): UserImage? + + fun loadAll(): List +} diff --git a/application/src/main/kotlin/com/pokit/user/port/out/UserPort.kt b/application/src/main/kotlin/com/pokit/user/port/out/UserPort.kt index 4a9daaa0..10d86275 100644 --- a/application/src/main/kotlin/com/pokit/user/port/out/UserPort.kt +++ b/application/src/main/kotlin/com/pokit/user/port/out/UserPort.kt @@ -12,6 +12,8 @@ interface UserPort { fun checkByNickname(nickname: String): Boolean + fun checkByNickname(nickname: String, userId: Long): Boolean + fun delete(user: User) fun loadAllIds(): List diff --git a/application/src/main/kotlin/com/pokit/user/port/service/UserService.kt b/application/src/main/kotlin/com/pokit/user/port/service/UserService.kt index 8c0d3cd3..d897f883 100644 --- a/application/src/main/kotlin/com/pokit/user/port/service/UserService.kt +++ b/application/src/main/kotlin/com/pokit/user/port/service/UserService.kt @@ -11,11 +11,13 @@ import com.pokit.common.exception.NotFoundCustomException import com.pokit.user.dto.request.CreateFcmTokenRequest import com.pokit.user.dto.request.SignUpRequest import com.pokit.user.dto.request.UpdateNicknameRequest +import com.pokit.user.dto.request.UserCommand import com.pokit.user.exception.UserErrorCode import com.pokit.user.model.FcmToken import com.pokit.user.model.User import com.pokit.user.port.`in`.UserUseCase import com.pokit.user.port.out.FcmTokenPort +import com.pokit.user.port.out.UserImagePort import com.pokit.user.port.out.UserPort import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -26,7 +28,8 @@ class UserService( private val userPort: UserPort, private val categoryPort: CategoryPort, private val categoryImagePort: CategoryImagePort, - private val fcmTokenPort: FcmTokenPort + private val fcmTokenPort: FcmTokenPort, + private val userImagePort: UserImagePort ) : UserUseCase { companion object { private const val UNCATEGORIZED_IMAGE_ID = 1 @@ -92,4 +95,21 @@ class UserService( return userPort.loadById(userId) ?: throw NotFoundCustomException(UserErrorCode.NOT_FOUND_USER) } + + @Transactional + override fun updateProfile(userId: Long, command: UserCommand): User { + val user = userPort.loadById(userId) + ?: throw NotFoundCustomException(UserErrorCode.NOT_FOUND_USER) + + val image = userImagePort.loadById(command.profileImageId) + ?: throw NotFoundCustomException(UserErrorCode.NOT_FOUND_PROFILE_IMAGE) + + val isDuplicate = userPort.checkByNickname(command.nickname, userId) + if (isDuplicate) { + throw ClientValidationException(UserErrorCode.ALREADY_EXISTS_NICKNAME) + } + + user.modifyProfile(image, command.nickname) + return userPort.persist(user) + } } diff --git a/application/src/test/kotlin/com/pokit/user/port/service/UserServiceTest.kt b/application/src/test/kotlin/com/pokit/user/port/service/UserServiceTest.kt index 884ed56e..c84ec027 100644 --- a/application/src/test/kotlin/com/pokit/user/port/service/UserServiceTest.kt +++ b/application/src/test/kotlin/com/pokit/user/port/service/UserServiceTest.kt @@ -10,7 +10,9 @@ import com.pokit.token.model.AuthPlatform import com.pokit.user.UserFixture import com.pokit.user.dto.request.UpdateNicknameRequest import com.pokit.user.model.User +import com.pokit.user.model.UserImage import com.pokit.user.port.out.FcmTokenPort +import com.pokit.user.port.out.UserImagePort import com.pokit.user.port.out.UserPort import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec @@ -23,12 +25,14 @@ class UserServiceTest : BehaviorSpec({ val categoryPort = mockk() val categoryImagePort = mockk() val fcmTokenPort = mockk() - val userService = UserService(userPort, categoryPort, categoryImagePort, fcmTokenPort) + val userImagePort = mockk() + val userService = UserService(userPort, categoryPort, categoryImagePort, fcmTokenPort, userImagePort) Given("회원을 등록할 때") { val user = UserFixture.getUser() val invalidUser = UserFixture.getInvalidUser() val request = UserFixture.getSignUpRequest() - val modifieUser = User(user.id, user.email, user.role, request.nickName, AuthPlatform.GOOGLE, sub = "sub") + val userImage = UserImage(1, "url") + val modifieUser = User(user.id, user.email, user.role, request.nickName, AuthPlatform.GOOGLE, sub = "sub", image = userImage) val image = CategoryImage(1, "https://www.image.com") val unCategorized = CategoryFixture.getUnCategorized(user.id, image) diff --git a/domain/src/main/kotlin/com/pokit/user/dto/request/UserCommand.kt b/domain/src/main/kotlin/com/pokit/user/dto/request/UserCommand.kt new file mode 100644 index 00000000..7c4617cb --- /dev/null +++ b/domain/src/main/kotlin/com/pokit/user/dto/request/UserCommand.kt @@ -0,0 +1,6 @@ +package com.pokit.user.dto.request + +data class UserCommand( + val profileImageId: Int, + val nickname: String +) diff --git a/domain/src/main/kotlin/com/pokit/user/exception/UserErrorCode.kt b/domain/src/main/kotlin/com/pokit/user/exception/UserErrorCode.kt index 8693f114..e16f33a3 100644 --- a/domain/src/main/kotlin/com/pokit/user/exception/UserErrorCode.kt +++ b/domain/src/main/kotlin/com/pokit/user/exception/UserErrorCode.kt @@ -10,5 +10,6 @@ enum class UserErrorCode( INVALID_INTEREST_TYPE("관심사가 잘못되었습니다.", "U_002"), NOT_FOUND_USER("존재하지 않는 회원입니다.", "U_003"), ALREADY_EXISTS_NICKNAME("이미 존재하는 닉네임입니다.", "U_004"), - ALREADY_REGISTERED("이미 회원가입 한 유저입니다.", "U_005") + ALREADY_REGISTERED("이미 회원가입 한 유저입니다.", "U_005"), + NOT_FOUND_PROFILE_IMAGE("존재하지 않는 프로필 이미지입니다.", "U_006"), } diff --git a/domain/src/main/kotlin/com/pokit/user/model/User.kt b/domain/src/main/kotlin/com/pokit/user/model/User.kt index 7b899ed1..5f772c3c 100644 --- a/domain/src/main/kotlin/com/pokit/user/model/User.kt +++ b/domain/src/main/kotlin/com/pokit/user/model/User.kt @@ -12,7 +12,8 @@ data class User( var nickName: String = "NOT_REGISTERED", val authPlatform: AuthPlatform, var registered: Boolean = false, - var sub: String? + var sub: String?, + var image: UserImage? = null ) { fun register(nickName: String) { this.nickName = nickName @@ -27,6 +28,11 @@ data class User( this.nickName = nickName } + fun modifyProfile(image: UserImage, nickname: String) { + this.image = image + this.nickName = nickname + } + init { val pattern = Pattern.compile( diff --git a/domain/src/main/kotlin/com/pokit/user/model/UserImage.kt b/domain/src/main/kotlin/com/pokit/user/model/UserImage.kt new file mode 100644 index 00000000..0dbb4dc1 --- /dev/null +++ b/domain/src/main/kotlin/com/pokit/user/model/UserImage.kt @@ -0,0 +1,6 @@ +package com.pokit.user.model + +data class UserImage( + val id: Int, + val url: String +)