diff --git a/build.gradle b/build.gradle index 9dc25e2af..6dd1f49ee 100644 --- a/build.gradle +++ b/build.gradle @@ -24,21 +24,53 @@ repositories { } dependencies { + // Spring Boot 스타터 implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Lombok compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + // Runtime DB 드라이버 runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' - annotationProcessor 'org.projectlombok:lombok' + + // Test testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' // testImplementation 'org.springframework.security:spring-security-test' testImplementation 'com.h2database:h2:2.1.214' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // Querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" +} + +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +// QueryDSL Q-클래스는 compileJava 단계에서만 생성 +tasks.named('compileJava', org.gradle.api.tasks.compile.JavaCompile) { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +// test 컴파일 단계에서는 어노테이션 프로세서를 비워서 Q-클래스 중복 생성 방지 +tasks.named('compileTestJava', org.gradle.api.tasks.compile.JavaCompile) { + options.annotationProcessorPath = files() +} + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +clean.doLast { + file(querydslDir).deleteDir() } tasks.named('test') { diff --git a/src/main/java/konkuk/thip/config/QuerydslConfig.java b/src/main/java/konkuk/thip/config/QuerydslConfig.java new file mode 100644 index 000000000..6e1a55ebd --- /dev/null +++ b/src/main/java/konkuk/thip/config/QuerydslConfig.java @@ -0,0 +1,19 @@ +package konkuk.thip.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index e799f0509..8fcfa2206 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -1,10 +1,12 @@ package konkuk.thip.user.adapter.in.web; import konkuk.thip.common.dto.BaseResponse; -import konkuk.thip.user.adapter.in.web.request.UserSignupRequest; -import konkuk.thip.user.adapter.in.web.response.UserSignupResponse; +import konkuk.thip.user.adapter.in.web.request.PostUserSignupRequest; +import konkuk.thip.user.adapter.in.web.request.PostUserVerifyNicknameRequest; +import konkuk.thip.user.adapter.in.web.response.PostUserSignupResponse; +import konkuk.thip.user.adapter.in.web.response.PostUserVerifyNicknameResponse; import konkuk.thip.user.application.port.in.UserSignupUseCase; -import konkuk.thip.user.application.port.in.dto.UserSignupCommand; +import konkuk.thip.user.application.port.in.VerifyNicknameUseCase; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; @@ -16,11 +18,19 @@ public class UserCommandController { private final UserSignupUseCase userSignupUseCase; + private final VerifyNicknameUseCase verifyNicknameUseCase; @PostMapping("/users/signup") - public BaseResponse signup(@Validated @RequestBody UserSignupRequest request) { - return BaseResponse.ok(UserSignupResponse.of( + public BaseResponse signup(@Validated @RequestBody PostUserSignupRequest request) { + return BaseResponse.ok(PostUserSignupResponse.of( userSignupUseCase.signup(request.toCommand())) ); } + + @PostMapping("/users/nickname") + public BaseResponse verifyNickname(@Validated @RequestBody PostUserVerifyNicknameRequest request) { + return BaseResponse.ok(PostUserVerifyNicknameResponse.of( + verifyNicknameUseCase.isNicknameUnique(request.nickname())) + ); + } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index a7d51c3cc..01ca9f6b0 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -1,10 +1,22 @@ package konkuk.thip.user.adapter.in.web; +import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.user.adapter.in.web.response.GetUserShowAliasChoiceResponse; +import konkuk.thip.user.application.port.in.ShowAliasChoiceViewUseCase; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor public class UserQueryController { + private final ShowAliasChoiceViewUseCase showAliasChoiceViewUseCase; + + @GetMapping("/users/alias") + public BaseResponse showAliasChoiceView() { + return BaseResponse.ok(GetUserShowAliasChoiceResponse.of( + showAliasChoiceViewUseCase.getAllAliasesAndCategories() + )); + } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java similarity index 62% rename from src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java rename to src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java index 96c9725a3..99e45578c 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserSignupRequest.java @@ -1,17 +1,14 @@ package konkuk.thip.user.adapter.in.web.request; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.*; import konkuk.thip.user.application.port.in.dto.UserSignupCommand; -import org.hibernate.validator.constraints.Length; -public record UserSignupRequest( +public record PostUserSignupRequest( @NotNull(message = "aliasId는 필수입니다.") Long aliasId, - @NotBlank(message = "닉네임은 공백일 수 없습니다.") - @Length(max = 10, message = "닉네임은 최대 10자 입니다.") + @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") + @Size(max = 10, message = "닉네임은 최대 10자 입니다.") String nickname, @NotBlank(message = "이메일은 공백일 수 없습니다.") diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java b/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java new file mode 100644 index 000000000..3a8734d9f --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/request/PostUserVerifyNicknameRequest.java @@ -0,0 +1,11 @@ +package konkuk.thip.user.adapter.in.web.request; + +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record PostUserVerifyNicknameRequest( + @Pattern(regexp = "[가-힣a-zA-Z0-9]+", message = "닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)") + @Size(max = 10, message = "닉네임은 최대 10자 입니다.") + String nickname +) { +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java new file mode 100644 index 000000000..b9f3116af --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/GetUserShowAliasChoiceResponse.java @@ -0,0 +1,29 @@ +package konkuk.thip.user.adapter.in.web.response; + +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; + +import java.util.List; + +public record GetUserShowAliasChoiceResponse(List aliasChoices) { + + public static GetUserShowAliasChoiceResponse of(AliasChoiceViewResult result) { + List choices = result.aliasChoices().stream() + .map(ac -> new AliasChoice( + ac.aliasId(), + ac.aliasName(), + ac.categoryName(), + ac.imageUrl(), + ac.color() + )) + .toList(); + return new GetUserShowAliasChoiceResponse(choices); + } + + public record AliasChoice( + Long aliasId, + String aliasName, + String categoryName, + String imageUrl, + String color + ) {} +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java new file mode 100644 index 000000000..c3cbb993c --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserSignupResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.user.adapter.in.web.response; + +public record PostUserSignupResponse(Long userId) { + public static PostUserSignupResponse of(Long userId) { + return new PostUserSignupResponse(userId); + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java new file mode 100644 index 000000000..c254d052e --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/PostUserVerifyNicknameResponse.java @@ -0,0 +1,7 @@ +package konkuk.thip.user.adapter.in.web.response; + +public record PostUserVerifyNicknameResponse(boolean isVerified) { + public static PostUserVerifyNicknameResponse of(boolean isVerified) { + return new PostUserVerifyNicknameResponse(isVerified); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java deleted file mode 100644 index f5a490e6e..000000000 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserSignupResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package konkuk.thip.user.adapter.in.web.response; - -public record UserSignupResponse(Long userId) { - public static UserSignupResponse of(Long userId) { - return new UserSignupResponse(userId); - } -} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java index 655bd4977..130df4c2c 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java @@ -3,5 +3,5 @@ import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface AliasJpaRepository extends JpaRepository { +public interface AliasJpaRepository extends JpaRepository, AliasQueryRepository { } \ No newline at end of file diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java new file mode 100644 index 000000000..5f4e01dd4 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java @@ -0,0 +1,18 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.out.AliasQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class AliasQueryPersistenceAdapter implements AliasQueryPort { + + private final AliasJpaRepository aliasJpaRepository; + + @Override + public AliasChoiceViewResult getAllAliasesAndCategories() { + return aliasJpaRepository.getAllAliasesAndCategories(); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java new file mode 100644 index 000000000..25ce90252 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java @@ -0,0 +1,8 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; + +public interface AliasQueryRepository { + + AliasChoiceViewResult getAllAliasesAndCategories(); +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java new file mode 100644 index 000000000..9cabe24cf --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java @@ -0,0 +1,41 @@ +package konkuk.thip.user.adapter.out.persistence; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import konkuk.thip.room.adapter.out.jpa.QCategoryJpaEntity; +import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class AliasQueryRepositoryImpl implements AliasQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public AliasChoiceViewResult getAllAliasesAndCategories() { + QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; + QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity; + + List aliasChoices = jpaQueryFactory + .select(Projections.constructor( + AliasChoiceViewResult.AliasChoice.class, + alias.aliasId, + alias.value, + category.value, + alias.imageUrl, + alias.color + )) + .from(alias) + .leftJoin(category) + .on(category.aliasForCategoryJpaEntity.eq(alias)) + .orderBy(alias.aliasId.asc()) + .fetch(); + + return new AliasChoiceViewResult(aliasChoices); + } +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java index feeb4f1a8..39798fe6a 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java @@ -3,5 +3,7 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserJpaRepository extends JpaRepository { +public interface UserJpaRepository extends JpaRepository, UserQueryRepository { + + boolean existsByNickname(String nickname); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java index ad2450362..039015537 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java @@ -9,7 +9,11 @@ @RequiredArgsConstructor public class UserQueryPersistenceAdapter implements UserQueryPort { - private final UserJpaRepository jpaRepository; + private final UserJpaRepository userJpaRepository; private final UserMapper userMapper; + @Override + public boolean existsByNickname(String nickname) { + return userJpaRepository.existsByNickname(nickname); + } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java new file mode 100644 index 000000000..72f18a70f --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java @@ -0,0 +1,5 @@ +package konkuk.thip.user.adapter.out.persistence; + +public interface UserQueryRepository { + +} diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java new file mode 100644 index 000000000..09a96a8b3 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java @@ -0,0 +1,11 @@ +package konkuk.thip.user.adapter.out.persistence; + +import org.springframework.stereotype.Repository; + +@Repository +public class UserQueryRepositoryImpl implements UserQueryRepository { + + /** + * QueryDsl 을 활용한 복잡한 조회 로직 구현 + */ +} diff --git a/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java new file mode 100644 index 000000000..994a6cf31 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/ShowAliasChoiceViewUseCase.java @@ -0,0 +1,8 @@ +package konkuk.thip.user.application.port.in; + +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; + +public interface ShowAliasChoiceViewUseCase { + + AliasChoiceViewResult getAllAliasesAndCategories(); +} diff --git a/src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java new file mode 100644 index 000000000..3cca5a9f3 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/VerifyNicknameUseCase.java @@ -0,0 +1,6 @@ +package konkuk.thip.user.application.port.in; + +public interface VerifyNicknameUseCase { + + boolean isNicknameUnique(String nickname); +} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java b/src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java new file mode 100644 index 000000000..3df6ca89e --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/dto/AliasChoiceViewResult.java @@ -0,0 +1,14 @@ +package konkuk.thip.user.application.port.in.dto; + +import java.util.List; + +public record AliasChoiceViewResult(List aliasChoices) { + + public record AliasChoice( + Long aliasId, + String aliasName, + String categoryName, + String imageUrl, + String color + ) {} +} diff --git a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java b/src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java deleted file mode 100644 index 452d64a2e..000000000 --- a/src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package konkuk.thip.user.application.port.in.dto; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class DummyResult { - -} diff --git a/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java new file mode 100644 index 000000000..c832e863a --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java @@ -0,0 +1,8 @@ +package konkuk.thip.user.application.port.out; + +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; + +public interface AliasQueryPort { + + AliasChoiceViewResult getAllAliasesAndCategories(); +} diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java index 448131481..3d2de47c0 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java @@ -2,4 +2,5 @@ public interface UserQueryPort { + boolean existsByNickname(String nickname); } diff --git a/src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java b/src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java new file mode 100644 index 000000000..bc96d2749 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/service/ShowAliasChoiceViewService.java @@ -0,0 +1,19 @@ +package konkuk.thip.user.application.service; + +import konkuk.thip.user.application.port.in.ShowAliasChoiceViewUseCase; +import konkuk.thip.user.application.port.in.dto.AliasChoiceViewResult; +import konkuk.thip.user.application.port.out.AliasQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ShowAliasChoiceViewService implements ShowAliasChoiceViewUseCase { + + private final AliasQueryPort aliasQueryPort; + + @Override + public AliasChoiceViewResult getAllAliasesAndCategories() { + return aliasQueryPort.getAllAliasesAndCategories(); + } +} diff --git a/src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java b/src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java new file mode 100644 index 000000000..f8cae39a1 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/service/VerifyNicknameService.java @@ -0,0 +1,18 @@ +package konkuk.thip.user.application.service; + +import konkuk.thip.user.application.port.in.VerifyNicknameUseCase; +import konkuk.thip.user.application.port.out.UserQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VerifyNicknameService implements VerifyNicknameUseCase { + + private final UserQueryPort userQueryPort; + + @Override + public boolean isNicknameUnique(String nickname) { + return !userQueryPort.existsByNickname(nickname); + } +} diff --git a/src/test/java/konkuk/thip/config/TestQuerydslConfig.java b/src/test/java/konkuk/thip/config/TestQuerydslConfig.java new file mode 100644 index 000000000..70c4d5b51 --- /dev/null +++ b/src/test/java/konkuk/thip/config/TestQuerydslConfig.java @@ -0,0 +1,19 @@ +package konkuk.thip.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class TestQuerydslConfig { + + @PersistenceContext + EntityManager em; + + @Bean + JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } +} diff --git a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java index 2b1d18198..3eef4a2bf 100644 --- a/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java +++ b/src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java @@ -14,12 +14,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @ActiveProfiles("test") +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class FeedJpaEntityTest { @PersistenceContext diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java index a69653bfe..cdfee771e 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; @@ -24,6 +25,7 @@ @DataJpaTest @ActiveProfiles("test") +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class RecordJpaEntityTest { @Autowired diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java index 302a5d683..ecb06c6a3 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java @@ -12,12 +12,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; @DataJpaTest @ActiveProfiles("test") +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class RoomJpaEntityTest { @PersistenceContext private EntityManager em; diff --git a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java index 8c626f0e3..e28fee521 100644 --- a/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java +++ b/src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import java.time.LocalDate; @@ -25,6 +26,7 @@ @DataJpaTest @ActiveProfiles("test") +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class VoteJpaEntityTest { @PersistenceContext diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java new file mode 100644 index 000000000..892e45555 --- /dev/null +++ b/src/test/java/konkuk/thip/user/adapter/in/web/ShowAliasChoiceViewControllerTest.java @@ -0,0 +1,107 @@ +package konkuk.thip.user.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.persistence.CategoryJpaRepository; +import konkuk.thip.user.adapter.in.web.response.GetUserShowAliasChoiceResponse; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +class ShowAliasChoiceViewControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private CategoryJpaRepository categoryJpaRepository; + + @AfterEach + void tearDown() { + categoryJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("현재 DB에 존재하는 모든 [칭호, 카테고리] 정보를 반환한다.") + void show_alias_choice_view() throws Exception { + //given + saveAliasesAndCategories(); + + //when + ResultActions result = mockMvc.perform(get("/users/alias") + .contentType(MediaType.APPLICATION_JSON)); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.aliasChoices").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + GetUserShowAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), GetUserShowAliasChoiceResponse.class); + List choices = showResponse.aliasChoices(); + + assertThat(choices).hasSize(2); + assertThat(choices) + .extracting("aliasName", "categoryName", "imageUrl", "color") + .containsExactlyInAnyOrder( + tuple("문학가", "문학", "문학가_image", "red"), + tuple("과학자", "과학/IT", "과학자_image", "blue") + ); + } + + private void saveAliasesAndCategories() { + AliasJpaEntity alias1 = AliasJpaEntity.builder() + .value("문학가") + .imageUrl("문학가_image") + .color("red") + .build(); + aliasJpaRepository.save(alias1); + + CategoryJpaEntity category1 = CategoryJpaEntity.builder() + .value("문학") + .aliasForCategoryJpaEntity(alias1) + .build(); + categoryJpaRepository.save(category1); + + AliasJpaEntity alias2 = AliasJpaEntity.builder() + .value("과학자") + .imageUrl("과학자_image") + .color("blue") + .build(); + aliasJpaRepository.save(alias2); + + CategoryJpaEntity category2 = CategoryJpaEntity.builder() + .value("과학/IT") + .aliasForCategoryJpaEntity(alias2) + .build(); + categoryJpaRepository.save(category2); + } +} diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java similarity index 75% rename from src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java rename to src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java index fe6ef27c2..cc7d92e83 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java @@ -2,11 +2,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import konkuk.thip.user.adapter.in.web.request.UserSignupRequest; +import konkuk.thip.user.adapter.in.web.request.PostUserSignupRequest; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -27,7 +28,7 @@ @SpringBootTest @ActiveProfiles("test") @AutoConfigureMockMvc -class UserCommandControllerTest { +class UserSignupControllerTest { @Autowired private MockMvc mockMvc; @@ -41,6 +42,12 @@ class UserCommandControllerTest { @Autowired private UserJpaRepository userJpaRepository; + @AfterEach + void tearDown() { + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + @Test @DisplayName("[칭호id, 닉네임, 이메일] 정보를 바탕으로 회원가입을 진행한다.") void signup_success() throws Exception { @@ -52,9 +59,9 @@ void signup_success() throws Exception { .build(); aliasJpaRepository.save(aliasJpaEntity); - UserSignupRequest request = new UserSignupRequest( + PostUserSignupRequest request = new PostUserSignupRequest( aliasJpaEntity.getAliasId(), - "테스트 유저", + "테스트유저", "test@test.com" ); @@ -80,9 +87,9 @@ void signup_success() throws Exception { @Test @DisplayName("[칭호id]값이 null일 경우, 400 error가 발생한다.") - void signup_whenAliasIdNull_thenBadRequest() throws Exception { + void signup_alias_id_null() throws Exception { //given: aliasId null - UserSignupRequest request = new UserSignupRequest( + PostUserSignupRequest request = new PostUserSignupRequest( null, "테스트유저", "test@test.com" @@ -99,9 +106,9 @@ void signup_whenAliasIdNull_thenBadRequest() throws Exception { @Test @DisplayName("[닉네임]값이 공백일 경우, 400 error가 발생한다.") - void signup_whenNicknameBlank_thenBadRequest() throws Exception { + void signup_nickname_blank() throws Exception { //given: nickname blank - UserSignupRequest request = new UserSignupRequest( + PostUserSignupRequest request = new PostUserSignupRequest( 1L, "", "test@test.com" @@ -113,16 +120,35 @@ void signup_whenNicknameBlank_thenBadRequest() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) - .andExpect(jsonPath("$.message", containsString("닉네임은 공백일 수 없습니다."))); + .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"))); + } + + @Test + @DisplayName("[닉네임]값이 한글, 영어, 숫자 외의 문자를 포함할 경우, 400 error가 발생한다.") + void signup_nickname_invalid_pattern() throws Exception { + //given: nickname with invalid characters + PostUserSignupRequest request = new PostUserSignupRequest( + 1L, + "닉네임!!", + "test@test.com" + ); + + //when //then + mockMvc.perform(post("/users/signup") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"))); } @Test @DisplayName("[닉네임]값이 11자 이상일 경우, 400 error가 발생한다.") - void signup_whenNicknameTooLong_thenBadRequest() throws Exception { - //given: nickname blank - UserSignupRequest request = new UserSignupRequest( + void signup_nickname_too_long() throws Exception { + //given: 11글자 nickname + PostUserSignupRequest request = new PostUserSignupRequest( 1L, - "11자_닉네임_입니다", + "11글자닉네임입니다아", "test@test.com" ); @@ -137,9 +163,9 @@ void signup_whenNicknameTooLong_thenBadRequest() throws Exception { @Test @DisplayName("[이메일]값이 공백일 경우, 400 error가 발생한다.") - void signup_whenEmailBlank_thenBadRequest() throws Exception { + void signup_email_blank() throws Exception { //given - UserSignupRequest request = new UserSignupRequest( + PostUserSignupRequest request = new PostUserSignupRequest( 1L, "테스트유저", "" @@ -156,9 +182,9 @@ void signup_whenEmailBlank_thenBadRequest() throws Exception { @Test @DisplayName("[이메일]값이 유효한 이메일 형식이 아닐 경우, 400 error가 발생한다.") - void signup_whenEmailInvalidFormat_thenBadRequest() throws Exception { + void signup_email_invalid_format() throws Exception { //given - UserSignupRequest request = new UserSignupRequest( + PostUserSignupRequest request = new PostUserSignupRequest( 1L, "테스트유저", "invalid-email-format" diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java new file mode 100644 index 000000000..a2c04a7f3 --- /dev/null +++ b/src/test/java/konkuk/thip/user/adapter/in/web/VerifyNicknameControllerTest.java @@ -0,0 +1,156 @@ +package konkuk.thip.user.adapter.in.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.user.adapter.in.web.request.PostUserVerifyNicknameRequest; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.UserJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import static konkuk.thip.common.exception.code.ErrorCode.API_INVALID_PARAM; +import static konkuk.thip.user.adapter.out.jpa.UserRole.USER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +class VerifyNicknameControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @AfterEach + void tearDown() { + userJpaRepository.deleteAll(); + aliasJpaRepository.deleteAll(); + } + + @Test + @DisplayName("[닉네임]값이 unique 할 경우, true를 반환한다.") + void verify_nickname_true() throws Exception { + //given + PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("테스트유저"); + + //when + ResultActions result = mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isVerified").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); + + assertThat(isVerified).isTrue(); + } + + @Test + @DisplayName("[닉네임]값이 이미 DB에 존재하는 경우, false를 반환한다.") + void verify_nickname_false() throws Exception { + //given: DB에 "테스트유저" 생성 + AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder() + .value("칭호") + .color("blue") + .imageUrl("http://image.url") + .build(); + aliasJpaRepository.save(aliasJpaEntity); + + UserJpaEntity userJpaEntity = UserJpaEntity.builder() + .email("test@test.com") + .nickname("테스트유저") + .imageUrl("http://image.url") + .role(USER) + .aliasForUserJpaEntity(aliasJpaEntity) + .build(); + userJpaRepository.save(userJpaEntity); + + PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("테스트유저"); + + //when + ResultActions result = mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.isVerified").exists()); + + String json = result.andReturn().getResponse().getContentAsString(); + JsonNode jsonNode = objectMapper.readTree(json); + boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean(); + + assertThat(isVerified).isFalse(); + } + + @Test + @DisplayName("[닉네임]값이 공백일 경우, 400 error가 발생한다.") + void nickname_blank() throws Exception { + //given: nickname blank + PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest(""); + + //when //then + mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"))); + } + + @Test + @DisplayName("[닉네임]값이 한글, 영어, 숫자 외의 문자를 포함할 경우, 400 error가 발생한다.") + void nickname_invalid_pattern() throws Exception { + //given: nickname with invalid characters + PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("닉네임!!"); + + //when //then + mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"))); + } + + @Test + @DisplayName("[닉네임]값이 11자 이상일 경우, 400 error가 발생한다.") + void nickname_too_long() throws Exception { + //given: 11글자 nickname + PostUserVerifyNicknameRequest request = new PostUserVerifyNicknameRequest("11글자닉네임입니다아"); + + //when //then + mockMvc.perform(post("/users/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) + .andExpect(jsonPath("$.message", containsString("닉네임은 최대 10자 입니다."))); + } +} diff --git a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java index afbe8ab44..fc2056ac3 100644 --- a/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java +++ b/src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java @@ -8,12 +8,14 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @ActiveProfiles("test") +@Import(konkuk.thip.config.TestQuerydslConfig.class) // DataJpaTest 이므로 JPA 제외 빈 추가로 import class UserJpaEntityTest { @PersistenceContext