diff --git a/application/src/main/kotlin/com/threedays/application/user/port/inbound/PutProfileWidget.kt b/application/src/main/kotlin/com/threedays/application/user/port/inbound/PutProfileWidget.kt new file mode 100644 index 0000000..4257467 --- /dev/null +++ b/application/src/main/kotlin/com/threedays/application/user/port/inbound/PutProfileWidget.kt @@ -0,0 +1,14 @@ +package com.threedays.application.user.port.inbound + +import com.threedays.domain.user.entity.ProfileWidget +import com.threedays.domain.user.entity.User + +interface PutProfileWidget { + + fun invoke(command: Command) + + data class Command( + val userId: User.Id, + val profileWidget: ProfileWidget + ) +} diff --git a/application/src/main/kotlin/com/threedays/application/user/service/UserService.kt b/application/src/main/kotlin/com/threedays/application/user/service/UserService.kt index 7380819..0c3e4a2 100644 --- a/application/src/main/kotlin/com/threedays/application/user/service/UserService.kt +++ b/application/src/main/kotlin/com/threedays/application/user/service/UserService.kt @@ -2,6 +2,7 @@ package com.threedays.application.user.service import com.threedays.application.auth.config.AuthProperties import com.threedays.application.auth.port.inbound.IssueLoginTokens +import com.threedays.application.user.port.inbound.PutProfileWidget import com.threedays.application.user.port.inbound.RegisterUser import com.threedays.domain.user.entity.Company import com.threedays.domain.user.entity.Location @@ -19,7 +20,7 @@ class UserService( private val companyQueryRepository: CompanyQueryRepository, private val issueLoginTokens: IssueLoginTokens, private val authProperties: AuthProperties, -) : RegisterUser { +) : RegisterUser, PutProfileWidget { @Transactional override fun invoke(command: RegisterUser.Command): RegisterUser.Result { @@ -53,4 +54,12 @@ class UserService( ) } + @Transactional + override fun invoke(command: PutProfileWidget.Command) { + userRepository + .get(command.userId) + .putProfileWidget(command.profileWidget) + .also { userRepository.save(it) } + } + } diff --git a/bootstrap/api/src/main/kotlin/com/threedays/bootstrap/api/user/UserController.kt b/bootstrap/api/src/main/kotlin/com/threedays/bootstrap/api/user/UserController.kt index bdaf028..4c68353 100644 --- a/bootstrap/api/src/main/kotlin/com/threedays/bootstrap/api/user/UserController.kt +++ b/bootstrap/api/src/main/kotlin/com/threedays/bootstrap/api/user/UserController.kt @@ -1,6 +1,8 @@ package com.threedays.bootstrap.api.user +import com.threedays.application.user.port.inbound.PutProfileWidget import com.threedays.application.user.port.inbound.RegisterUser +import com.threedays.bootstrap.api.support.security.UserAuthentication import com.threedays.bootstrap.api.support.security.withUserAuthentication import com.threedays.domain.auth.vo.PhoneNumber import com.threedays.domain.user.entity.User @@ -11,6 +13,7 @@ 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.GetMyUserInfoResponse +import com.threedays.oas.model.ProfileWidget import com.threedays.oas.model.RegisterUserRequest import com.threedays.oas.model.TokenResponse import com.threedays.oas.model.UserProfile @@ -24,6 +27,7 @@ import java.time.Year @RestController class UserController( private val registerUser: RegisterUser, + private val putProfileWidget: PutProfileWidget, private val userRepository: UserRepository, ) : UsersApi { @@ -101,7 +105,28 @@ class UserController( ) }, preferDistance = com.threedays.oas.model.PreferDistance.valueOf(user.desiredPartner.preferDistance.name), - ) + ), + profileWidgets = user.profile.profileWidgets.map { + ProfileWidget( + type = com.threedays.oas.model.ProfileWidgetType.valueOf(it.type.name), + content = it.content, + ) + } ).let { ResponseEntity.ok(it) } } + + override fun putProfileWidget(body: ProfileWidget): ResponseEntity = + withUserAuthentication { userAuthentication: UserAuthentication -> + val command = PutProfileWidget.Command( + userId = userAuthentication.userId, + profileWidget = com.threedays.domain.user.entity.ProfileWidget( + type = com.threedays.domain.user.entity.ProfileWidget.Type.valueOf(body.type.name), + content = body.content + ) + ) + + putProfileWidget + .invoke(command) + .let { ResponseEntity.ok(body) } + } } diff --git a/domain/src/main/kotlin/com/threedays/domain/user/entity/ProfileWidget.kt b/domain/src/main/kotlin/com/threedays/domain/user/entity/ProfileWidget.kt index 160d80d..fe00235 100644 --- a/domain/src/main/kotlin/com/threedays/domain/user/entity/ProfileWidget.kt +++ b/domain/src/main/kotlin/com/threedays/domain/user/entity/ProfileWidget.kt @@ -2,7 +2,7 @@ package com.threedays.domain.user.entity data class ProfileWidget( val type: Type, - val comment: String, + val content: String, ) { enum class Type { diff --git a/domain/src/main/kotlin/com/threedays/domain/user/entity/User.kt b/domain/src/main/kotlin/com/threedays/domain/user/entity/User.kt index a19b103..d55a4ad 100644 --- a/domain/src/main/kotlin/com/threedays/domain/user/entity/User.kt +++ b/domain/src/main/kotlin/com/threedays/domain/user/entity/User.kt @@ -86,4 +86,8 @@ data class User( } } + fun putProfileWidget(widget: ProfileWidget): User { + return copy(profile = profile.putProfileWidget(widget)) + } + } diff --git a/domain/src/main/kotlin/com/threedays/domain/user/entity/UserProfile.kt b/domain/src/main/kotlin/com/threedays/domain/user/entity/UserProfile.kt index f62067c..4c2e9ee 100644 --- a/domain/src/main/kotlin/com/threedays/domain/user/entity/UserProfile.kt +++ b/domain/src/main/kotlin/com/threedays/domain/user/entity/UserProfile.kt @@ -13,4 +13,17 @@ data class UserProfile( val jobOccupation: JobOccupation, val locations: List, val profileWidgets: List = emptyList(), -) : DomainEntity() +) : DomainEntity() { + + fun putProfileWidget(profileWidget: ProfileWidget): UserProfile { + val existingProfileWidget: ProfileWidget? = + profileWidgets.find { it.type == profileWidget.type } + + return if (existingProfileWidget == null) { + copy(profileWidgets = profileWidgets + profileWidget) + } else { + copy(profileWidgets = profileWidgets.map { if (it.type == profileWidget.type) profileWidget else it }) + } + } + +} diff --git a/domain/src/main/kotlin/com/threedays/domain/user/repository/UserRepository.kt b/domain/src/main/kotlin/com/threedays/domain/user/repository/UserRepository.kt index 8ce3f58..17ed73d 100644 --- a/domain/src/main/kotlin/com/threedays/domain/user/repository/UserRepository.kt +++ b/domain/src/main/kotlin/com/threedays/domain/user/repository/UserRepository.kt @@ -7,6 +7,9 @@ import com.threedays.support.common.exception.NotFoundException interface UserRepository : Repository { + fun get(id: User.Id): User = find(id) + ?: throw NotFoundException("User not found by id: $id") + fun findByPhoneNumber(phoneNumber: PhoneNumber): User? fun getByPhoneNumber(phoneNumber: PhoneNumber): User = findByPhoneNumber(phoneNumber) ?: throw NotFoundException("User not found by phone number: $phoneNumber") diff --git a/domain/src/test/kotlin/com/threedays/domain/user/entity/UserTest.kt b/domain/src/test/kotlin/com/threedays/domain/user/entity/UserTest.kt index 037c2e8..155b391 100644 --- a/domain/src/test/kotlin/com/threedays/domain/user/entity/UserTest.kt +++ b/domain/src/test/kotlin/com/threedays/domain/user/entity/UserTest.kt @@ -3,6 +3,7 @@ package com.threedays.domain.user.entity import com.navercorp.fixturemonkey.FixtureMonkey import com.navercorp.fixturemonkey.kotlin.giveMeBuilder import com.navercorp.fixturemonkey.kotlin.introspector.PrimaryConstructorArbitraryIntrospector +import com.navercorp.fixturemonkey.kotlin.set import com.threedays.domain.auth.vo.PhoneNumber import com.threedays.domain.user.vo.BirthYearRange import com.threedays.domain.user.vo.Gender @@ -163,5 +164,65 @@ class UserTest : DescribeSpec({ } } + describe("putProfileWidget - 프로필 위젯 추가") { + it("프로필 위젯을 추가한다") { + // arrange + val profileWidget: ProfileWidget = fixtureMonkey + .giveMeBuilder() + .sample() + + val userDesiredPartner: UserDesiredPartner = fixtureMonkey + .giveMeBuilder() + .set(UserDesiredPartner::allowSameCompany, null) + .sample() + + val user: User = fixtureMonkey + .giveMeBuilder() + .set(User::phoneNumber, PhoneNumber("01012345678")) + .set(User::desiredPartner, userDesiredPartner) + .sample() + + // act + val result: User = user.putProfileWidget(profileWidget) + + // assert + result.profile.profileWidgets.find { it == profileWidget } shouldBe profileWidget + } + + ProfileWidget.Type.entries.forEach { widgetType -> + context("동일한 타입($widgetType)의 위젯이 이미 있는 경우") { + it("$widgetType 타입의 위젯 내용을 수정한다") { + // arrange + val profileWidget: ProfileWidget = fixtureMonkey + .giveMeBuilder() + .set(ProfileWidget::type, widgetType) + .sample() + + val userDesiredPartner: UserDesiredPartner = fixtureMonkey + .giveMeBuilder() + .set(UserDesiredPartner::allowSameCompany, null) + .sample() + + val user: User = fixtureMonkey + .giveMeBuilder() + .set(User::phoneNumber, PhoneNumber("01012345678")) + .set(User::desiredPartner, userDesiredPartner) + .sample() + .putProfileWidget(profileWidget) + + val updatedProfileWidget: ProfileWidget = fixtureMonkey + .giveMeBuilder() + .set(ProfileWidget::type, widgetType) + .sample() + + // act + val result: User = user.putProfileWidget(updatedProfileWidget) + + // assert + result.profile.profileWidgets.find { it == updatedProfileWidget } shouldBe updatedProfileWidget + } + } + } + } }) diff --git a/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/ProfileWidgetJpaEmbeddable.kt b/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/ProfileWidgetJpaEmbeddable.kt new file mode 100644 index 0000000..62a8fc5 --- /dev/null +++ b/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/ProfileWidgetJpaEmbeddable.kt @@ -0,0 +1,29 @@ +package com.threedays.persistence.user.entity + +import com.threedays.domain.user.entity.ProfileWidget +import jakarta.persistence.Embeddable +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated + +@Embeddable +data class ProfileWidgetJpaEmbeddable( + @Enumerated(EnumType.STRING) + val type: ProfileWidget.Type, + val content: String, +) { + + companion object { + + fun ProfileWidget.toJpaEmbeddable() = ProfileWidgetJpaEmbeddable( + type = type, + content = content, + ) + + } + + fun toValueObject() = ProfileWidget( + type = type, + content = content, + ) + +} diff --git a/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/UserProfileJpaEntity.kt b/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/UserProfileJpaEntity.kt index 9890cb9..dd55560 100644 --- a/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/UserProfileJpaEntity.kt +++ b/infrastructure/persistence/src/main/kotlin/com/threedays/persistence/user/entity/UserProfileJpaEntity.kt @@ -6,8 +6,10 @@ import com.threedays.domain.user.entity.UserProfile import com.threedays.domain.user.vo.Gender import com.threedays.persistence.user.entity.CompanyJpaEntity.Companion.toJpaEntity import com.threedays.persistence.user.entity.LocationJpaEntity.Companion.toJpaEntity +import com.threedays.persistence.user.entity.ProfileWidgetJpaEmbeddable.Companion.toJpaEmbeddable import jakarta.persistence.CollectionTable import jakarta.persistence.Column +import jakarta.persistence.ElementCollection import jakarta.persistence.Entity import jakarta.persistence.EnumType import jakarta.persistence.Enumerated @@ -29,6 +31,7 @@ class UserProfileJpaEntity( company: CompanyJpaEntity?, jobOccupation: JobOccupation, locations: List, + profileWidgets: List, ) { @Id @@ -62,6 +65,14 @@ class UserProfileJpaEntity( var locations: List = locations private set + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "user_profile_widgets", + joinColumns = [JoinColumn(name = "user_profile_id")], + ) + var profileWidgets: List = profileWidgets + private set + companion object { fun UserProfile.toJpaEntity() = UserProfileJpaEntity( @@ -71,6 +82,7 @@ class UserProfileJpaEntity( company = company?.toJpaEntity(), jobOccupation = jobOccupation, locations = locations.map { it.toJpaEntity() }, + profileWidgets = profileWidgets.map { it.toJpaEmbeddable() } ) } @@ -82,6 +94,7 @@ class UserProfileJpaEntity( company = company?.toDomainEntity(), jobOccupation = jobOccupation, locations = locations.map { it.toDomainEntity() }, + profileWidgets = profileWidgets.map { it.toValueObject() } ) } diff --git a/infrastructure/persistence/src/main/resources/db/migration/V1__init_user.sql b/infrastructure/persistence/src/main/resources/db/migration/V1__init_user.sql index eaea076..def08ff 100644 --- a/infrastructure/persistence/src/main/resources/db/migration/V1__init_user.sql +++ b/infrastructure/persistence/src/main/resources/db/migration/V1__init_user.sql @@ -59,3 +59,12 @@ CREATE TABLE users CONSTRAINT pk_users PRIMARY KEY (id), CONSTRAINT uq_users_phone_number UNIQUE (phone_number) ); + +CREATE TABLE user_profile_widgets +( + user_profile_id BINARY(16) NOT NULL, + type VARCHAR(255) NOT NULL, + content VARCHAR(255) NOT NULL, + PRIMARY KEY (user_profile_id, type), + FOREIGN KEY (user_profile_id) REFERENCES user_profiles (id) ON DELETE CASCADE +); diff --git a/openapi b/openapi index f81da79..2214f58 160000 --- a/openapi +++ b/openapi @@ -1 +1 @@ -Subproject commit f81da79dc8537bde60d3ec02da999d4369f74a1e +Subproject commit 2214f58319ca9b6a5a59e1e2c9d73cc8d293020a