diff --git a/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java b/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java index 5581c06f..111c3dfd 100644 --- a/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java +++ b/cakk-api/src/main/java/com/cakk/api/service/user/SignService.java @@ -14,8 +14,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.UserWriter; import com.cakk.domain.redis.repository.TokenRedisRepository; @Service @@ -26,13 +26,13 @@ public class SignService { private final JwtProvider jwtProvider; private final UserReader userReader; - private final UserWriter userWriter; + private final UserCommandFacade userCommandFacade; private final TokenRedisRepository tokenRedisRepository; @Transactional public JwtResponse signUp(final UserSignUpRequest dto) { final String providerId = oidcProviderFactory.getProviderId(dto.provider(), dto.idToken()); - final User user = userWriter.create(UserMapper.supplyUserBy(dto, providerId)); + final User user = userCommandFacade.create(UserMapper.supplyUserBy(dto, providerId)); return JwtResponse.from(jwtProvider.generateToken(user)); } diff --git a/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java b/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java index 13ff7168..96c545f7 100644 --- a/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java +++ b/cakk-api/src/main/java/com/cakk/api/service/user/UserService.java @@ -11,21 +11,15 @@ import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.entity.user.UserWithdrawal; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.BusinessInformationWriter; -import com.cakk.domain.mysql.repository.writer.CakeHeartWriter; -import com.cakk.domain.mysql.repository.writer.CakeShopHeartWriter; -import com.cakk.domain.mysql.repository.writer.UserWriter; @Service @RequiredArgsConstructor public class UserService { private final UserReader userReader; - private final UserWriter userWriter; - private final CakeShopHeartWriter cakeShopHeartWriter; - private final CakeHeartWriter cakeHeartWriter; - private final BusinessInformationWriter businessInformationWriter; + private final UserCommandFacade userCommandFacade; @Transactional(readOnly = true) public ProfileInformationResponse findProfile(final User signInUser) { @@ -39,18 +33,14 @@ public void updateInformation(final User signInUser, final ProfileUpdateRequest final User user = userReader.findByUserId(signInUser.getId()); final ProfileUpdateParam param = UserMapper.supplyProfileUpdateParamBy(dto); - user.updateProfile(param); + userCommandFacade.updateProfile(user, param); } @Transactional public void withdraw(final User signInUser) { - final User user = userReader.findByUserId(signInUser.getId()); + final User user = userReader.findByIdWithAll(signInUser.getId()); final UserWithdrawal withdrawal = UserMapper.supplyUserWithdrawalBy(user); - cakeHeartWriter.deleteAllByUser(user); - cakeShopHeartWriter.deleteAllByUser(user); - businessInformationWriter.deleteAllByUser(user); - - userWriter.delete(user, withdrawal); + userCommandFacade.withdraw(user, withdrawal); } } diff --git a/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java b/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java index 7af3e991..43e3dc34 100644 --- a/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/common/base/ServiceTest.java @@ -1,5 +1,7 @@ package com.cakk.api.common.base; +import java.time.LocalDate; + import org.junit.jupiter.api.extension.ExtendWith; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; @@ -18,6 +20,7 @@ import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin; import com.cakk.common.enums.Provider; +import com.cakk.common.enums.Role; import com.cakk.domain.mysql.config.JpaConfig; import com.cakk.domain.mysql.entity.user.User; @@ -55,10 +58,14 @@ protected final FixtureMonkey getBuilderMonkey() { } protected User getUser() { - return getReflectionMonkey().giveMeBuilder(User.class) + return getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("birthday", LocalDate.now()) + .set("role", Arbitraries.of(Role.class)) .sample(); } diff --git a/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java b/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java index 0c1012b6..a971ab99 100644 --- a/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java +++ b/cakk-api/src/test/java/com/cakk/api/integration/user/MyPageIntegrationTest.java @@ -4,7 +4,6 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.*; import java.time.LocalDate; -import java.util.Map; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -28,11 +27,12 @@ import com.cakk.common.exception.CakkException; import com.cakk.common.response.ApiResponse; import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.reader.UserReader; @SqlGroup({ @Sql(scripts = { - "/sql/insert-test-user.sql" + "/sql/insert-test-user.sql", + "/sql/insert-cake-shop.sql", + "/sql/insert-heart.sql", }, executionPhase = BEFORE_TEST_METHOD), @Sql(scripts = "/sql/delete-all.sql", executionPhase = AFTER_TEST_METHOD) }) diff --git a/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java b/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java index fcdfaf8c..c86a0c0d 100644 --- a/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/service/user/SignServiceTest.java @@ -22,8 +22,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.UserWriter; import com.cakk.domain.redis.repository.TokenRedisRepository; @DisplayName("Sign 관련 비즈니스 로직 테스트") @@ -42,7 +42,7 @@ class SignServiceTest extends ServiceTest { private UserReader userReader; @Mock - private UserWriter userWriter; + private UserCommandFacade userCommandFacade; @Mock private TokenRedisRepository tokenRedisRepository; @@ -51,7 +51,7 @@ class SignServiceTest extends ServiceTest { void signUp1() { // given UserSignUpRequest dto = getConstructorMonkey().giveMeOne(UserSignUpRequest.class); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) @@ -63,7 +63,7 @@ void signUp1() { .sample(); doReturn(user.getProviderId()).when(oidcProviderFactory).getProviderId(dto.provider(), dto.idToken()); - doReturn(user).when(userWriter).create(any(User.class)); + doReturn(user).when(userCommandFacade).create(any(User.class)); doReturn(jwt).when(jwtProvider).generateToken(user); // when @@ -75,7 +75,7 @@ void signUp1() { Assertions.assertNotNull(result.grantType()); verify(oidcProviderFactory, times(1)).getProviderId(dto.provider(), dto.idToken()); - verify(userWriter, times(1)).create(any(User.class)); + verify(userCommandFacade, times(1)).create(any(User.class)); verify(jwtProvider, times(1)).generateToken(user); } @@ -83,7 +83,7 @@ void signUp1() { void signUp2() { // given UserSignUpRequest dto = getConstructorMonkey().giveMeOne(UserSignUpRequest.class); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) @@ -98,7 +98,7 @@ void signUp2() { ReturnCode.EXPIRED_JWT_TOKEN.getMessage()); verify(oidcProviderFactory, times(1)).getProviderId(dto.provider(), dto.idToken()); - verify(userWriter, times(0)).create(any(User.class)); + verify(userCommandFacade, times(0)).create(any(User.class)); verify(jwtProvider, times(0)).generateToken(user); } @@ -107,7 +107,7 @@ void signIn() { // given UserSignInRequest dto = getConstructorMonkey().giveMeOne(UserSignInRequest.class); String providerId = Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20).sample(); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", providerId) @@ -140,7 +140,7 @@ void signIn2() { // given UserSignInRequest dto = getConstructorMonkey().giveMeOne(UserSignInRequest.class); String providerId = Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20).sample(); - User user = getReflectionMonkey().giveMeBuilder(User.class) + User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", dto.provider()) .set("providerId", providerId) @@ -164,7 +164,7 @@ void signIn2() { void recreateToken() { // given final String refreshToken = "refresh token"; - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("providerId", Arbitraries.strings().alpha().ofMinLength(10).ofMaxLength(20)) .set("createdAt", LocalDateTime.now()) diff --git a/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java b/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java index efbcbf1f..5eaf6d6b 100644 --- a/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java +++ b/cakk-api/src/test/java/com/cakk/api/service/user/UserServiceTest.java @@ -17,11 +17,8 @@ import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; import com.cakk.domain.mysql.repository.reader.UserReader; -import com.cakk.domain.mysql.repository.writer.BusinessInformationWriter; -import com.cakk.domain.mysql.repository.writer.CakeHeartWriter; -import com.cakk.domain.mysql.repository.writer.CakeShopHeartWriter; -import com.cakk.domain.mysql.repository.writer.UserWriter; @DisplayName("유저 관련 비즈니스 로직 테스트") class UserServiceTest extends ServiceTest { @@ -33,21 +30,12 @@ class UserServiceTest extends ServiceTest { private UserReader userReader; @Mock - private UserWriter userWriter; - - @Mock - private CakeShopHeartWriter cakeShopHeartWriter; - - @Mock - private CakeHeartWriter cakeHeartWriter; - - @Mock - private BusinessInformationWriter businessInformationWriter; + private UserCommandFacade userCommandFacade; @TestWithDisplayName("유저 프로필을 조회한다.") void findProfile1() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -67,7 +55,7 @@ void findProfile1() { @TestWithDisplayName("유저가 존재하지 않으면 유저 프로필 조회에 실패한다.") void findProfile2() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -86,7 +74,7 @@ void findProfile2() { @TestWithDisplayName("유저 프로필을 수정한다.") void updateInformation() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) + final User user = getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("provider", Arbitraries.of(Provider.class)) .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) @@ -104,44 +92,30 @@ void updateInformation() { @TestWithDisplayName("유저를 탈퇴한다.") void withdraw1() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) - .set("id", Arbitraries.longs().greaterOrEqual(10)) - .set("provider", Arbitraries.of(Provider.class)) - .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) - .sample(); + final User user = getUser(); - doReturn(user).when(userReader).findByUserId(user.getId()); + doReturn(user).when(userReader).findByIdWithAll(user.getId()); // when & then Assertions.assertDoesNotThrow(() -> userService.withdraw(user)); - verify(userReader, times(1)).findByUserId(user.getId()); - verify(cakeHeartWriter, times(1)).deleteAllByUser(user); - verify(cakeShopHeartWriter, times(1)).deleteAllByUser(user); - verify(businessInformationWriter, times(1)).deleteAllByUser(user); - verify(userWriter, times(1)).delete(any(), any()); + verify(userReader, times(1)).findByIdWithAll(user.getId()); + verify(userCommandFacade, times(1)).withdraw(any(), any()); } @TestWithDisplayName("유저가 없는 경우, 탈퇴에 실패한다.") void withdraw2() { // given - final User user = getReflectionMonkey().giveMeBuilder(User.class) - .set("id", Arbitraries.longs().greaterOrEqual(10)) - .set("provider", Arbitraries.of(Provider.class)) - .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) - .sample(); + final User user = getUser(); - doThrow(new CakkException(ReturnCode.NOT_EXIST_USER)).when(userReader).findByUserId(user.getId()); + doThrow(new CakkException(ReturnCode.NOT_EXIST_USER)).when(userReader).findByIdWithAll(user.getId()); // when & then Assertions.assertThrows(CakkException.class, () -> userService.withdraw(user), ReturnCode.NOT_EXIST_USER.getMessage()); - verify(userReader, times(1)).findByUserId(user.getId()); - verify(cakeHeartWriter, times(0)).deleteAllByUser(user); - verify(cakeShopHeartWriter, times(0)).deleteAllByUser(user); - verify(businessInformationWriter, times(0)).deleteAllByUser(user); - verify(userWriter, times(0)).delete(any(), any()); + verify(userReader, times(1)).findByIdWithAll(user.getId()); + verify(userCommandFacade, never()).withdraw(any(), any()); } } diff --git a/cakk-domain/mysql/build.gradle b/cakk-domain/mysql/build.gradle index 289af826..0ce2d0f0 100644 --- a/cakk-domain/mysql/build.gradle +++ b/cakk-domain/mysql/build.gradle @@ -11,6 +11,8 @@ dependencies { testImplementation("com.navercorp.fixturemonkey:fixture-monkey-starter:1.0.23") testImplementation('org.assertj:assertj-core') testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation('org.mockito:mockito-core') + testImplementation('org.mockito:mockito-junit-jupiter') testRuntimeOnly('org.junit.platform:junit-platform-launcher') // querydsl diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java index 0f463261..b843c963 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/BusinessInformation.java @@ -56,7 +56,7 @@ public class BusinessInformation extends AuditEntity { @ColumnDefault("0") @Convert(converter = VerificationStatusConverter.class) @Column(name = "verification_status", nullable = false) - private VerificationStatus verificationStatus = VerificationStatus.UNREQUESTED; + private VerificationStatus verificationStatus; @OneToOne @MapsId @@ -85,6 +85,11 @@ public void updateBusinessOwner(final VerificationPolicy verificationPolicy, fin verificationStatus = verificationPolicy.approveToBusinessOwner(verificationStatus); } + public void unLinkBusinessOwner() { + user = null; + verificationStatus = VerificationStatus.UNREQUESTED; + } + public boolean isBusinessOwnerCandidate(VerificationPolicy verificationPolicy) { return verificationPolicy.isCandidate(Objects.requireNonNull(verificationStatus)); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java index e2ae1556..7884c6a7 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/entity/user/User.java @@ -2,8 +2,11 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -11,10 +14,12 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import org.hibernate.annotations.ColumnDefault; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; @@ -31,7 +36,10 @@ import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; import com.cakk.domain.mysql.entity.audit.AuditEntity; import com.cakk.domain.mysql.entity.cake.Cake; +import com.cakk.domain.mysql.entity.cake.CakeHeart; import com.cakk.domain.mysql.entity.shop.CakeShop; +import com.cakk.domain.mysql.entity.shop.CakeShopHeart; +import com.cakk.domain.mysql.entity.shop.CakeShopLike; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -83,6 +91,22 @@ public class User extends AuditEntity { @Column(name = "deleted_at") private LocalDateTime deletedAt; + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) + private Set businessInformationSet = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeHearts = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeShopHearts = new HashSet<>(); + + @JsonIgnore + @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true) + private Set cakeShopLikes = new HashSet<>(); + @Builder public User( Provider provider, @@ -136,6 +160,12 @@ public void unHeartCakeShop(final CakeShop cakeShop) { cakeShop.unHeart(this); } + public void unHeartAndLikeAll() { + cakeHearts.clear(); + cakeShopHearts.clear(); + cakeShopLikes.clear(); + } + @Override public boolean equals(Object object) { if (this == object) { diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java similarity index 59% rename from cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java rename to cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java index e32ba9b5..e1b6b465 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/UserWriter.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/facade/user/UserCommandFacade.java @@ -1,18 +1,20 @@ -package com.cakk.domain.mysql.repository.writer; +package com.cakk.domain.mysql.facade.user; import lombok.RequiredArgsConstructor; import com.cakk.common.enums.ReturnCode; import com.cakk.common.exception.CakkException; -import com.cakk.domain.mysql.annotation.Writer; +import com.cakk.domain.mysql.annotation.DomainFacade; +import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; +import com.cakk.domain.mysql.entity.user.BusinessInformation; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.entity.user.UserWithdrawal; import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; import com.cakk.domain.mysql.repository.jpa.UserWithdrawalJpaRepository; -@Writer @RequiredArgsConstructor -public class UserWriter { +@DomainFacade +public class UserCommandFacade { private final UserJpaRepository userJpaRepository; private final UserWithdrawalJpaRepository userWithdrawalJpaRepository; @@ -26,7 +28,14 @@ public User create(final User user) { return userJpaRepository.save(user); } - public void delete(final User user, final UserWithdrawal withdrawal) { + public void updateProfile(final User user, final ProfileUpdateParam param) { + user.updateProfile(param); + } + + public void withdraw(final User user, final UserWithdrawal withdrawal) { + user.unHeartAndLikeAll(); + user.getBusinessInformationSet().forEach(BusinessInformation::unLinkBusinessOwner); + userWithdrawalJpaRepository.save(withdrawal); userJpaRepository.delete(user); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java new file mode 100644 index 00000000..bbc1f515 --- /dev/null +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/query/UserQueryRepository.java @@ -0,0 +1,33 @@ +package com.cakk.domain.mysql.repository.query; + +import static com.cakk.domain.mysql.entity.user.QUser.*; + +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import lombok.RequiredArgsConstructor; + +import com.cakk.domain.mysql.entity.user.User; + +@Repository +@RequiredArgsConstructor +public class UserQueryRepository { + + private final JPAQueryFactory queryFactory; + + public User searchByIdWithAll(final Long userId) { + return queryFactory.selectFrom(user) + .leftJoin(user.businessInformationSet).fetchJoin() + .leftJoin(user.cakeHearts).fetchJoin() + .leftJoin(user.cakeShopHearts).fetchJoin() + .leftJoin(user.cakeShopLikes).fetchJoin() + .where(eqUserId(userId)) + .fetchOne(); + } + + private BooleanExpression eqUserId(final Long userId) { + return user.id.eq(userId); + } +} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java index a1bd6094..8bc4dd7f 100644 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java +++ b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/reader/UserReader.java @@ -1,5 +1,7 @@ package com.cakk.domain.mysql.repository.reader; +import static java.util.Objects.*; + import java.util.List; import lombok.RequiredArgsConstructor; @@ -9,12 +11,14 @@ import com.cakk.domain.mysql.annotation.Reader; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; +import com.cakk.domain.mysql.repository.query.UserQueryRepository; @Reader @RequiredArgsConstructor public class UserReader { private final UserJpaRepository userJpaRepository; + private final UserQueryRepository userQueryRepository; public User findByUserId(final Long userId) { return userJpaRepository.findById(userId).orElseThrow(() -> new CakkException(ReturnCode.NOT_EXIST_USER)); @@ -24,6 +28,16 @@ public User findByProviderId(final String providerId) { return userJpaRepository.findByProviderId(providerId).orElseThrow(() -> new CakkException(ReturnCode.NOT_EXIST_USER)); } + public User findByIdWithAll(final Long userId) { + final User user = userQueryRepository.searchByIdWithAll(userId); + + if (isNull(user)) { + throw new CakkException(ReturnCode.NOT_EXIST_USER); + } + + return user; + } + public List findAll() { return userJpaRepository.findAll(); } diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java deleted file mode 100644 index 20e2393d..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/BusinessInformationWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import static java.util.Objects.*; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.user.BusinessInformation; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.BusinessInformationJpaRepository; - -@Writer -@RequiredArgsConstructor -public class BusinessInformationWriter { - - private final BusinessInformationJpaRepository businessInformationJpaRepository; - - public void deleteAllByUser(final User user) { - List businessInformationList = businessInformationJpaRepository.findAllByUser(user); - - if (isNull(businessInformationList) || businessInformationList.isEmpty()) { - return; - } - - businessInformationJpaRepository.deleteAllInBatch(businessInformationList); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java deleted file mode 100644 index 41e0a9e9..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeHeartWriter.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import static java.util.Objects.*; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.cake.CakeHeart; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.CakeHeartJpaRepository; - -@Writer -@RequiredArgsConstructor -public class CakeHeartWriter { - - private final CakeHeartJpaRepository cakeHeartJpaRepository; - - public void deleteAllByUser(final User user) { - final List cakeHearts = cakeHeartJpaRepository.findAllByUser(user); - - if (isNull(cakeHearts) || cakeHearts.isEmpty()) { - return; - } - - cakeHeartJpaRepository.deleteAllInBatch(cakeHearts); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java deleted file mode 100644 index 7c35e04f..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopHeartWriter.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.shop.CakeShopHeart; -import com.cakk.domain.mysql.entity.user.User; -import com.cakk.domain.mysql.repository.jpa.CakeShopHeartJpaRepository; - -@Writer -@RequiredArgsConstructor -public class CakeShopHeartWriter { - - private final CakeShopHeartJpaRepository cakeShopHeartJpaRepository; - - public void deleteAllByUser(final User user) { - final List cakeShopHearts = cakeShopHeartJpaRepository.findAllByUser(user); - - if (cakeShopHearts == null || cakeShopHearts.isEmpty()) { - return; - } - - cakeShopHeartJpaRepository.deleteAllInBatch(cakeShopHearts); - } -} diff --git a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java b/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java deleted file mode 100644 index 486e1a5f..00000000 --- a/cakk-domain/mysql/src/main/java/com/cakk/domain/mysql/repository/writer/CakeShopLikeWriter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.cakk.domain.mysql.repository.writer; - -import lombok.RequiredArgsConstructor; - -import com.cakk.domain.mysql.annotation.Writer; -import com.cakk.domain.mysql.entity.shop.CakeShop; -import com.cakk.domain.mysql.entity.user.User; - -@Writer -@RequiredArgsConstructor -public class CakeShopLikeWriter { - - public void like(final CakeShop cakeShop, final User user) { - user.likeCakeShop(cakeShop); - } -} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java new file mode 100644 index 00000000..ff42f323 --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/annotation/TestWithDisplayName.java @@ -0,0 +1,41 @@ +package com.cakk.domain.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Test +@DisplayNameGeneration(TestWithDisplayName.TestDisplayNameGenerator.class) +public @interface TestWithDisplayName { + + String value() default ""; + + class TestDisplayNameGenerator extends DisplayNameGenerator.Standard { + + @Override + public String generateDisplayNameForClass(Class testClass) { + final TestWithDisplayName testWithDisplayName = testClass.getAnnotation(TestWithDisplayName.class); + + if (testWithDisplayName != null && !testWithDisplayName.value().isEmpty()) { + return testWithDisplayName.value(); + } + + return super.generateDisplayNameForClass(testClass); + } + + @Override + public String generateDisplayNameForMethod(Class testClass, Method testMethod) { + return testMethod.getName(); + } + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java index d02275ba..6c61d86e 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/DomainTest.java @@ -59,7 +59,7 @@ protected static Point supplyPointBy(Double latitude, Double longitude) { } protected User getUserFixture(Role role) { - return getReflectionMonkey().giveMeBuilder(User.class) + return getConstructorMonkey().giveMeBuilder(User.class) .set("id", Arbitraries.longs().greaterOrEqual(10)) .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) .set("role", role) @@ -94,7 +94,7 @@ protected BusinessInformation getBusinessInformationFixtureWithCakeShop(Verifica } protected CertificationParam getCertificationParamFixtureWithUser(User user) { - return getBuilderMonkey().giveMeBuilder(CertificationParam.class) + return getConstructorMonkey().giveMeBuilder(CertificationParam.class) .set("businessRegistrationImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(20)) .set("idCardImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(20)) .set("cakeShopId", Arbitraries.longs().greaterOrEqual(0)) diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java new file mode 100644 index 00000000..9d8942ac --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/base/FacadeTest.java @@ -0,0 +1,94 @@ +package com.cakk.domain.base; + +import java.time.LocalDate; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.PrecisionModel; +import org.mockito.junit.jupiter.MockitoExtension; + +import net.jqwik.api.Arbitraries; + +import com.navercorp.fixturemonkey.FixtureMonkey; +import com.navercorp.fixturemonkey.api.introspector.BuilderArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.introspector.ConstructorPropertiesArbitraryIntrospector; +import com.navercorp.fixturemonkey.api.introspector.FieldReflectionArbitraryIntrospector; +import com.navercorp.fixturemonkey.customizer.Values; +import com.navercorp.fixturemonkey.jakarta.validation.plugin.JakartaValidationPlugin; + +import com.cakk.common.enums.Provider; +import com.cakk.common.enums.Role; +import com.cakk.domain.mysql.entity.cake.Cake; +import com.cakk.domain.mysql.entity.shop.CakeShop; +import com.cakk.domain.mysql.entity.user.User; + +@ExtendWith(MockitoExtension.class) +public abstract class FacadeTest { + + private static final int SPATIAL_REFERENCE_IDENTIFIER_NUMBER = 4326; + + private static final GeometryFactory geometryFactory = new GeometryFactory( + new PrecisionModel(), + SPATIAL_REFERENCE_IDENTIFIER_NUMBER + ); + + protected final FixtureMonkey getConstructorMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(ConstructorPropertiesArbitraryIntrospector.INSTANCE) + .build(); + } + + protected final FixtureMonkey getReflectionMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(FieldReflectionArbitraryIntrospector.INSTANCE) + .build(); + } + + protected final FixtureMonkey getBuilderMonkey() { + return FixtureMonkey.builder() + .plugin(new JakartaValidationPlugin()) + .objectIntrospector(BuilderArbitraryIntrospector.INSTANCE) + .build(); + } + + protected User getUserFixture(final Role role) { + return getConstructorMonkey().giveMeBuilder(User.class) + .set("id", Arbitraries.longs().greaterOrEqual(10)) + .set("provider", Arbitraries.of(Provider.class)) + .set("providerId", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(50)) + .set("birthday", LocalDate.now()) + .set("role", role) + .sample(); + } + + protected Cake getCakeFixture() { + return getConstructorMonkey().giveMeBuilder(Cake.class) + .set("cakeImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("cakeShop", Values.just(getCakeShopFixture())) + .sample(); + } + + protected CakeShop getCakeShopFixture() { + return getConstructorMonkey().giveMeBuilder(CakeShop.class) + .set("shopName", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(30)) + .set("shopBio", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(40)) + .set("shopDescription", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(500)) + .set("likeCount", 0) + .set("heartCount", 0) + .set("location", supplyPointBy( + Arbitraries.doubles().between(-90, 90).sample(), + Arbitraries.doubles().between(-180, 180).sample()) + ) + .sample(); + } + + public static Point supplyPointBy(Double latitude, Double longitude) { + return geometryFactory.createPoint(new Coordinate(longitude, latitude)); + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java new file mode 100644 index 00000000..69cbf4d3 --- /dev/null +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserCommandFacadeTest.java @@ -0,0 +1,113 @@ +package com.cakk.domain.facade.user; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Optional; + +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import net.jqwik.api.Arbitraries; + +import com.cakk.common.enums.Gender; +import com.cakk.common.enums.ReturnCode; +import com.cakk.common.enums.Role; +import com.cakk.common.exception.CakkException; +import com.cakk.domain.annotation.TestWithDisplayName; +import com.cakk.domain.base.FacadeTest; +import com.cakk.domain.mysql.dto.param.user.ProfileUpdateParam; +import com.cakk.domain.mysql.entity.user.User; +import com.cakk.domain.mysql.entity.user.UserWithdrawal; +import com.cakk.domain.mysql.facade.user.UserCommandFacade; +import com.cakk.domain.mysql.repository.jpa.UserJpaRepository; +import com.cakk.domain.mysql.repository.jpa.UserWithdrawalJpaRepository; + +class UserCommandFacadeTest extends FacadeTest { + + @InjectMocks + private UserCommandFacade userCommandFacade; + + @Mock + private UserJpaRepository userJpaRepository; + + @Mock + private UserWithdrawalJpaRepository userWithdrawalJpaRepository; + + @TestWithDisplayName("유저를 생성한다") + void create() { + // given + final User user = getUserFixture(Role.USER); + + // when + userCommandFacade.create(user); + + // then + verify(userJpaRepository, times(1)).findByProviderId(user.getProviderId()); + verify(userJpaRepository, times(1)).save(user); + } + + @TestWithDisplayName("유저가 이미 있으면 User 생성에 실패한다") + void create2() { + // given + final User user = getUserFixture(Role.USER); + + doReturn(Optional.of(user)).when(userJpaRepository).findByProviderId(user.getProviderId()); + + // when + assertThrows( + CakkException.class, + () -> userCommandFacade.create(user), + ReturnCode.ALREADY_EXIST_USER.getMessage() + ); + + // then + verify(userJpaRepository, times(1)).findByProviderId(user.getProviderId()); + verify(userJpaRepository, never()).save(user); + } + + @TestWithDisplayName("유저 정보를 수정한다") + void updateProfile() { + // given + final User user = getUserFixture(Role.USER); + final ProfileUpdateParam param = getConstructorMonkey().giveMeBuilder(ProfileUpdateParam.class) + .set("profileImageUrl", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(100)) + .set("nickname", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(30)) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("gender", Arbitraries.of(Gender.class)) + .set("birthday", LocalDate.now()) + .sample(); + + // when + userCommandFacade.updateProfile(user, param); + + // then + assertEquals(param.profileImageUrl(), user.getProfileImageUrl()); + assertEquals(param.nickname(), user.getNickname()); + assertEquals(param.email(), user.getEmail()); + assertEquals(param.gender(), user.getGender()); + assertEquals(param.birthday(), user.getBirthday()); + } + + @TestWithDisplayName("유저를 탈퇴한다") + void withdraw() { + // given + final User user = getUserFixture(Role.USER); + final UserWithdrawal withdrawal = getConstructorMonkey().giveMeBuilder(UserWithdrawal.class) + .set("email", Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(50)) + .set("gender", Arbitraries.of(Gender.class)) + .set("birthday", LocalDate.now()) + .set("role", Arbitraries.of(Role.class)) + .set("withdrawalDate", LocalDateTime.now()) + .sample(); + + // when + userCommandFacade.withdraw(user, withdrawal); + + // then + verify(userWithdrawalJpaRepository, times(1)).save(any()); + verify(userJpaRepository, times(1)).delete(user); + } +} diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java index 4bd57d61..2ab46c38 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserHeartFacadeTest.java @@ -4,17 +4,19 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import com.cakk.common.enums.Role; -import com.cakk.domain.base.DomainTest; +import com.cakk.domain.base.FacadeTest; import com.cakk.domain.mysql.entity.cake.Cake; import com.cakk.domain.mysql.entity.shop.CakeShop; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.facade.user.UserHeartFacade; -class UserHeartFacadeTest extends DomainTest { +class UserHeartFacadeTest extends FacadeTest { - private final UserHeartFacade userHeartFacade = new UserHeartFacade(); + @InjectMocks + private UserHeartFacade userHeartFacade; @Test @DisplayName("케이크 하트를 성공한다") diff --git a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java index 27dfc58d..08ccf240 100644 --- a/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java +++ b/cakk-domain/mysql/src/test/java/com/cakk/domain/facade/user/UserLikeFacadeTest.java @@ -7,18 +7,20 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import com.cakk.common.enums.ReturnCode; import com.cakk.common.enums.Role; import com.cakk.common.exception.CakkException; -import com.cakk.domain.base.DomainTest; +import com.cakk.domain.base.FacadeTest; import com.cakk.domain.mysql.entity.shop.CakeShop; import com.cakk.domain.mysql.entity.user.User; import com.cakk.domain.mysql.facade.user.UserLikeFacade; -class UserLikeFacadeTest extends DomainTest { +class UserLikeFacadeTest extends FacadeTest { - private final UserLikeFacade userLikeFacade = new UserLikeFacade(); + @InjectMocks + private UserLikeFacade userLikeFacade; @Test @DisplayName("케이크 샵 기대돼요 동작을 성공한다")