diff --git a/.github/workflows/cicd-ec2-prod.yml b/.github/workflows/cicd-ec2-prod.yml index 18583aa0..dcf3ce14 100644 --- a/.github/workflows/cicd-ec2-prod.yml +++ b/.github/workflows/cicd-ec2-prod.yml @@ -3,7 +3,7 @@ name: Build and Deploy to PROD on: push: - branches: [ "main" ] + branches: [ "main-ec2" ] # 환경 변수 $변수명으로 사용 env: diff --git a/.github/workflows/cicd-light-sail-prod.yml b/.github/workflows/cicd-light-sail-prod.yml new file mode 100644 index 00000000..05c7dbf1 --- /dev/null +++ b/.github/workflows/cicd-light-sail-prod.yml @@ -0,0 +1,244 @@ +name: Build and Deploy to PROD (Lightsail Blue/Green via SSH + Docker) + +on: + push: + branches: [ "main" ] + +env: + # --- 애플리케이션/컨테이너 공통 --- + PROJECT_NAME: "devdevdev" + IMAGE_NAME: "devdevdev/app" # 로컬 빌드 이미지 이름(태그 latest, SHA) + CONTAINER_BASE: "devdevdev-main-server" # 컨테이너 베이스명 + BLUE_SUFFIX: "-blue" + GREEN_SUFFIX: "-green" + + # --- 포트 구성 --- + BLUE_PORT: "18080" # 호스트 포트(Blue) + GREEN_PORT: "18081" # 호스트 포트(Green) + APP_PORT: "8080" # 컨테이너 내부 포트(Spring Boot) + + # --- 헬스체크 --- + HEALTHCHECK_PATH: "/actuator/health" # Actuator 미사용이면 "/" 로 변경하세요 + HEALTHCHECK_TIMEOUT: "3" # curl 타임아웃(초) + HEALTHCHECK_RETRY: "20" # 재시도 횟수 (5초 * 20 = 최대 100초) + + # --- SSH/Lightsail --- + SSH_USER: "ec2-user" # SSH 접속 사용자 + LIGHTSAIL_HOST: "${{ secrets.LIGHTSAIL_HOST }}" # 퍼블릭 IP 또는 도메인 + +jobs: + build: + name: Build and Deploy (Blue/Green) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + # ====== 리소스/시크릿 주입 (현재 파이프라인과 동일) ====== + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: 21 + distribution: corretto + + - name: make application-prod.yml + run: | + cd ./src/main/resources + echo "${{ secrets.application_prod }}" >> ./application-prod.yml + echo "${{ secrets.application_jwt_prod }}" >> ./application-jwt-prod.yml + echo "${{ secrets.application_oauth2_prod }}" >> ./application-oauth2-prod.yml + echo "${{ secrets.application_storage_s3_prod }}" >> ./application-storage-s3-prod.yml + echo "${{ secrets.application_open_ai }}" >> ./application-open-ai.yml + echo "${{ secrets.application_opensearch_prod }}" >> ./application-opensearch-prod.yml + + - name: make application-test.yml + run: | + cd ./src/test/resources + echo "${{ secrets.application_storage_s3 }}" >> ./application-storage-s3.yml + echo "${{ secrets.application_open_ai }}" >> ./application-open-ai.yml + echo "${{ secrets.application_opensearch_test }}" >> ./application-opensearch-test.yml + + # ====== Gradle 빌드 (Docker가 JAR을 COPY할 수 있도록 선행) ====== + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle (bootJar) + run: ./gradlew bootJar -x test -x asciidoctor + + # ====== Docker 빌드 ====== + - name: Use Dockerfile-prod if present + run: | + if [ -f Dockerfile-prod ]; then + rm -f Dockerfile + cp Dockerfile-prod Dockerfile + fi + + - name: Build Docker image + run: | + docker build \ + -t ${IMAGE_NAME}:${GITHUB_SHA} \ + -t ${IMAGE_NAME}:latest \ + . + + - name: Save image as archive + run: | + mkdir -p out + # SHA와 latest 두 태그 모두 아카이브에 포함 + docker save ${IMAGE_NAME}:${GITHUB_SHA} ${IMAGE_NAME}:latest | gzip > out/image.tar.gz + echo "ARCHIVE=out/image.tar.gz" >> $GITHUB_ENV + + # ====== SSH 준비 ====== + - name: Prepare SSH key + run: | + echo "${{ secrets.LIGHTSAIL_SSH_KEY }}" > key.pem + chmod 600 key.pem + mkdir -p ~/.ssh + ssh-keyscan -H ${LIGHTSAIL_HOST} >> ~/.ssh/known_hosts + + # ====== 아카이브/스크립트 전송 ====== + - name: Upload image archive + run: | + scp -i key.pem -o StrictHostKeyChecking=yes "$ARCHIVE" \ + ${SSH_USER}@${LIGHTSAIL_HOST}:/home/${SSH_USER}/image.tar.gz + + - name: Upload blue/green deploy script + run: | + cat > deploy_blue_green.sh <<'EOS' + #!/usr/bin/env bash + set -euo pipefail + + sudo systemctl enable --now docker >/dev/null 2>&1 || true + + IMAGE_NAME=${IMAGE_NAME:-devdevdev/app} + CONTAINER_BASE=${CONTAINER_BASE:-devdevdev-main-server} + BLUE_SUFFIX=${BLUE_SUFFIX:--blue} + GREEN_SUFFIX=${GREEN_SUFFIX:--green} + BLUE_PORT=${BLUE_PORT:-18080} + GREEN_PORT=${GREEN_PORT:-18081} + APP_PORT=${APP_PORT:-8080} + HEALTHCHECK_PATH=${HEALTHCHECK_PATH:-/} # actuator 없으면 / + HEALTHCHECK_TIMEOUT=${HEALTHCHECK_TIMEOUT:-3} + HEALTHCHECK_RETRY=${HEALTHCHECK_RETRY:-20} + + UPSTREAM_FILE="/etc/nginx/conf.d/backend-upstream.upstream" + BLUE_NAME="${CONTAINER_BASE}${BLUE_SUFFIX}" + GREEN_NAME="${CONTAINER_BASE}${GREEN_SUFFIX}" + + # 아카이브 경로는 HOME 기준으로 + ARCHIVE_FILE="$HOME/image.tar.gz" + + echo "[1/9] Load image: ${ARCHIVE_FILE}" + ls -lh "${ARCHIVE_FILE}" || { echo "[!] archive missing"; exit 1; } + gzip -t "${ARCHIVE_FILE}" + gunzip -c "${ARCHIVE_FILE}" | sudo docker load + + # 보강: :latest 태그가 없으면 가장 최근 태그를 latest로 재태깅 + if ! sudo docker image inspect "${IMAGE_NAME}:latest" >/dev/null 2>&1; then + echo "[info] ${IMAGE_NAME}:latest not found. Retagging…" + # 해당 리포의 임의의 태그 하나를 찾아 latest로 붙임 + NEW_TAG=$(sudo docker images --format '{{.Repository}}:{{.Tag}}' \ + | awk -v repo="${IMAGE_NAME}" -F: '$1==repo && $2!="latest"{print $2; exit}') + if [ -n "${NEW_TAG:-}" ]; then + sudo docker tag "${IMAGE_NAME}:${NEW_TAG}" "${IMAGE_NAME}:latest" + else + echo "[!] no tag to retag as latest"; exit 1 + fi + fi + + ACTIVE_PORT="" + if [ -f "${UPSTREAM_FILE}" ]; then + ACTIVE_PORT=$(grep -oE '127\.0\.0\.1:([0-9]+)' "${UPSTREAM_FILE}" | awk -F: '{print $2}' || true) + fi + if [ -z "${ACTIVE_PORT}" ]; then + echo "server 127.0.0.1:${BLUE_PORT};" | sudo tee "${UPSTREAM_FILE}" >/dev/null + ACTIVE_PORT="${BLUE_PORT}" + fi + echo "[2/9] Current active port: ${ACTIVE_PORT}" + + if [ "${ACTIVE_PORT}" = "${BLUE_PORT}" ]; then + TARGET_NAME="${GREEN_NAME}"; TARGET_PORT="${GREEN_PORT}" + OLD_NAME="${BLUE_NAME}"; OLD_PORT="${BLUE_PORT}" + else + TARGET_NAME="${BLUE_NAME}"; TARGET_PORT="${BLUE_PORT}" + OLD_NAME="${GREEN_NAME}"; OLD_PORT="${GREEN_PORT}" + fi + echo "[3/9] Target container: ${TARGET_NAME} on ${TARGET_PORT}" + + if sudo docker ps -a --format '{{.Names}}' | grep -qw "${TARGET_NAME}"; then + sudo docker stop "${TARGET_NAME}" || true + sudo docker rm "${TARGET_NAME}" || true + fi + + echo "[4/9] Run new container" + sudo docker run -d \ + --name "${TARGET_NAME}" \ + --restart=always \ + -p 127.0.0.1:${TARGET_PORT}:${APP_PORT} \ + -e SPRING_PROFILES_ACTIVE=prod \ + ${IMAGE_NAME}:latest + + echo "[5/9] Health check http://127.0.0.1:${TARGET_PORT}${HEALTHCHECK_PATH}" + code=$(curl -sS -o /dev/null -w "%{http_code}" \ + --max-time ${HEALTHCHECK_TIMEOUT} --noproxy '*' \ + "http://127.0.0.1:${TARGET_PORT}${HEALTHCHECK_PATH}" || echo "000") + ok=0 + for i in $(seq 1 ${HEALTHCHECK_RETRY}); do + if curl -fsS --max-time ${HEALTHCHECK_TIMEOUT} "http://127.0.0.1:${TARGET_PORT}${HEALTHCHECK_PATH}" >/dev/null 2>&1; then + ok=1; break + fi + echo " retry $i/${HEALTHCHECK_RETRY}..." + sleep 5 + done + if [ "$ok" -ne 1 ]; then + echo "[!] Health check failed. Rollback." + sudo docker logs --tail 200 "${TARGET_NAME}" || true + sudo docker stop "${TARGET_NAME}" || true + sudo docker rm "${TARGET_NAME}" || true + exit 1 + fi + + echo "[6/9] Switch upstream to ${TARGET_PORT}" + echo "server 127.0.0.1:${TARGET_PORT};" | sudo tee "${UPSTREAM_FILE}" >/dev/null + sudo nginx -t + sudo systemctl reload nginx + + echo "[7/9] Stop old container: ${OLD_NAME} (if any)" + if sudo docker ps -a --format '{{.Names}}' | grep -qw "${OLD_NAME}"; then + sudo docker stop "${OLD_NAME}" || true + sudo docker rm "${OLD_NAME}" || true + fi + + echo "[8/9] Cleanup old archives (keep last 3)" + cd "$HOME" && ls -t image*.tar.gz | tail -n +4 | xargs -r rm -f + + echo "[9/9] Done." + EOS + chmod +x deploy_blue_green.sh + scp -i key.pem -o StrictHostKeyChecking=yes deploy_blue_green.sh ${SSH_USER}@${LIGHTSAIL_HOST}:/home/${SSH_USER}/ + + # ====== 원격 실행 ====== + - name: Remote Blue/Green deploy + run: | + ssh -i key.pem -o StrictHostKeyChecking=yes ${SSH_USER}@${LIGHTSAIL_HOST} \ + "env IMAGE_NAME='${IMAGE_NAME}' \ + CONTAINER_BASE='${CONTAINER_BASE}' \ + BLUE_SUFFIX='${BLUE_SUFFIX}' \ + GREEN_SUFFIX='${GREEN_SUFFIX}' \ + BLUE_PORT='${BLUE_PORT}' \ + GREEN_PORT='${GREEN_PORT}' \ + APP_PORT='${APP_PORT}' \ + HEALTHCHECK_PATH='${HEALTHCHECK_PATH}' \ + HEALTHCHECK_TIMEOUT='${HEALTHCHECK_TIMEOUT}' \ + HEALTHCHECK_RETRY='${HEALTHCHECK_RETRY}' \ + bash /home/${SSH_USER}/deploy_blue_green.sh" + + # ====== Slack 알림 ====== + - name: action-slack + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + author_name: "[PROD] 배포 결과를 알려드려요" + fields: repo,message,commit,author,eventName,ref,took + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: always() diff --git a/Dockerfile-prod b/Dockerfile-prod index 7c6e0949..d0416e95 100644 --- a/Dockerfile-prod +++ b/Dockerfile-prod @@ -1,9 +1,6 @@ -FROM openjdk:21-jdk -# JAR 파일 메인 디렉토리에 복사 +FROM eclipse-temurin:21-jre +WORKDIR /app COPY build/libs/*.jar app.jar - -# 타임존 설정 -ENV TZ Asia/Seoul - -# 시스템 진입점 정의 -CMD java -jar -Dspring.profiles.active=prod /app.jar \ No newline at end of file +ENV TZ=Asia/Seoul +EXPOSE 8080 +ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","/app/app.jar"] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article/detail.adoc b/src/docs/asciidoc/api/tech-article/detail.adoc index ce677362..e552ff5b 100644 --- a/src/docs/asciidoc/api/tech-article/detail.adoc +++ b/src/docs/asciidoc/api/tech-article/detail.adoc @@ -19,5 +19,4 @@ include::{snippets}/tech-article-detail/response-fields.adoc[] === 예외 ==== HTTP Response -include::{snippets}/not-found-elastic-id-exception/response-body.adoc[] include::{snippets}/not-found-tech-article-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article/main.adoc b/src/docs/asciidoc/api/tech-article/main.adoc index 9e9ef62a..941b4f8c 100644 --- a/src/docs/asciidoc/api/tech-article/main.adoc +++ b/src/docs/asciidoc/api/tech-article/main.adoc @@ -30,6 +30,5 @@ include::{snippets}/tech-article-main/response-fields.adoc[] ==== HTTP Response -include::{snippets}/not-found-elastic-tech-article-cursor-exception/response-body.adoc[] include::{snippets}/not-found-score-exception/response-body.adoc[] include::{snippets}/keyword-with-special-symbols-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java index 97d552cb..ef812952 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java @@ -20,9 +20,9 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; + +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -66,7 +66,6 @@ public class LocalInitData { private final PickPopularScorePolicy pickPopularScorePolicy; private final TechArticleRepository techArticleRepository; private final BookmarkRepository bookmarkRepository; - private final ElasticTechArticleRepository elasticTechArticleRepository; private final CompanyRepository companyRepository; private final MemberNicknameDictionaryRepository memberNicknameDictionaryRepository; private final BlameTypeRepository blameTypeRepository; @@ -93,10 +92,9 @@ public void dataInsert() { pickVoteRepository.saveAll(pickVotes); List companies = createCompanies(); - List savedCompanies = companyRepository.saveAll(companies); + companyRepository.saveAll(companies); - Map companyIdMap = getCompanyIdMap(savedCompanies); - List techArticles = createTechArticles(companyIdMap); + List techArticles = createTechArticles(companies); techArticleRepository.saveAll(techArticles); List bookmarks = createBookmarks(member, techArticles); @@ -205,15 +203,12 @@ private List createBookmarks(Member member, List techArti return bookmarks; } - private List createTechArticles(Map companyIdMap) { + private List createTechArticles(List companies) { List techArticles = new ArrayList<>(); - Iterable elasticTechArticles = elasticTechArticleRepository.findTop10By(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticles) { - Company company = companyIdMap.get(elasticTechArticle.getCompanyId()); - if (company != null) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } + for (int i = 0; i < companies.size(); i++) { + Company company = companies.get(i); + TechArticle techArticle = createTechArticle(i, company); + techArticles.add(techArticle); } return techArticles; } @@ -363,4 +358,20 @@ private List createBlameTypes() { private BlameType createBlameType(String reason, int sortOrder) { return new BlameType(reason, sortOrder); } + + private TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java index a4d0a31b..42dbb4d3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechArticle.java @@ -4,7 +4,6 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import jakarta.persistence.AttributeOverride; import jakarta.persistence.Column; import jakarta.persistence.Embedded; @@ -18,6 +17,8 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -29,7 +30,9 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { - @Index(name = "idx_tech_article_01", columnList = "elasticId") + @Index(name = "idx_tech_article_01", columnList = "title"), + @Index(name = "idx_tech_article_02", columnList = "contents"), + @Index(name = "idx_tech_article_03", columnList = "title, contents") }) public class TechArticle extends BasicTime { @Id @@ -38,6 +41,30 @@ public class TechArticle extends BasicTime { private Title title; + @Column(columnDefinition = "LONGTEXT") + private String contents; + + @Column(length = 255) + private String author; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "company_id", nullable = false) + private Company company; + + private LocalDate regDate; + + @Embedded + @AttributeOverride(name = "url", + column = @Column(name = "tech_article_url") + ) + private Url techArticleUrl; + + @Embedded + @AttributeOverride(name = "url", + column = @Column(name = "thumbnail_url") + ) + private Url thumbnailUrl; + @Embedded @AttributeOverride(name = "count", column = @Column(name = "view_total_count") @@ -62,19 +89,6 @@ public class TechArticle extends BasicTime { ) private Count popularScore; - @Embedded - @AttributeOverride(name = "url", - column = @Column(name = "tech_article_url") - ) - private Url techArticleUrl; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "company_id", nullable = false) - private Company company; - - @Column - private String elasticId; - @OneToMany(mappedBy = "techArticle") private List bookmarks = new ArrayList<>(); @@ -86,48 +100,20 @@ public TechArticle(Long id) { } @Builder - private TechArticle(Title title, Count viewTotalCount, Count recommendTotalCount, Count commentTotalCount, - Count popularScore, - Url techArticleUrl, Company company, String elasticId) { + private TechArticle(Title title, String contents, String author, Company company, + LocalDate regDate, Url techArticleUrl, Url thumbnailUrl, + Count viewTotalCount, Count recommendTotalCount, Count commentTotalCount, Count popularScore) { this.title = title; + this.contents = contents; + this.author = author; + this.company = company; + this.regDate = regDate; this.techArticleUrl = techArticleUrl; + this.thumbnailUrl = thumbnailUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; - this.company = company; - this.elasticId = elasticId; - } - - public static TechArticle createTechArticle(ElasticTechArticle elasticTechArticle, Company company) { - TechArticle techArticle = TechArticle.builder() - .title(new Title(elasticTechArticle.getTitle())) - .techArticleUrl(new Url(elasticTechArticle.getTechArticleUrl())) - .viewTotalCount(new Count(elasticTechArticle.getViewTotalCount())) - .recommendTotalCount(new Count(elasticTechArticle.getRecommendTotalCount())) - .commentTotalCount(new Count(elasticTechArticle.getCommentTotalCount())) - .popularScore(new Count(elasticTechArticle.getPopularScore())) - .elasticId(elasticTechArticle.getId()) - .build(); - - techArticle.changeCompany(company); - - return techArticle; - } - - public static TechArticle createTechArticle(Title title, Url techArticleUrl, Count viewTotalCount, - Count recommendTotalCount, Count commentTotalCount, Count popularScore, - String elasticId, Company company) { - return TechArticle.builder() - .title(title) - .techArticleUrl(techArticleUrl) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .elasticId(elasticId) - .company(company) - .build(); } public void changeBookmarks(List bookmarks) { @@ -162,7 +148,6 @@ public void decrementCommentCount() { this.commentTotalCount = Count.minusOne(this.commentTotalCount); } - public void incrementRecommendTotalCount() { this.recommendTotalCount = Count.plusOne(this.recommendTotalCount); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java index 28fee850..2ef88f84 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleSort.java @@ -1,17 +1,13 @@ package com.dreamypatisiel.devdevdev.domain.repository.techArticle; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.MOST_COMMENTED_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.MOST_VIEWED_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.POPULAR_SORT_FIELD_NAME; +import static com.dreamypatisiel.devdevdev.domain.entity.QTechArticle.techArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.opensearch.search.sort.FieldSortBuilder; -import org.opensearch.search.sort.SortBuilder; -import org.opensearch.search.sort.SortBuilders; -import org.opensearch.search.sort.SortOrder; @Getter @RequiredArgsConstructor @@ -19,68 +15,70 @@ public enum TechArticleSort { LATEST("최신순") { @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(LATEST_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.regDate); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getRegDate().toString(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.regDate.lt(findTechArticle.getRegDate()) + .or(techArticle.regDate.eq(findTechArticle.getRegDate()) + .and(techArticle.id.lt(findTechArticle.getId()))); } }, POPULAR("인기순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(POPULAR_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.popularScore.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getPopularScore(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.popularScore.count.lt(findTechArticle.getPopularScore().getCount()) + .or(techArticle.popularScore.count.eq(findTechArticle.getPopularScore().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, MOST_VIEWED("조회순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(MOST_VIEWED_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.viewTotalCount.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getViewTotalCount(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.viewTotalCount.count.lt(findTechArticle.getViewTotalCount().getCount()) + .or(techArticle.viewTotalCount.count.eq(findTechArticle.getViewTotalCount().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, MOST_COMMENTED("댓글순") { - @Override - public SortBuilder getSortCondition() { - return getFieldSortBuilder(MOST_COMMENTED_SORT_FIELD_NAME); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.commentTotalCount.count); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return elasticTechArticle.getCommentTotalCount(); + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.commentTotalCount.count.lt(findTechArticle.getCommentTotalCount().getCount()) + .or(techArticle.commentTotalCount.count.eq(findTechArticle.getCommentTotalCount().getCount()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }, HIGHEST_SCORE("정확도순") { - @Override - public SortBuilder getSortCondition() { - return SortBuilders.scoreSort().order(SortOrder.DESC); + public OrderSpecifier getOrderSpecifierByTechArticleSort() { + return new OrderSpecifier<>(Order.DESC, techArticle.id); } @Override - public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle) { - return null; + public BooleanExpression getCursorCondition(TechArticle findTechArticle) { + return techArticle.regDate.lt(findTechArticle.getRegDate()) + .or(techArticle.regDate.eq(findTechArticle.getRegDate()) + .and(techArticle.regDate.eq(findTechArticle.getRegDate()))); } }; - abstract public SortBuilder getSortCondition(); - - abstract public Object getSearchAfterCondition(ElasticTechArticle elasticTechArticle); + abstract public OrderSpecifier getOrderSpecifierByTechArticleSort(); - private static FieldSortBuilder getFieldSortBuilder(String sortFieldName) { - return SortBuilders.fieldSort(sortFieldName).order(SortOrder.DESC); - } + abstract public BooleanExpression getCursorCondition(TechArticle techArticle); private final String description; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java index cbaf6dec..c874cc41 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryCustom.java @@ -3,13 +3,16 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; -import java.util.List; + +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; public interface TechArticleRepositoryCustom { - List findAllByElasticIdIn(List elasticIds); - Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, BookmarkSort bookmarkSort, Member member); + + SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, + Long companyId, String keyword, Float score); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java index 7ff6313c..e3d18159 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechArticleRepositoryImpl.java @@ -7,51 +7,41 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; import com.querydsl.jpa.JPQLQueryFactory; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; + import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; @RequiredArgsConstructor public class TechArticleRepositoryImpl implements TechArticleRepositoryCustom { - private final JPQLQueryFactory query; - - @Override - public List findAllByElasticIdIn(List elasticIds) { - - List findTechArticles = query.selectFrom(techArticle) - .where(techArticle.elasticId.in(elasticIds)) - .fetch(); - - // elasticId 목록의 순서를 기반으로 결과 목록 재정렬(h2 database에서는 order by Field() 쿼리를 지원하지 않으므로 재정렬 필요) - Map techArticles = findTechArticles.stream() - .collect(Collectors.toMap(TechArticle::getElasticId, Function.identity())); + public static final String MATCH_AGAINST_FUNCTION = "match_against"; + public static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; - return elasticIds.stream() - .map(techArticles::get) - .collect(Collectors.toList()); - } + private final JPQLQueryFactory query; @Override public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Long techArticleId, - BookmarkSort bookmarkSort, - Member member) { - + BookmarkSort bookmarkSort, Member member + ) { List contents = query.selectFrom(techArticle) .innerJoin(bookmark) .on(techArticle.eq(bookmark.techArticle)) .where(bookmark.member.eq(member), bookmark.status.isTrue(), - getCursorCondition(bookmarkSort, techArticleId, member)) + getCursorConditionFromBookmarkSort(bookmarkSort, techArticleId, member)) .orderBy(bookmarkSort(bookmarkSort), techArticle.id.desc()) .limit(pageable.getPageSize()) .fetch(); @@ -59,7 +49,88 @@ public Slice findBookmarkedByMemberAndCursor(Pageable pageable, Lon return new SliceImpl<>(contents, pageable, hasNextPage(contents, pageable.getPageSize())); } - private Predicate getCursorCondition(BookmarkSort bookmarkSort, Long techArticleId, Member member) { + @Override + public SliceCustom findTechArticlesByCursor(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId, + String keyword, Float score + ) { + // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 + if (StringUtils.hasText(keyword)) { + return findTechArticlesByCursorWithKeyword(pageable, techArticleId, techArticleSort, companyId, keyword, score); + } + return findTechArticlesByCursorWithoutKeyword(pageable, techArticleId, techArticleSort, companyId); + } + + // 키워드 검색 + private SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId, + String keyword, Float score + ) { + // FULLTEXT 검색 조건 생성 + BooleanExpression titleMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techArticle.title.title, keyword + ); + + BooleanExpression contentsMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techArticle.contents, keyword + ); + + // 스코어 계산을 위한 expression (Natural Language Mode) + NumberTemplate titleScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", + techArticle.title.title, keyword + ); + NumberTemplate contentsScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", + techArticle.contents, keyword + ); + + // 전체 스코어 계산 (제목 가중치 2배) + NumberTemplate totalScore = Expressions.numberTemplate(Double.class, + "({0} * 2.0) + {1}", titleScore, contentsScore + ); + + List contents = query.selectFrom(techArticle) + .where(titleMatch.or(contentsMatch)) + .where(getCompanyIdCondition(companyId)) + .where(getCursorConditionForKeywordSearch(techArticleSort, techArticleId, score, totalScore)) + .orderBy(getOrderSpecifierForKeywordSearch(techArticleSort, totalScore), techArticle.id.desc()) + .limit(pageable.getPageSize()) + .fetch(); + + // 키워드 검색 결과 총 갯수 + long totalElements = query.select(techArticle.count()) + .from(techArticle) + .where(titleMatch.or(contentsMatch)) + .where(getCompanyIdCondition(companyId)) + .fetchCount(); + + return new SliceCustom<>(contents, pageable, totalElements); + } + + // 일반 조회 + private SliceCustom findTechArticlesByCursorWithoutKeyword(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId + ) { + List contents = query.selectFrom(techArticle) + .where(getCursorConditionFromTechArticleSort(techArticleSort, techArticleId)) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .orderBy(techArticleSort(techArticleSort), techArticle.id.desc()) + .limit(pageable.getPageSize()) + .fetch(); + + // 기술블로그 총 갯수 + long totalElements = query.select(techArticle.count()) + .from(techArticle) + .where(companyId != null ? techArticle.company.id.eq(companyId) : null) + .fetchCount(); + + return new SliceCustom<>(contents, pageable, totalElements); + } + + private Predicate getCursorConditionFromBookmarkSort(BookmarkSort bookmarkSort, Long techArticleId, Member member) { if (ObjectUtils.isEmpty(techArticleId)) { return null; } @@ -90,6 +161,78 @@ private OrderSpecifier bookmarkSort(BookmarkSort bookmarkSort) { } + private Predicate getCursorConditionFromTechArticleSort(TechArticleSort techArticleSort, Long techArticleId) { + if (ObjectUtils.isEmpty(techArticleId)) { + return null; + } + + // techArticleId로 기술블로그 조회 + TechArticle findTechArticle = query.selectFrom(techArticle) + .where(techArticle.id.eq(techArticleId)) + .fetchOne(); + + // 일치하는 기술블로그가 없으면 + if (ObjectUtils.isEmpty(findTechArticle)) { + return techArticle.id.loe(techArticleId); + } + + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.LATEST).getCursorCondition(findTechArticle); + } + + private OrderSpecifier techArticleSort(TechArticleSort techArticleSort) { + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.LATEST).getOrderSpecifierByTechArticleSort(); + + } + + // 키워드 검색을 위한 커서 조건 생성 + private Predicate getCursorConditionForKeywordSearch(TechArticleSort techArticleSort, Long techArticleId, + Float score, NumberTemplate totalScore) { + if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { + return null; + } + + // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 커서 사용 + if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + return totalScore.lt(score.doubleValue()) + .or(totalScore.eq(score.doubleValue()) + .and(techArticle.id.lt(techArticleId))); + } + + // 다른 정렬 방식인 경우 기존 커서 조건 사용 + TechArticle findTechArticle = query.selectFrom(techArticle) + .where(techArticle.id.eq(techArticleId)) + .fetchOne(); + + if (ObjectUtils.isEmpty(findTechArticle)) { + return techArticle.id.loe(techArticleId); + } + + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.HIGHEST_SCORE).getCursorCondition(findTechArticle); + } + + // 키워드 검색을 위한 정렬 조건 생성 + private OrderSpecifier getOrderSpecifierForKeywordSearch(TechArticleSort techArticleSort, + NumberTemplate totalScore) { + // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 정렬 + if (techArticleSort == TechArticleSort.HIGHEST_SCORE) { + return totalScore.desc(); + } + + // 다른 정렬 방식인 경우 기존 정렬 사용 + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.HIGHEST_SCORE).getOrderSpecifierByTechArticleSort(); + } + + public BooleanExpression getCompanyIdCondition(Long companyId) { + if(companyId == null) { + return null; + } + return techArticle.company.id.eq(companyId); + } + private boolean hasNextPage(List contents, int pageSize) { return contents.size() >= pageSize; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java index 46d782a5..044fe3a3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java @@ -16,6 +16,8 @@ public class TechKeywordRepositoryImpl implements TechKeywordRepositoryCustom { public static final String MATCH_AGAINST_FUNCTION = "match_against"; + public static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; + private final JPQLQueryFactory query; @Override @@ -30,13 +32,13 @@ public List searchKeyword(String inputJamo, String inputChosung, Pa techKeyword.chosungKey, inputChosung ); - // 스코어 계산을 위한 expression + // 스코어 계산을 위한 expression (Natural Language Mode) NumberTemplate jamoScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techKeyword.jamoKey, inputJamo ); NumberTemplate chosungScore = Expressions.numberTemplate(Double.class, - "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", techKeyword.chosungKey, inputChosung ); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java index d0962c3d..4e5acda6 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberService.java @@ -22,8 +22,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.survey.custom.SurveyAnswerJdbcTemplateRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import com.dreamypatisiel.devdevdev.exception.NicknameException; import com.dreamypatisiel.devdevdev.exception.SurveyException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; @@ -38,13 +36,11 @@ import com.dreamypatisiel.devdevdev.web.dto.response.member.MemberExitSurveyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.MyPickMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.subscription.SubscribedCompanyResponse; -import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.CompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -53,17 +49,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; -import org.springframework.beans.factory.annotation.Value; - @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -74,7 +61,6 @@ public class MemberService { private final SurveyVersionQuestionMapperRepository surveyVersionQuestionMapperRepository; private final SurveyAnswerRepository surveyAnswerRepository; private final TechArticleRepository techArticleRepository; - private final TechArticleCommonService techArticleCommonService; private final TimeProvider timeProvider; private final SurveyQuestionOptionRepository surveyQuestionOptionRepository; private final SurveyAnswerJdbcTemplateRepository surveyAnswerJdbcTemplateRepository; @@ -227,33 +213,16 @@ public Slice getBookmarkedTechArticles(Pageable pageabl // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); - // 북마크 기술블로그 조회(rds, elasticsearch) - Slice techArticleSlices = techArticleRepository.findBookmarkedByMemberAndCursor(pageable, - techArticleId, bookmarkSort, findMember); - - List techArticles = techArticleSlices.getContent(); - - List elasticTechArticles = techArticleCommonService.findElasticTechArticlesByTechArticles( - techArticles); + // 북마크 기술블로그 조회 + Slice techArticles = techArticleRepository.findBookmarkedByMemberAndCursor( + pageable, techArticleId, bookmarkSort, findMember); // 데이터 가공 - List techArticleMainResponse = techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles, findMember)) + List techArticleMainResponse = techArticles.getContent().stream() + .map(techArticle -> TechArticleMainResponse.of(techArticle, findMember)) .toList(); - return new SliceImpl<>(techArticleMainResponse, pageable, techArticleSlices.hasNext()); - } - - /** - * 기술블로그 목록 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List elasticTechArticles, - Member member) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle, - CompanyResponse.from(techArticle.getCompany()), member)); + return new SliceImpl<>(techArticleMainResponse, techArticles.getPageable(), techArticles.hasNext()); } /** diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java index 20345407..2e82d2fe 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationService.java @@ -11,7 +11,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.notification.NotificationRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; @@ -26,7 +25,6 @@ import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationPopupResponse; import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationReadResponse; import com.dreamypatisiel.devdevdev.web.dto.response.notification.NotificationResponse; -import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.CompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import java.util.LinkedHashSet; import java.util.List; @@ -167,60 +165,25 @@ public SliceCustom getNotifications(Pageable pageable, Lon // 데이터 가공 // NotificationType 에 따라 다른 DTO로 변환 - Map elasticTechArticles = getTechArticleIdToElastic(notifications.getContent()); - List response = notifications.getContent().stream() - .map(notification -> mapToNotificationResponse(notification, elasticTechArticles)) + .map(this::mapToNotificationResponse) .toList(); return new SliceCustom<>(response, pageable, notifications.hasNext(), notifications.getTotalElements()); } - private NotificationResponse mapToNotificationResponse(Notification notification, - Map elasticTechArticles) { + private NotificationResponse mapToNotificationResponse(Notification notification) { // TODO: 현재는 SUBSCRIPTION 타입만 제공, 알림 타입이 추가될 경우 각 타입에 맞는 응답 DTO 변환 매핑 필요 if (notification.getType() == NotificationType.SUBSCRIPTION) { - return NotificationNewArticleResponse.from(notification, - getTechArticleMainResponse(notification, elasticTechArticles)); + return NotificationNewArticleResponse.from(notification, getTechArticleMainResponse(notification)); } throw new NotFoundException(NotificationExceptionMessage.NOT_FOUND_NOTIFICATION_TYPE); } // NotificationType.SUBSCRIPTION 알림의 경우 TechArticleMainResponse 생성 - private TechArticleMainResponse getTechArticleMainResponse(Notification notification, - Map elasticTechArticles) { + private TechArticleMainResponse getTechArticleMainResponse(Notification notification) { TechArticle techArticle = notification.getTechArticle(); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); - ElasticTechArticle elasticTechArticle = elasticTechArticles.get(notification.getId()); - - return TechArticleMainResponse.of(techArticle, elasticTechArticle, companyResponse); - } - - // 알림 ID를 키로 하고, ElasticTechArticle 을 값으로 가지는 맵을 반환 - private Map getTechArticleIdToElastic(List notifications) { - // 1. NotificationType.SUBSCRIPTION 알림만 필터링하여 ElasticTechArticle 리스트 생성 - List techArticles = notifications.stream() - .filter(notification -> notification.getType() == NotificationType.SUBSCRIPTION) - .map(Notification::getTechArticle) - .toList(); - - List elasticTechArticles = techArticleCommonService.findElasticTechArticlesByTechArticles( - techArticles); - - // 2. ElasticID → ElasticTechArticle 매핑 - Map elasticIdToElastic = elasticTechArticles.stream() - .collect(Collectors.toMap( - ElasticTechArticle::getId, - elasticTechArticle -> elasticTechArticle - )); - - // 3. Notification ID → ElasticTechArticle 매핑 - return notifications.stream() - .filter(notification -> notification.getType() == NotificationType.SUBSCRIPTION) - .collect(Collectors.toMap( - Notification::getId, - notification -> elasticIdToElastic.get(notification.getTechArticle().getElasticId()) - )); + return TechArticleMainResponse.of(techArticle); } /** diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java index b593c468..1d312e5c 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/GuestTechArticleService.java @@ -8,29 +8,20 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticTechArticleService; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.stream.Stream; - -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.hasNextPage; @Slf4j @Service @@ -39,43 +30,40 @@ public class GuestTechArticleService extends TechArticleCommonService implements public static final String INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE = "비회원은 현재 해당 기능을 이용할 수 없습니다."; - private final ElasticTechArticleService elasticTechArticleService; private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; private final AnonymousMemberService anonymousMemberService; private final TechArticleRecommendRepository techArticleRecommendRepository; + private final TechArticleRepository techArticleRepository; - public GuestTechArticleService(TechArticleRepository techArticleRepository, - ElasticTechArticleRepository elasticTechArticleRepository, - ElasticTechArticleService elasticTechArticleService, - TechArticlePopularScorePolicy techArticlePopularScorePolicy, + public GuestTechArticleService(TechArticlePopularScorePolicy techArticlePopularScorePolicy, AnonymousMemberService anonymousMemberService, + TechArticleRepository techArticleRepository, TechArticleRecommendRepository techArticleRecommendRepository - ) { - super(techArticleRepository, elasticTechArticleRepository); - this.elasticTechArticleService = elasticTechArticleService; + ) { + super(techArticleRepository); this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; this.anonymousMemberService = anonymousMemberService; this.techArticleRecommendRepository = techArticleRecommendRepository; + this.techArticleRepository = techArticleRepository; } @Override - public Slice getTechArticles(Pageable pageable, String elasticId, + public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication) { // 익명 사용자 호출인지 확인 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - // 엘라스틱서치 기술블로그 조회 - SearchHits elasticTechArticleSearchHits = elasticTechArticleService.getTechArticles( - pageable, elasticId, techArticleSort, keyword, companyId, score); - List> elasticTechArticles = elasticTechArticleService.mapToElasticResponse( - elasticTechArticleSearchHits); + // 기술블로그 조회 + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 - List techArticlesResponse = getTechArticlesResponse(elasticTechArticles); + List techArticlesResponse = techArticles.stream() + .map(TechArticleMainResponse::of) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -86,17 +74,16 @@ public TechArticleDetailResponse getTechArticle(Long techArticleId, String anony // 익명 회원을 조회하거나 생성 AnonymousMember anonymousMember = getAnonymousMemberOrNull(anonymousMemberId); + // 기술블로그 조회 TechArticle techArticle = findTechArticle(techArticleId); - ElasticTechArticle elasticTechArticle = findElasticTechArticle(techArticle); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); // 조회수 증가 techArticle.incrementViewTotalCount(); techArticle.changePopularScore(techArticlePopularScorePolicy); // 데이터 가공 - return TechArticleDetailResponse.of(techArticle, elasticTechArticle, companyResponse, anonymousMember); + return TechArticleDetailResponse.of(techArticle, anonymousMember); } @Override @@ -153,34 +140,6 @@ public TechArticleRecommendResponse updateRecommend(Long techArticleId, String a return new TechArticleRecommendResponse(techArticle.getId(), techArticleRecommend.isRecommended(), techArticle.getRecommendTotalCount().getCount()); } - /** - * 엘라스틱서치 검색 결과로 기술블로그 목록 응답을 생성합니다. - */ - private List getTechArticlesResponse( - List> elasticTechArticles) { - // 조회 결과가 없을 경우 빈 리스트 응답 - if (elasticTechArticles.isEmpty()) { - return Collections.emptyList(); - } - - List techArticles = findTechArticlesByElasticTechArticles(elasticTechArticles); - - return techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles)) - .toList(); - } - - /** - * 기술블로그 목록을 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List> elasticTechArticles) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.content().getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle.content(), - CompanyResponse.from(techArticle.getCompany()), elasticTechArticle.score())); - } - /** * anonymousMemberId가 있으면 익명 회원을 조회 또는 생성하고, 없으면 null 반환 */ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java index 34ec9e46..ea0b854e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/MemberTechArticleService.java @@ -1,7 +1,5 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle; -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.hasNextPage; - import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; @@ -11,69 +9,61 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticTechArticleService; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; - -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + @Slf4j @Service @Transactional(readOnly = true) public class MemberTechArticleService extends TechArticleCommonService implements TechArticleService { - private final ElasticTechArticleService elasticTechArticleService; private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; + private final MemberProvider memberProvider; private final BookmarkRepository bookmarkRepository; private final TechArticleRecommendRepository techArticleRecommendRepository; - private final MemberProvider memberProvider; - - public MemberTechArticleService(TechArticleRepository techArticleRepository, - ElasticTechArticleRepository elasticTechArticleRepository, - ElasticTechArticleService elasticTechArticleService, - TechArticlePopularScorePolicy techArticlePopularScorePolicy, - BookmarkRepository bookmarkRepository, TechArticleRecommendRepository techArticleRecommendRepository, - MemberProvider memberProvider) { - super(techArticleRepository, elasticTechArticleRepository); - this.elasticTechArticleService = elasticTechArticleService; + private final TechArticleRepository techArticleRepository; + + public MemberTechArticleService(TechArticlePopularScorePolicy techArticlePopularScorePolicy, + MemberProvider memberProvider, + BookmarkRepository bookmarkRepository, + TechArticleRecommendRepository techArticleRecommendRepository, + TechArticleRepository techArticleRepository + ) { + super(techArticleRepository); this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; this.bookmarkRepository = bookmarkRepository; this.techArticleRecommendRepository = techArticleRecommendRepository; this.memberProvider = memberProvider; + this.techArticleRepository = techArticleRepository; } @Override - public Slice getTechArticles(Pageable pageable, String elasticId, + public Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication) { // 회원 조회 Member member = memberProvider.getMemberByAuthentication(authentication); - // 엘라스틱서치 기술블로그 조회 - SearchHits elasticTechArticleSearchHits = elasticTechArticleService.getTechArticles( - pageable, elasticId, techArticleSort, keyword, companyId, score); - List> elasticTechArticles = elasticTechArticleService.mapToElasticResponse( - elasticTechArticleSearchHits); + // 기술블로그 조회 + SliceCustom techArticles = techArticleRepository.findTechArticlesByCursor( + pageable, techArticleId, techArticleSort, companyId, keyword, score); // 데이터 가공 - List techArticlesResponse = getTechArticlesResponse(elasticTechArticles, member); + List techArticlesResponse = techArticles.stream() + .map(techArticle -> TechArticleMainResponse.of(techArticle, member)) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -84,15 +74,13 @@ public TechArticleDetailResponse getTechArticle(Long techArticleId, String anony // 기술블로그 조회 TechArticle techArticle = findTechArticle(techArticleId); - ElasticTechArticle elasticTechArticle = findElasticTechArticle(techArticle); - CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); // 조회수 증가 techArticle.incrementViewTotalCount(); techArticle.changePopularScore(techArticlePopularScorePolicy); // 데이터 가공 - return TechArticleDetailResponse.of(techArticle, elasticTechArticle, companyResponse, member); + return TechArticleDetailResponse.of(techArticle, member); } @Override @@ -174,33 +162,4 @@ public TechArticleRecommendResponse updateRecommend(Long techArticleId, String a return new TechArticleRecommendResponse(techArticle.getId(), techArticleRecommend.isRecommended(), techArticle.getRecommendTotalCount().getCount()); } - - /** - * 엘라스틱서치 검색 결과로 기술블로그 목록 응답을 생성합니다. - */ - private List getTechArticlesResponse( - List> elasticTechArticles, Member member) { - // 조회 결과가 없을 경우 빈 리스트 응답 - if (elasticTechArticles.isEmpty()) { - return Collections.emptyList(); - } - - List techArticles = findTechArticlesByElasticTechArticles(elasticTechArticles); - - return techArticles.stream() - .flatMap(techArticle -> mapToTechArticlesResponse(techArticle, elasticTechArticles, member)) - .toList(); - } - - /** - * 기술블로그 목록을 응답 형태로 가공합니다. - */ - private Stream mapToTechArticlesResponse(TechArticle techArticle, - List> elasticTechArticles, - Member member) { - return elasticTechArticles.stream() - .filter(elasticTechArticle -> techArticle.getElasticId().equals(elasticTechArticle.content().getId())) - .map(elasticTechArticle -> TechArticleMainResponse.of(techArticle, elasticTechArticle.content(), - CompanyResponse.from(techArticle.getCompany()), elasticTechArticle.score(), member)); - } } \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java index 050b89af..68749ce8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleCommonService.java @@ -1,25 +1,15 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; -import java.util.Collections; -import java.util.List; -import java.util.stream.StreamSupport; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; @Component @RequiredArgsConstructor @@ -27,61 +17,12 @@ public class TechArticleCommonService { private final TechArticleRepository techArticleRepository; - private final ElasticTechArticleRepository elasticTechArticleRepository; - - protected ElasticTechArticle findElasticTechArticle(TechArticle techArticle) { - String elasticId = techArticle.getElasticId(); - - if (!StringUtils.hasText(elasticId)) { - throw new TechArticleException(NOT_FOUND_ELASTIC_ID_MESSAGE); - } - - return elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - } public TechArticle findTechArticle(Long techArticleId) { return techArticleRepository.findById(techArticleId) .orElseThrow(() -> new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); } - protected List findTechArticlesByElasticTechArticles( - List> elasticTechArticlesResponse) { - List elasticIds = getElasticIdsFromElasticTechArticles(elasticTechArticlesResponse); - // 추출한 elasticId가 없다면 빈 리스트 응답 - if (elasticIds.isEmpty()) { - return Collections.emptyList(); - } - - return techArticleRepository.findAllByElasticIdIn(elasticIds); - } - - public List findElasticTechArticlesByTechArticles(List techArticles) { - List elasticIds = getElasticIdsFromTechArticles(techArticles); - // 추출한 elasticId가 없다면 빈 리스트 응답 - if (elasticIds.isEmpty()) { - return Collections.emptyList(); - } - - Iterable elasticTechArticles = elasticTechArticleRepository.findAllById(elasticIds); - - return StreamSupport.stream(elasticTechArticles.spliterator(), false) - .toList(); - } - - private List getElasticIdsFromTechArticles(List techArticles) { - return techArticles.stream() - .map(TechArticle::getElasticId) - .toList(); - } - - private List getElasticIdsFromElasticTechArticles( - List> elasticTechArticlesResponse) { - return elasticTechArticlesResponse.stream() - .map(elasticResponse -> elasticResponse.content().getId()) - .toList(); - } - public static void validateIsDeletedTechComment(TechComment techComment, String message, @Nullable String messageArgs) { if (techComment.isDeleted()) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java index 48a9f41d..475b91d1 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techArticle/TechArticleService.java @@ -10,7 +10,7 @@ import org.springframework.security.core.Authentication; public interface TechArticleService { - Slice getTechArticles(Pageable pageable, String elasticId, TechArticleSort techArticleSort, + Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, String keyword, Long companyId, Float score, Authentication authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java index f9146d7a..ff808863 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/ElasticsearchIndexConfigService.java @@ -1,12 +1,12 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Getter -@Service -public class ElasticsearchIndexConfigService { - @Value("${opensearch.index:}") - private String indexName; -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import lombok.Getter; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +// +//@Getter +//@Service +//public class ElasticsearchIndexConfigService { +// @Value("${opensearch.index:}") +// private String indexName; +//} \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java index 5fd90484..072ee308 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/config/OpenSearchRestClientConfiguration.java @@ -1,66 +1,66 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import java.time.Duration; -import org.apache.http.HttpHost; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.CredentialsProvider; -import org.apache.http.impl.client.BasicCredentialsProvider; -import org.opensearch.client.RestClient; -import org.opensearch.client.RestClientBuilder; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.data.client.orhlc.AbstractOpenSearchConfiguration; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; - -@Configuration -@EnableElasticsearchRepositories(basePackages = "com.dreamypatisiel.devdevdev.elastic.domain.repository") -public class OpenSearchRestClientConfiguration extends AbstractOpenSearchConfiguration { - - public static final String ENDPOINT_SCHEME = "https"; - public static final int ENDPOINT_PORT = 443; - public static final int CONNECTION_TIMEOUT_IN_SECONDS = 30; - public static final int SOCKET_TIMEOUT_IN_SECONDS = 60; - - @Value("${opensearch.endpoint}") - private String endpoint; - - @Value("${opensearch.credentials.username}") - private String username; - - @Value("${opensearch.credentials.password}") - private String password; - - @Autowired - private ApplicationContext applicationContext; - - @Bean - public CredentialsProvider credentialsProvider() { - CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials( - AuthScope.ANY, new UsernamePasswordCredentials(username, password)); - return credentialsProvider; - } - - @Bean - @Override - public RestHighLevelClient opensearchClient() { - CredentialsProvider credentialsProvider = applicationContext.getBean( - CredentialsProvider.class); - - RestClientBuilder builder = RestClient.builder(new HttpHost(endpoint, ENDPOINT_PORT, ENDPOINT_SCHEME)) - .setHttpClientConfigCallback( - httpAsyncClientBuilder -> httpAsyncClientBuilder.setDefaultCredentialsProvider( - credentialsProvider)) - .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder - .setConnectTimeout((int) Duration.ofSeconds(CONNECTION_TIMEOUT_IN_SECONDS).toMillis()) - .setSocketTimeout((int) Duration.ofSeconds(SOCKET_TIMEOUT_IN_SECONDS).toMillis())); - - RestHighLevelClient client = new RestHighLevelClient(builder); - return client; - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import java.time.Duration; +//import org.apache.http.HttpHost; +//import org.apache.http.auth.AuthScope; +//import org.apache.http.auth.UsernamePasswordCredentials; +//import org.apache.http.client.CredentialsProvider; +//import org.apache.http.impl.client.BasicCredentialsProvider; +//import org.opensearch.client.RestClient; +//import org.opensearch.client.RestClientBuilder; +//import org.opensearch.client.RestHighLevelClient; +//import org.opensearch.data.client.orhlc.AbstractOpenSearchConfiguration; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.context.ApplicationContext; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; +// +//@Configuration +//@EnableElasticsearchRepositories(basePackages = "com.dreamypatisiel.devdevdev.elastic.domain.repository") +//public class OpenSearchRestClientConfiguration extends AbstractOpenSearchConfiguration { +// +// public static final String ENDPOINT_SCHEME = "https"; +// public static final int ENDPOINT_PORT = 443; +// public static final int CONNECTION_TIMEOUT_IN_SECONDS = 30; +// public static final int SOCKET_TIMEOUT_IN_SECONDS = 60; +// +// @Value("${opensearch.endpoint}") +// private String endpoint; +// +// @Value("${opensearch.credentials.username}") +// private String username; +// +// @Value("${opensearch.credentials.password}") +// private String password; +// +// @Autowired +// private ApplicationContext applicationContext; +// +// @Bean +// public CredentialsProvider credentialsProvider() { +// CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); +// credentialsProvider.setCredentials( +// AuthScope.ANY, new UsernamePasswordCredentials(username, password)); +// return credentialsProvider; +// } +// +// @Bean +// @Override +// public RestHighLevelClient opensearchClient() { +// CredentialsProvider credentialsProvider = applicationContext.getBean( +// CredentialsProvider.class); +// +// RestClientBuilder builder = RestClient.builder(new HttpHost(endpoint, ENDPOINT_PORT, ENDPOINT_SCHEME)) +// .setHttpClientConfigCallback( +// httpAsyncClientBuilder -> httpAsyncClientBuilder.setDefaultCredentialsProvider( +// credentialsProvider)) +// .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder +// .setConnectTimeout((int) Duration.ofSeconds(CONNECTION_TIMEOUT_IN_SECONDS).toMillis()) +// .setSocketTimeout((int) Duration.ofSeconds(SOCKET_TIMEOUT_IN_SECONDS).toMillis())); +// +// RestHighLevelClient client = new RestHighLevelClient(builder); +// return client; +// } +//} \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java index 5d5aa336..d1c87644 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/constant/ElasticsearchConstant.java @@ -1,11 +1,11 @@ -package com.dreamypatisiel.devdevdev.elastic.constant; - -public class ElasticsearchConstant { - public static final String _ID = "_id"; - public static final String _COMPANY_ID = "companyId"; - - public final static String LATEST_SORT_FIELD_NAME = "regDate"; - public final static String POPULAR_SORT_FIELD_NAME = "popularScore"; - public final static String MOST_VIEWED_SORT_FIELD_NAME = "viewTotalCount"; - public final static String MOST_COMMENTED_SORT_FIELD_NAME = "commentTotalCount"; -} +//package com.dreamypatisiel.devdevdev.elastic.constant; +// +//public class ElasticsearchConstant { +// public static final String _ID = "_id"; +// public static final String _COMPANY_ID = "companyId"; +// +// public final static String LATEST_SORT_FIELD_NAME = "regDate"; +// public final static String POPULAR_SORT_FIELD_NAME = "popularScore"; +// public final static String MOST_VIEWED_SORT_FIELD_NAME = "viewTotalCount"; +// public final static String MOST_COMMENTED_SORT_FIELD_NAME = "commentTotalCount"; +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java index 71055d63..44480bad 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/data/response/ElasticResponse.java @@ -1,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.elastic.data.response; - -public record ElasticResponse(T content, Float score) { -} +//package com.dreamypatisiel.devdevdev.elastic.data.response; +// +//public record ElasticResponse(T content, Float score) { +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java index 0322acad..7c04e04d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticKeyword.java @@ -1,30 +1,30 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.document; - -import lombok.Builder; -import lombok.Getter; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Getter -@Document(indexName = "keywords" + "#{@elasticsearchIndexConfigService.getIndexName()}") -public class ElasticKeyword { - @Id - private String id; - - @Field(type = FieldType.Text) - private String keyword; - - @Builder - private ElasticKeyword(String id, String keyword) { - this.id = id; - this.keyword = keyword; - } - - public static ElasticKeyword create(String keyword) { - return ElasticKeyword.builder() - .keyword(keyword) - .build(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.document; +// +//import lombok.Builder; +//import lombok.Getter; +//import org.springframework.data.annotation.Id; +//import org.springframework.data.elasticsearch.annotations.Document; +//import org.springframework.data.elasticsearch.annotations.Field; +//import org.springframework.data.elasticsearch.annotations.FieldType; +// +//@Getter +//@Document(indexName = "keywords" + "#{@elasticsearchIndexConfigService.getIndexName()}") +//public class ElasticKeyword { +// @Id +// private String id; +// +// @Field(type = FieldType.Text) +// private String keyword; +// +// @Builder +// private ElasticKeyword(String id, String keyword) { +// this.id = id; +// this.keyword = keyword; +// } +// +// public static ElasticKeyword create(String keyword) { +// return ElasticKeyword.builder() +// .keyword(keyword) +// .build(); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java index b7bd33e1..9ce5b9b1 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/document/ElasticTechArticle.java @@ -1,76 +1,76 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.document; - -import java.time.LocalDate; -import lombok.Builder; -import lombok.Getter; -import org.springframework.data.annotation.Id; -import org.springframework.data.elasticsearch.annotations.Document; -import org.springframework.data.elasticsearch.annotations.Field; -import org.springframework.data.elasticsearch.annotations.FieldType; - -@Getter -@Document(indexName = "articles" + "#{@elasticsearchIndexConfigService.getIndexName()}") -public class ElasticTechArticle { - @Id - private String id; - - @Field(type = FieldType.Text) - private String title; - - @Field(type = FieldType.Date) - private LocalDate regDate; - - @Field(type = FieldType.Text) - private String contents; - - @Field(type = FieldType.Text) - private String techArticleUrl; - - @Field(type = FieldType.Text) - private String description; - - @Field(type = FieldType.Text) - private String thumbnailUrl; - - @Field(type = FieldType.Text) - private String author; - - @Field(type = FieldType.Text) - private String company; - - @Field(type = FieldType.Long) - private Long companyId; - - @Field(type = FieldType.Long) - private Long viewTotalCount; - - @Field(type = FieldType.Long) - private Long recommendTotalCount; - - @Field(type = FieldType.Long) - private Long commentTotalCount; - - @Field(type = FieldType.Long) - private Long popularScore; - - @Builder - public ElasticTechArticle(String id, String title, LocalDate regDate, String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, Long companyId, String company, - Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, - Long popularScore) { - this.id = id; - this.title = title; - this.regDate = regDate; - this.contents = contents; - this.techArticleUrl = techArticleUrl; - this.description = description; - this.thumbnailUrl = thumbnailUrl; - this.author = author; - this.companyId = companyId; - this.company = company; - this.viewTotalCount = viewTotalCount; - this.recommendTotalCount = recommendTotalCount; - this.commentTotalCount = commentTotalCount; - this.popularScore = popularScore; - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.document; +// +//import java.time.LocalDate; +//import lombok.Builder; +//import lombok.Getter; +//import org.springframework.data.annotation.Id; +//import org.springframework.data.elasticsearch.annotations.Document; +//import org.springframework.data.elasticsearch.annotations.Field; +//import org.springframework.data.elasticsearch.annotations.FieldType; +// +//@Getter +//@Document(indexName = "articles" + "#{@elasticsearchIndexConfigService.getIndexName()}") +//public class ElasticTechArticle { +// @Id +// private String id; +// +// @Field(type = FieldType.Text) +// private String title; +// +// @Field(type = FieldType.Date) +// private LocalDate regDate; +// +// @Field(type = FieldType.Text) +// private String contents; +// +// @Field(type = FieldType.Text) +// private String techArticleUrl; +// +// @Field(type = FieldType.Text) +// private String description; +// +// @Field(type = FieldType.Text) +// private String thumbnailUrl; +// +// @Field(type = FieldType.Text) +// private String author; +// +// @Field(type = FieldType.Text) +// private String company; +// +// @Field(type = FieldType.Long) +// private Long companyId; +// +// @Field(type = FieldType.Long) +// private Long viewTotalCount; +// +// @Field(type = FieldType.Long) +// private Long recommendTotalCount; +// +// @Field(type = FieldType.Long) +// private Long commentTotalCount; +// +// @Field(type = FieldType.Long) +// private Long popularScore; +// +// @Builder +// public ElasticTechArticle(String id, String title, LocalDate regDate, String contents, String techArticleUrl, +// String description, String thumbnailUrl, String author, Long companyId, String company, +// Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, +// Long popularScore) { +// this.id = id; +// this.title = title; +// this.regDate = regDate; +// this.contents = contents; +// this.techArticleUrl = techArticleUrl; +// this.description = description; +// this.thumbnailUrl = thumbnailUrl; +// this.author = author; +// this.companyId = companyId; +// this.company = company; +// this.viewTotalCount = viewTotalCount; +// this.recommendTotalCount = recommendTotalCount; +// this.commentTotalCount = commentTotalCount; +// this.popularScore = popularScore; +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java index 7c1d59a1..0370bf45 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticKeywordRepository.java @@ -1,9 +1,9 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.repository; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.repository.CrudRepository; - -public interface ElasticKeywordRepository extends ElasticsearchRepository, - CrudRepository { -} +//package com.dreamypatisiel.devdevdev.elastic.domain.repository; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; +//import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +//import org.springframework.data.repository.CrudRepository; +// +//public interface ElasticKeywordRepository extends ElasticsearchRepository, +// CrudRepository { +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java index ec644ed5..21f79535 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/repository/ElasticTechArticleRepository.java @@ -1,11 +1,11 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.repository; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import java.util.List; -import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; -import org.springframework.data.repository.CrudRepository; - -public interface ElasticTechArticleRepository extends ElasticsearchRepository, - CrudRepository { - List findTop10By(); -} +//package com.dreamypatisiel.devdevdev.elastic.domain.repository; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import java.util.List; +//import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +//import org.springframework.data.repository.CrudRepository; +// +//public interface ElasticTechArticleRepository extends ElasticsearchRepository, +// CrudRepository { +// List findTop10By(); +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java index b270263e..6ffeae0d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordService.java @@ -1,56 +1,56 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchResponse; -import org.opensearch.client.RequestOptions; -import org.opensearch.client.RestHighLevelClient; -import org.opensearch.common.unit.Fuzziness; -import org.opensearch.index.query.MultiMatchQueryBuilder; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.builder.SearchSourceBuilder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ElasticKeywordService { - - @Value("#{@elasticsearchIndexConfigService.getIndexName()}") - private String INDEX_NAME_POSTFIX; - public static final String INDEX_NAME = "keywords"; - public static final String FIELD_NAME = "keyword"; - public static final String[] MULTI_FIELD_NAMES = {"keyword", "keyword.nfc", "keyword.chosung"}; - public static final int AUTOCOMPLETION_MAX_SIZE = 20; - - - private final RestHighLevelClient elasticsearchClient; - - public List autocompleteKeyword(String prefix) throws IOException { - - MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery( - prefix, // 검색할 쿼리 스트링 - MULTI_FIELD_NAMES); // multi-match 쿼리를 실행할 필드 목록 정의 - multiMatchQueryBuilder.fuzziness(Fuzziness.ZERO); // Fuzziness를 0으로 설정하여 정확히 일치하는 키워드만 검색 - - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() - .query(multiMatchQueryBuilder) - .size(AUTOCOMPLETION_MAX_SIZE); // 최대 20개 조회 - - // 조회 쿼리 생성 - SearchRequest searchRequest = new SearchRequest(INDEX_NAME + INDEX_NAME_POSTFIX) - .source(searchSourceBuilder); - - // 응답 데이터 가공 - SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT); - return Arrays.stream(searchResponse.getHits().getHits()) - .map(hit -> hit.getSourceAsMap().get(FIELD_NAME).toString()) - .toList(); - } -} - +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import java.io.IOException; +//import java.util.Arrays; +//import java.util.List; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.opensearch.action.search.SearchRequest; +//import org.opensearch.action.search.SearchResponse; +//import org.opensearch.client.RequestOptions; +//import org.opensearch.client.RestHighLevelClient; +//import org.opensearch.common.unit.Fuzziness; +//import org.opensearch.index.query.MultiMatchQueryBuilder; +//import org.opensearch.index.query.QueryBuilders; +//import org.opensearch.search.builder.SearchSourceBuilder; +//import org.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Service; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class ElasticKeywordService { +// +// @Value("#{@elasticsearchIndexConfigService.getIndexName()}") +// private String INDEX_NAME_POSTFIX; +// public static final String INDEX_NAME = "keywords"; +// public static final String FIELD_NAME = "keyword"; +// public static final String[] MULTI_FIELD_NAMES = {"keyword", "keyword.nfc", "keyword.chosung"}; +// public static final int AUTOCOMPLETION_MAX_SIZE = 20; +// +// +// private final RestHighLevelClient elasticsearchClient; +// +// public List autocompleteKeyword(String prefix) throws IOException { +// +// MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery( +// prefix, // 검색할 쿼리 스트링 +// MULTI_FIELD_NAMES); // multi-match 쿼리를 실행할 필드 목록 정의 +// multiMatchQueryBuilder.fuzziness(Fuzziness.ZERO); // Fuzziness를 0으로 설정하여 정확히 일치하는 키워드만 검색 +// +// SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder() +// .query(multiMatchQueryBuilder) +// .size(AUTOCOMPLETION_MAX_SIZE); // 최대 20개 조회 +// +// // 조회 쿼리 생성 +// SearchRequest searchRequest = new SearchRequest(INDEX_NAME + INDEX_NAME_POSTFIX) +// .source(searchSourceBuilder); +// +// // 응답 데이터 가공 +// SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT); +// return Arrays.stream(searchResponse.getHits().getHits()) +// .map(hit -> hit.getSourceAsMap().get(FIELD_NAME).toString()) +// .toList(); +// } +//} +// diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java index b20df68b..26223b97 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticService.java @@ -1,13 +1,13 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; -import java.util.List; -import org.springframework.data.elasticsearch.core.SearchHits; - -public interface ElasticService { - default List> mapToElasticResponse(SearchHits searchHits) { - return searchHits.stream() - .map(searchHit -> new ElasticResponse<>(searchHit.getContent(), searchHit.getScore())) - .toList(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import com.dreamypatisiel.devdevdev.elastic.data.response.ElasticResponse; +//import java.util.List; +//import org.springframework.data.elasticsearch.core.SearchHits; +// +//public interface ElasticService { +// default List> mapToElasticResponse(SearchHits searchHits) { +// return searchHits.stream() +// .map(searchHit -> new ElasticResponse<>(searchHit.getContent(), searchHit.getScore())) +// .toList(); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java index bc30edd4..7c212f7e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleService.java @@ -1,188 +1,194 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_ELASTIC_METHODS_CALL_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._COMPANY_ID; -import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._ID; - -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; -import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import java.util.List; -import java.util.Optional; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.opensearch.action.search.SearchType; -import org.opensearch.data.client.orhlc.NativeSearchQuery; -import org.opensearch.data.client.orhlc.NativeSearchQueryBuilder; -import org.opensearch.index.query.Operator; -import org.opensearch.index.query.QueryBuilders; -import org.opensearch.search.sort.FieldSortBuilder; -import org.opensearch.search.sort.SortBuilder; -import org.opensearch.search.sort.SortBuilders; -import org.opensearch.search.sort.SortOrder; -import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; -import org.springframework.data.elasticsearch.core.ElasticsearchOperations; -import org.springframework.data.elasticsearch.core.SearchHits; -import org.springframework.stereotype.Service; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -@Slf4j -@Service -@RequiredArgsConstructor -public class ElasticTechArticleService implements ElasticService { - - private final ElasticsearchOperations elasticsearchOperations; - private final ElasticTechArticleRepository elasticTechArticleRepository; - - public SearchHits getTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, String keyword, - Long companyId, Float score) { - if (!StringUtils.hasText(keyword)) { - return findTechArticles(pageable, elasticId, techArticleSort, companyId); - } - - return searchTechArticles(pageable, elasticId, techArticleSort, keyword, companyId, score); - - } - - private SearchHits findTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, - Long companyId) { - // 정렬 기준 검증 - TechArticleSort validTechArticleSort = getValidSort(techArticleSort); - - // 쿼리 생성 - NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() - .withSearchType(SearchType.QUERY_THEN_FETCH) - .withPageable(pageable) - // 정렬 조건 설정 - .withSorts(getSortCondition(validTechArticleSort), - getPrimarySortCondition(LATEST_SORT_FIELD_NAME), - getPrimarySortCondition(_ID)); - - // 회사 필터 설정 - setFilterWithCompanyId(companyId, queryBuilder); - - NativeSearchQuery searchQuery = queryBuilder.build(); - - // searchAfter 설정 - setSearchAfterCondition(elasticId, validTechArticleSort, searchQuery); - - return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); - } - - private SearchHits searchTechArticles(Pageable pageable, String elasticId, - TechArticleSort techArticleSort, - String keyword, - Long companyId, Float score) - throws UncategorizedElasticsearchException { - - // 검색어 유무 확인 - if (!StringUtils.hasText(keyword)) { - throw new ElasticTechArticleException(INVALID_ELASTIC_METHODS_CALL_MESSAGE); - } - - // 정렬 기준 검증 - TechArticleSort validTechArticleSort = getValidSortWhenSearch(techArticleSort); - - // 쿼리 생성 - NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() - .withSearchType(SearchType.QUERY_THEN_FETCH) - .withPageable(pageable) - // 쿼리스트링 - .withQuery(QueryBuilders.queryStringQuery(keyword).defaultOperator(Operator.AND)) - // 정렬 조건 설정 - .withSorts(getSortCondition(validTechArticleSort), - getPrimarySortCondition(LATEST_SORT_FIELD_NAME), - getPrimarySortCondition(_ID)); - - // 회사 필터 설정 - setFilterWithCompanyId(companyId, queryBuilder); - - NativeSearchQuery searchQuery = queryBuilder.build(); - - // searchAfter 설정 - setSearchAfterConditionWhenSearch(elasticId, score, validTechArticleSort, searchQuery); - - return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); - } - - private static void setFilterWithCompanyId(Long companyId, NativeSearchQueryBuilder queryBuilder) { - if (ObjectUtils.isEmpty(companyId)) { - return; - } - queryBuilder.withFilter(QueryBuilders.termQuery(_COMPANY_ID, companyId)); - } - - private FieldSortBuilder getPrimarySortCondition(String fieldName) { - return SortBuilders.fieldSort(fieldName).order(SortOrder.DESC); - } - - private SortBuilder getSortCondition(TechArticleSort techArticleSort) { - return techArticleSort.getSortCondition(); - } - - private static TechArticleSort getValidSort(TechArticleSort techArticleSort) { - return Optional.ofNullable(techArticleSort) - .filter(sort -> sort != TechArticleSort.HIGHEST_SCORE) - .orElse(TechArticleSort.LATEST); - } - - private static TechArticleSort getValidSortWhenSearch(TechArticleSort techArticleSort) { - return Optional.ofNullable(techArticleSort).orElse(TechArticleSort.HIGHEST_SCORE); - } - - private void setSearchAfterCondition(String elasticId, TechArticleSort techArticleSort, - NativeSearchQuery searchQuery) { - if (!StringUtils.hasText(elasticId)) { - return; - } - - ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - - searchQuery.setSearchAfter(getSearchAfter(elasticTechArticle, techArticleSort)); - } - - private void setSearchAfterConditionWhenSearch(String elasticId, Float score, TechArticleSort techArticleSort, - NativeSearchQuery searchQuery) { - if (!StringUtils.hasText(elasticId)) { - return; - } - - ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) - .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); - - searchQuery.setSearchAfter(getSearchAfterWhenSearch(elasticTechArticle, techArticleSort, score)); - } - - private List getSearchAfter(ElasticTechArticle elasticTechArticle, TechArticleSort techArticleSort) { - return List.of(techArticleSort.getSearchAfterCondition(elasticTechArticle), - TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), - elasticTechArticle.getId()); - } - - private List getSearchAfterWhenSearch(ElasticTechArticle elasticTechArticle, - TechArticleSort techArticleSort, - Float score) { - // 정확도순 정렬이 아닌 경우 - if (!TechArticleSort.HIGHEST_SCORE.equals(techArticleSort)) { - return getSearchAfter(elasticTechArticle, techArticleSort); - } - - if (ObjectUtils.isEmpty(score)) { - throw new ElasticTechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE); - } - - return List.of(score, - TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), - elasticTechArticle.getId()); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_ELASTIC_METHODS_CALL_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.LATEST_SORT_FIELD_NAME; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._COMPANY_ID; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant._ID; +// +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; +//import com.dreamypatisiel.devdevdev.exception.NotFoundException; +//import java.util.List; +//import java.util.Optional; +//import lombok.RequiredArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.opensearch.action.search.SearchType; +//import org.opensearch.data.client.orhlc.NativeSearchQuery; +//import org.opensearch.data.client.orhlc.NativeSearchQueryBuilder; +//import org.opensearch.index.query.Operator; +//import org.opensearch.index.query.QueryBuilders; +//import org.opensearch.search.sort.FieldSortBuilder; +//import org.opensearch.search.sort.SortBuilder; +//import org.opensearch.search.sort.SortBuilders; +//import org.opensearch.search.sort.SortOrder; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; +//import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +//import org.springframework.data.elasticsearch.core.SearchHits; +//import org.springframework.stereotype.Service; +//import org.springframework.util.ObjectUtils; +//import org.springframework.util.StringUtils; +// +//import java.util.List; +//import java.util.Optional; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.*; +//import static com.dreamypatisiel.devdevdev.elastic.constant.ElasticsearchConstant.*; +// +//@Slf4j +//@Service +//@RequiredArgsConstructor +//public class ElasticTechArticleService implements ElasticService { +// +// private final ElasticsearchOperations elasticsearchOperations; +// private final ElasticTechArticleRepository elasticTechArticleRepository; +// +// public SearchHits getTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, String keyword, +// Long companyId, Float score) { +// if (!StringUtils.hasText(keyword)) { +// return findTechArticles(pageable, elasticId, techArticleSort, companyId); +// } +// +// return searchTechArticles(pageable, elasticId, techArticleSort, keyword, companyId, score); +// +// } +// +// private SearchHits findTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, +// Long companyId) { +// // 정렬 기준 검증 +// TechArticleSort validTechArticleSort = getValidSort(techArticleSort); +// +// // 쿼리 생성 +// NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() +// .withSearchType(SearchType.QUERY_THEN_FETCH) +// .withPageable(pageable) +// // 정렬 조건 설정 +// .withSorts(getSortCondition(validTechArticleSort), +// getPrimarySortCondition(LATEST_SORT_FIELD_NAME), +// getPrimarySortCondition(_ID)); +// +// // 회사 필터 설정 +// setFilterWithCompanyId(companyId, queryBuilder); +// +// NativeSearchQuery searchQuery = queryBuilder.build(); +// +// // searchAfter 설정 +// setSearchAfterCondition(elasticId, validTechArticleSort, searchQuery); +// +// return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); +// } +// +// private SearchHits searchTechArticles(Pageable pageable, String elasticId, +// TechArticleSort techArticleSort, +// String keyword, +// Long companyId, Float score) +// throws UncategorizedElasticsearchException { +// +// // 검색어 유무 확인 +// if (!StringUtils.hasText(keyword)) { +// throw new ElasticTechArticleException(INVALID_ELASTIC_METHODS_CALL_MESSAGE); +// } +// +// // 정렬 기준 검증 +// TechArticleSort validTechArticleSort = getValidSortWhenSearch(techArticleSort); +// +// // 쿼리 생성 +// NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder() +// .withSearchType(SearchType.QUERY_THEN_FETCH) +// .withPageable(pageable) +// // 쿼리스트링 +// .withQuery(QueryBuilders.queryStringQuery(keyword).defaultOperator(Operator.AND)) +// // 정렬 조건 설정 +// .withSorts(getSortCondition(validTechArticleSort), +// getPrimarySortCondition(LATEST_SORT_FIELD_NAME), +// getPrimarySortCondition(_ID)); +// +// // 회사 필터 설정 +// setFilterWithCompanyId(companyId, queryBuilder); +// +// NativeSearchQuery searchQuery = queryBuilder.build(); +// +// // searchAfter 설정 +// setSearchAfterConditionWhenSearch(elasticId, score, validTechArticleSort, searchQuery); +// +// return elasticsearchOperations.search(searchQuery, ElasticTechArticle.class); +// } +// +// private static void setFilterWithCompanyId(Long companyId, NativeSearchQueryBuilder queryBuilder) { +// if (ObjectUtils.isEmpty(companyId)) { +// return; +// } +// queryBuilder.withFilter(QueryBuilders.termQuery(_COMPANY_ID, companyId)); +// } +// +// private FieldSortBuilder getPrimarySortCondition(String fieldName) { +// return SortBuilders.fieldSort(fieldName).order(SortOrder.DESC); +// } +// +// private SortBuilder getSortCondition(TechArticleSort techArticleSort) { +// return techArticleSort.getSortCondition(); +// } +// +// private static TechArticleSort getValidSort(TechArticleSort techArticleSort) { +// return Optional.ofNullable(techArticleSort) +// .filter(sort -> sort != TechArticleSort.HIGHEST_SCORE) +// .orElse(TechArticleSort.LATEST); +// } +// +// private static TechArticleSort getValidSortWhenSearch(TechArticleSort techArticleSort) { +// return Optional.ofNullable(techArticleSort).orElse(TechArticleSort.HIGHEST_SCORE); +// } +// +// private void setSearchAfterCondition(String elasticId, TechArticleSort techArticleSort, +// NativeSearchQuery searchQuery) { +// if (!StringUtils.hasText(elasticId)) { +// return; +// } +// +// ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) +// .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); +// +// searchQuery.setSearchAfter(getSearchAfter(elasticTechArticle, techArticleSort)); +// } +// +// private void setSearchAfterConditionWhenSearch(String elasticId, Float score, TechArticleSort techArticleSort, +// NativeSearchQuery searchQuery) { +// if (!StringUtils.hasText(elasticId)) { +// return; +// } +// +// ElasticTechArticle elasticTechArticle = elasticTechArticleRepository.findById(elasticId) +// .orElseThrow(() -> new NotFoundException(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)); +// +// searchQuery.setSearchAfter(getSearchAfterWhenSearch(elasticTechArticle, techArticleSort, score)); +// } +// +// private List getSearchAfter(ElasticTechArticle elasticTechArticle, TechArticleSort techArticleSort) { +// return List.of(techArticleSort.getSearchAfterCondition(elasticTechArticle), +// TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), +// elasticTechArticle.getId()); +// } +// +// private List getSearchAfterWhenSearch(ElasticTechArticle elasticTechArticle, +// TechArticleSort techArticleSort, +// Float score) { +// // 정확도순 정렬이 아닌 경우 +// if (!TechArticleSort.HIGHEST_SCORE.equals(techArticleSort)) { +// return getSearchAfter(elasticTechArticle, techArticleSort); +// } +// +// if (ObjectUtils.isEmpty(score)) { +// throw new ElasticTechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE); +// } +// +// return List.of(score, +// TechArticleSort.LATEST.getSearchAfterCondition(elasticTechArticle), +// elasticTechArticle.getId()); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java b/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java index b6af9703..8d4a5c31 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/exception/ElasticTechArticleException.java @@ -1,7 +1,7 @@ -package com.dreamypatisiel.devdevdev.exception; - -public class ElasticTechArticleException extends IllegalArgumentException { - public ElasticTechArticleException(String s) { - super(s); - } -} +//package com.dreamypatisiel.devdevdev.exception; +// +//public class ElasticTechArticleException extends IllegalArgumentException { +// public ElasticTechArticleException(String s) { +// super(s); +// } +//} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java index 1328d9a2..6cbd1275 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java @@ -8,11 +8,18 @@ public class CustomMySQLFunctionContributor implements FunctionContributor { private static final String MATCH_AGAINST_FUNCTION = "match_against"; private static final String MATCH_AGAINST_PATTERN = "match (?1) against (?2 in boolean mode)"; + + private static final String MATCH_AGAINST_NL_FUNCTION = "match_against_nl"; + private static final String MATCH_AGAINST_NL_PATTERN = "match (?1) against (?2 in natural language mode)"; @Override public void contributeFunctions(FunctionContributions functionContributions) { functionContributions.getFunctionRegistry() .registerPattern(MATCH_AGAINST_FUNCTION, MATCH_AGAINST_PATTERN, functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE)); + + functionContributions.getFunctionRegistry() + .registerPattern(MATCH_AGAINST_NL_FUNCTION, MATCH_AGAINST_NL_PATTERN, + functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE)); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java index 2e249471..798895ae 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/constant/SecurityConstant.java @@ -22,6 +22,7 @@ public class SecurityConstant { "/**.html", "/**.css", "/**.js", + "/actuator/health", "/devdevdev/api/v1/oauth2/authorization/**", "/devdevdev/api/v1/oauth2/authorization/kakao", "/devdevdev/api/v1/login/oauth2/code/**", @@ -62,6 +63,7 @@ public class SecurityConstant { "/**.html", "/**.css", "/**.js", + "/actuator/health", "/devdevdev/api/v1/oauth2/authorization/**", "/devdevdev/api/v1/oauth2/authorization/kakao", "/devdevdev/api/v1/login/oauth2/code/**", diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java index 50402c69..977c348c 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/member/MypageController.java @@ -55,8 +55,8 @@ public ResponseEntity>> getBookmark @RequestParam(required = false) Long techArticleId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); - Slice response = memberService.getBookmarkedTechArticles(pageable, techArticleId, - bookmarkSort, authentication); + Slice response = memberService.getBookmarkedTechArticles( + pageable, techArticleId, bookmarkSort, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java index b3749ef3..f7423c9d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleController.java @@ -34,15 +34,15 @@ public class TechArticleController { public ResponseEntity>> getTechArticles( @PageableDefault Pageable pageable, @RequestParam(required = false) TechArticleSort techArticleSort, - @RequestParam(required = false) String elasticId, + @RequestParam(required = false) Long techArticleId, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long companyId, - @RequestParam(required = false) Float score) { - + @RequestParam(required = false) Float score + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); - Slice response = techArticleService.getTechArticles(pageable, elasticId, - techArticleSort, keyword, companyId, score, authentication); + Slice response = techArticleService.getTechArticles( + pageable, techArticleId, techArticleSort, keyword, companyId, score, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } @@ -51,8 +51,8 @@ public ResponseEntity>> getTechArti @GetMapping("/articles/{techArticleId}") public ResponseEntity> getTechArticle( @PathVariable Long techArticleId, - @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId) { - + @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); TechArticleDetailResponse response = techArticleService.getTechArticle(techArticleId, anonymousMemberId, authentication); @@ -62,8 +62,9 @@ public ResponseEntity> getTechArticle( @Operation(summary = "기술블로그 북마크") @PostMapping("/articles/{techArticleId}/bookmark") - public ResponseEntity> updateBookmark(@PathVariable Long techArticleId) { - + public ResponseEntity> updateBookmark( + @PathVariable Long techArticleId + ) { TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); BookmarkResponse response = techArticleService.updateBookmark(techArticleId, authentication); @@ -77,7 +78,6 @@ public ResponseEntity> updateRecomme @PathVariable Long techArticleId, @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId ) { - TechArticleService techArticleService = techArticleServiceStrategy.getTechArticleService(); Authentication authentication = AuthenticationMemberUtils.getAuthentication(); TechArticleRecommendResponse response = techArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java index bcf0f84d..d7f73b61 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleDetailResponse.java @@ -3,10 +3,12 @@ import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import java.time.LocalDate; + +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import lombok.Builder; import lombok.Data; +import org.springframework.util.ObjectUtils; import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.*; @@ -15,14 +17,13 @@ public class TechArticleDetailResponse { private static final int CONTENTS_MAX_LENGTH = 1000; - public final String elasticId; - public final String thumbnailUrl; - public final String techArticleUrl; public final String title; public final String contents; + public final String author; public final CompanyResponse company; public final LocalDate regDate; - public final String author; + public final String techArticleUrl; + public final String thumbnailUrl; public final Long viewTotalCount; public final Long recommendTotalCount; public final Long commentTotalCount; @@ -31,75 +32,74 @@ public class TechArticleDetailResponse { public final Boolean isRecommended; @Builder - private TechArticleDetailResponse(String elasticId, String thumbnailUrl, String techArticleUrl, String title, - String contents, CompanyResponse company, LocalDate regDate, String author, + private TechArticleDetailResponse(String title, String contents, String author, CompanyResponse company, LocalDate regDate, + String thumbnailUrl, String techArticleUrl, Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, Boolean isBookmarked, Boolean isRecommended) { - this.elasticId = elasticId; - this.thumbnailUrl = thumbnailUrl; - this.techArticleUrl = techArticleUrl; this.title = title; this.contents = contents; + this.author = author; this.company = company; this.regDate = regDate; - this.author = author; - this.isBookmarked = isBookmarked; + this.thumbnailUrl = thumbnailUrl; + this.techArticleUrl = techArticleUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; + this.isBookmarked = isBookmarked; this.isRecommended = isRecommended; } - public static TechArticleDetailResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - AnonymousMember anonymousMember) { + public static TechArticleDetailResponse of(TechArticle techArticle, AnonymousMember anonymousMember) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleDetailResponse.builder() + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl())) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(elasticTechArticle.getThumbnailUrl()) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(false) .isRecommended(isRecommendedByAnonymousMember(techArticle, anonymousMember)) + .isBookmarked(false) .build(); } - public static TechArticleDetailResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Member member) { + public static TechArticleDetailResponse of(TechArticle techArticle, Member member) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleDetailResponse.builder() + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl())) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(elasticTechArticle.getThumbnailUrl()) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) .isRecommended(isRecommendedByMember(techArticle, member)) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - private static String truncateString(String elasticTechArticleContents, int maxLength) { - if (elasticTechArticleContents.length() <= maxLength) { - return elasticTechArticleContents; + private static String truncateString(String contents, int maxLength) { + if (ObjectUtils.isEmpty(contents) || contents.length() <= maxLength) { + return contents; } + return contents.substring(0, maxLength); + } - return elasticTechArticleContents.substring(0, maxLength); + private static String getThumbnailUrl(Url thumbnailUrl) { + if (ObjectUtils.isEmpty(thumbnailUrl)) { + return null; + } + return thumbnailUrl.getUrl(); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java index 10d54dcf..a4eb9d2b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponse.java @@ -1,174 +1,159 @@ package com.dreamypatisiel.devdevdev.web.dto.response.techArticle; -import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.isBookmarkedByMember; - import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import java.time.LocalDate; -import java.util.Objects; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import lombok.Builder; import lombok.Data; import org.springframework.util.ObjectUtils; +import java.time.LocalDate; +import java.util.Objects; + +import static com.dreamypatisiel.devdevdev.web.dto.util.TechArticleResponseUtils.isBookmarkedByMember; + @Data public class TechArticleMainResponse { public static final int CONTENTS_MAX_LENGTH = 1000; public final Long id; - public final String elasticId; - public final String thumbnailUrl; - public final Boolean isLogoImage; - public final String techArticleUrl; public final String title; public final String contents; + public final String author; public final CompanyResponse company; public final LocalDate regDate; - public final String author; + public final String techArticleUrl; + public final String thumbnailUrl; public final Long viewTotalCount; public final Long recommendTotalCount; public final Long commentTotalCount; public final Long popularScore; + public final Boolean isLogoImage; public final Boolean isBookmarked; public final Float score; @Builder - private TechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, - String contents, - CompanyResponse company, LocalDate regDate, String author, Long viewTotalCount, - Long recommendTotalCount, Long commentTotalCount, Long popularScore, - Boolean isBookmarked, Float score) { + private TechArticleMainResponse(Long id, String title, String contents, String author, CompanyResponse company, + LocalDate regDate, String thumbnailUrl, String techArticleUrl, + Long viewTotalCount, Long recommendTotalCount, Long commentTotalCount, Long popularScore, + Boolean isLogoImage, Boolean isBookmarked, Float score) { this.id = id; - this.elasticId = elasticId; - this.thumbnailUrl = thumbnailUrl; - this.isLogoImage = isLogoImage; - this.techArticleUrl = techArticleUrl; this.title = title; this.contents = contents; + this.author = author; this.company = company; this.regDate = regDate; - this.author = author; - this.isBookmarked = isBookmarked; + this.techArticleUrl = techArticleUrl; + this.thumbnailUrl = thumbnailUrl; this.viewTotalCount = viewTotalCount; this.recommendTotalCount = recommendTotalCount; this.commentTotalCount = commentTotalCount; this.popularScore = popularScore; + this.isLogoImage = isLogoImage; + this.isBookmarked = isBookmarked; this.score = score; } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse) { + + public static TechArticleMainResponse of(TechArticle techArticle, Member member) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(false) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Member member) { + public static TechArticleMainResponse of(TechArticle techArticle) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) + .isBookmarked(false) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Float score) { + public static TechArticleMainResponse of(TechArticle techArticle, Member member, Float score) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) - .isBookmarked(false) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) + .isBookmarked(isBookmarkedByMember(techArticle, member)) .score(getValidScore(score)) .build(); } - public static TechArticleMainResponse of(TechArticle techArticle, - ElasticTechArticle elasticTechArticle, - CompanyResponse companyResponse, - Float score, - Member member) { + public static TechArticleMainResponse of(TechArticle techArticle, Float score) { + CompanyResponse companyResponse = CompanyResponse.from(techArticle.getCompany()); return TechArticleMainResponse.builder() .id(techArticle.getId()) + .title(techArticle.getTitle().getTitle()) + .contents(truncateString(techArticle.getContents(), CONTENTS_MAX_LENGTH)) + .author(techArticle.getAuthor()) + .company(companyResponse) + .regDate(techArticle.getRegDate()) + .thumbnailUrl(getThumbnailUrl(techArticle.getThumbnailUrl(), companyResponse)) + .techArticleUrl(techArticle.getTechArticleUrl().getUrl()) .viewTotalCount(techArticle.getViewTotalCount().getCount()) .recommendTotalCount(techArticle.getRecommendTotalCount().getCount()) .commentTotalCount(techArticle.getCommentTotalCount().getCount()) .popularScore(techArticle.getPopularScore().getCount()) - .elasticId(elasticTechArticle.getId()) - .thumbnailUrl(getThumbnailUrl(elasticTechArticle, companyResponse)) - .isLogoImage(ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) - .techArticleUrl(elasticTechArticle.getTechArticleUrl()) - .title(elasticTechArticle.getTitle()) - .contents(truncateString(elasticTechArticle.getContents(), CONTENTS_MAX_LENGTH)) - .company(companyResponse) - .regDate(elasticTechArticle.getRegDate()) - .author(elasticTechArticle.getAuthor()) + .isLogoImage(ObjectUtils.isEmpty(techArticle.getThumbnailUrl())) + .isBookmarked(false) .score(getValidScore(score)) - .isBookmarked(isBookmarkedByMember(techArticle, member)) .build(); } - private static String getThumbnailUrl(ElasticTechArticle elasticTechArticle, CompanyResponse companyResponse) { + private static String getThumbnailUrl(Url thumbnailUrl, CompanyResponse companyResponse) { // 썸네일 이미지가 없다면 회사 로고로 내려준다. - if (ObjectUtils.isEmpty(elasticTechArticle.getThumbnailUrl())) { + if (ObjectUtils.isEmpty(thumbnailUrl) || thumbnailUrl.getUrl() == null) { return companyResponse.getOfficialImageUrl(); } - - return elasticTechArticle.getThumbnailUrl(); + return thumbnailUrl.getUrl(); } private static Float getValidScore(Float score) { return Objects.isNull(score) || Float.isNaN(score) ? null : score; } - private static String truncateString(String elasticTechArticleContents, int maxLength) { - if (elasticTechArticleContents.length() <= maxLength) { - return elasticTechArticleContents; + private static String truncateString(String contents, int maxLength) { + if (ObjectUtils.isEmpty(contents) || contents.length() <= maxLength) { + return contents; } - - return elasticTechArticleContents.substring(0, maxLength); + return contents.substring(0, maxLength); } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 82718e14..10d39c46 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -66,6 +66,17 @@ spring: max-idle: 8 min-idle: 2 +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + show-details: never + probes: + enabled: true + #MyBatis mybatis: type-aliases-package: com.dreamypatisiel.devdevdev.domain.repository diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b4129870..0d4d76a7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -6,5 +6,4 @@ spring: - "oauth2-${spring.profiles.active}" - "jwt-${spring.profiles.active}" - "storage-s3-${spring.profiles.active}" - - "opensearch-${spring.profiles.active}" - open-ai \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java index 7980c0fc..7cd3834c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleRepositoryTest.java @@ -39,31 +39,6 @@ class TechArticleRepositoryTest { @Autowired EntityManager em; - @Test - @DisplayName("elasticIds 리스트의 elasticId에 해당하는 기술블로그 엔티티를 순서대로 가져올 수 있다.") - void findAllByElasticIdIn() { - // given - Company company = Company.builder().name(new CompanyName("회사")).build(); - companyRepository.save(company); - - TechArticle techArticle1 = createTechArticle(company, "elasticId1"); - TechArticle techArticle2 = createTechArticle(company, "elasticId2"); - TechArticle techArticle3 = createTechArticle(company, "elasticId3"); - TechArticle techArticle4 = createTechArticle(company, "elasticId4"); - - techArticleRepository.saveAll(List.of(techArticle1, techArticle2, techArticle3, techArticle4)); - - List elasticIds = List.of("elasticId1", "elasticId3", "elasticId2"); - - // when - List techArticles = techArticleRepository.findAllByElasticIdIn(elasticIds); - - // then - assertThat(techArticles).hasSize(3) - .extracting(TechArticle::getElasticId) - .containsExactly("elasticId1", "elasticId3", "elasticId2"); - } - @Test @DisplayName("기술블로그 북마크 목록을 북마크 등록시간 내림차순으로 가져올 수 있다.") void findBookmarkedByCursorOrderByBookmarkedDesc() { @@ -163,12 +138,8 @@ void findBookmarkedByCursorOrderByCommentDesc() { .containsExactly(techArticle1, techArticle3); } - private static TechArticle createTechArticle(Company company, String elasticId) { - return TechArticle.builder() - .company(company) - .elasticId(elasticId) - .build(); - } + // TODO + // 기술블로그 조회 관련 테스트코드 private static TechArticle createTechArticle(Company company, Count commentTotalCount) { return TechArticle.builder() diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java index 1df770b8..20378785 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberServiceTest.java @@ -32,7 +32,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NicknameException; import com.dreamypatisiel.devdevdev.exception.SurveyException; @@ -53,6 +52,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.subscription.SubscribedCompanyResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -73,9 +74,11 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.transaction.annotation.Transactional; @SpringBootTest -class MemberServiceTest extends ElasticsearchSupportTest { +@Transactional +class MemberServiceTest { String userId = "dreamy5patisiel"; String name = "꿈빛파티시엘"; @@ -422,7 +425,14 @@ void getBookmarkedTechArticles() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Bookmark bookmark = createBookmark(member, firstTechArticle, true); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company, "기술블로그 제목"); + techArticleRepository.save(techArticle); + + Bookmark bookmark = createBookmark(member, techArticle, true); bookmarkRepository.save(bookmark); em.flush(); @@ -1314,8 +1324,17 @@ private static TechComment createTechComment(TechArticle techArticle, Member mem private static TechArticle createTechArticle(Company company, String title) { return TechArticle.builder() - .company(company) .title(new Title(title)) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/article.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } @@ -1432,4 +1451,14 @@ private Bookmark createBookmark(Member member, TechArticle techArticle, boolean .status(status) .build(); } + + public static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialUrl(new Url(officialUrl)) + .careerUrl(new Url(careerUrl)) + .officialImageUrl(new Url(officialImageUrl)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java index 5ba792d3..c61e236a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/notification/NotificationServiceTest.java @@ -15,8 +15,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.notification.NotificationRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; @@ -72,8 +70,6 @@ class NotificationServiceTest { @Autowired TechArticleRepository techArticleRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired SubscriptionRepository subscriptionRepository; String userId = "dreamy5patisiel"; @@ -85,9 +81,7 @@ class NotificationServiceTest { String role = Role.ROLE_USER.name(); @AfterAll - static void tearDown(@Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired TechArticleRepository techArticleRepository) { - elasticTechArticleRepository.deleteAll(); + static void tearDown(@Autowired TechArticleRepository techArticleRepository) { techArticleRepository.deleteAllInBatch(); } @@ -253,9 +247,7 @@ void getNotificationPopup() { List notifications = new ArrayList<>(); for (int i = 0; i < 6; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); - + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); } @@ -326,24 +318,11 @@ void getNotifications() { "https://example.com"); companyRepository.save(company); - - List elasticTechArticles = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId" + i, "기술블로그 제목 "+i, - LocalDate.now(), "기술블로그 내용", "https://example.com", "기술블로그 설명", - "https://example.com/thumbnail.png", "작성자", "회사명", company.getId(), - 1L, 1L, 1L, 1L); - elasticTechArticles.add(elasticTechArticle); - } - elasticTechArticleRepository.saveAll(elasticTechArticles); - List techArticles = new ArrayList<>(); List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), elasticTechArticles.get(i).getId(), company); - + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); } @@ -371,7 +350,6 @@ void getNotifications() { assertThat(content).allSatisfy(notificationNewArticleResponse -> { assertThat(notificationNewArticleResponse.getNotificationId()).isInstanceOf(Long.class); assertThat(notificationNewArticleResponse.getTechArticle().getId()).isInstanceOf(Long.class); - assertThat(notificationNewArticleResponse.getTechArticle().getElasticId()).isInstanceOf(String.class); assertThat(notificationNewArticleResponse.getTechArticle().getCompany().getId()).isInstanceOf(Long.class); }); @@ -583,8 +561,7 @@ void getUnreadNotificationCount() { List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); @@ -635,8 +612,7 @@ void deleteAllByMember() { List notifications = new ArrayList<>(); for (int i = 0; i < 10; i++) { - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목 "+i), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticles.add(techArticle); notifications.add(createNotification(member, "알림 메시지 " + i, NotificationType.SUBSCRIPTION, false, techArticle)); @@ -669,12 +645,6 @@ private static Member createMember() { .build(); } - private static TechArticle createTechArticle(Company company) { - return TechArticle.builder() - .company(company) - .build(); - } - private static Company createCompany(String name) { return Company.builder() .name(new CompanyName(name)) @@ -734,27 +704,35 @@ private static Company createCompany(String companyName, String officialImageUrl .build(); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("기술블로그 제목 " + i)) + .contents("기술블로그 내용") .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com")) + .thumbnailUrl(new Url("https://example.com/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java index 4d4080ca..5abbb131 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechArticleServiceTest.java @@ -1,39 +1,40 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.Company; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechArticleRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleRecommendResponse; import jakarta.persistence.EntityManager; +import org.springframework.test.context.transaction.BeforeTransaction; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -42,17 +43,37 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; - +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; -import java.util.Optional; -class GuestTechArticleServiceTest extends ElasticsearchSupportTest { +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@SpringBootTest +@Transactional +@Testcontainers +class GuestTechArticleServiceTest { @Autowired GuestTechArticleService guestTechArticleService; @Autowired TechArticleRepository techArticleRepository; @Autowired + CompanyRepository companyRepository; + @Autowired TechArticleRecommendRepository techArticleRecommendRepository; @Autowired AnonymousMemberService anonymousMemberService; @@ -62,11 +83,89 @@ class GuestTechArticleServiceTest extends ElasticsearchSupportTest { Authentication authentication; @Mock SecurityContext securityContext; + @Autowired + DataSource dataSource; + + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); String email = "dreamy5patisiel@kakao.com"; String socialType = SocialType.KAKAO.name(); String role = Role.ROLE_USER.name(); + private static final int TEST_ARTICLES_COUNT = 20; + private static Company testCompany; + private static List testTechArticles; + private static boolean indexesCreated = false; + + @BeforeTransaction + public void initIndexes() throws SQLException { + if (!indexesCreated) { + // 인덱스 생성 + createFulltextIndexesWithJDBC(); + indexesCreated = true; + + // 데이터 추가 + testCompany = createCompany("꿈빛 파티시엘", "https://example.com/company.png", + "https://example.com", "https://example.com"); + companyRepository.save(testCompany); + + testTechArticles = new ArrayList<>(); + for (int i = 0; i < TEST_ARTICLES_COUNT; i++) { + TechArticle techArticle = createTechArticle(i, testCompany); + testTechArticles.add(techArticle); + } + techArticleRepository.saveAll(testTechArticles); + } + } + + /** + * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 + */ + private void createFulltextIndexesWithJDBC() throws SQLException { + Connection connection = null; + try { + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__title ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__contents ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__title_contents ON tech_article"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 (개별 + 복합) + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title ON tech_article (title) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__contents ON tech_article (contents) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title_contents ON tech_article (title, contents) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + } + @Test @DisplayName("익명 사용자가 커서 방식으로 기술블로그를 조회하여 응답을 생성한다.") void getTechArticles() { @@ -83,7 +182,29 @@ void getTechArticles() { // then assertThat(techArticles) - .hasSize(pageable.getPageSize()); + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @Test @@ -109,14 +230,20 @@ void getTechArticlesNotAnonymousUserException() { @DisplayName("익명 사용자가 기술블로그 상세를 조회한다. 이때 북마크 값은 false 이다.") void getTechArticle() { // given - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); // when - TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(id, null, + TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(techArticleId, null, authentication); // then @@ -124,7 +251,23 @@ void getTechArticle() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isFalse(); + assertThat(article.getIsRecommended()).isNotNull(); }); } @@ -138,10 +281,17 @@ void getTechArticleWithRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, firstTechArticle); + TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, techArticle); techArticleRecommendRepository.save(techArticleRecommend); em.flush(); @@ -156,6 +306,22 @@ void getTechArticleWithRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isFalse(); assertThat(article.getIsRecommended()).isTrue(); }); } @@ -164,21 +330,44 @@ void getTechArticleWithRecommend() { @DisplayName("익명 사용자가 기술블로그 상세를 조회하면 조회수가 1 증가한다.") void getTechArticleIncrementViewCount() { // given - Long id = firstTechArticle.getId(); - long prevViewTotalCount = firstTechArticle.getViewTotalCount().getCount(); - long prevPopularScore = firstTechArticle.getPopularScore().getCount(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + long prevViewTotalCount = techArticle.getViewTotalCount().getCount(); + long prevPopularScore = techArticle.getPopularScore().getCount(); when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); // when - TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(id, null, + TechArticleDetailResponse techArticleDetailResponse = guestTechArticleService.getTechArticle(techArticleId, null, authentication); // then assertThat(techArticleDetailResponse) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isFalse(); + assertThat(article.getIsRecommended()).isNotNull(); assertThat(article.getViewTotalCount()).isEqualTo(prevViewTotalCount + 1); assertThat(article.getPopularScore()).isEqualTo(prevPopularScore + 2); }); @@ -188,7 +377,13 @@ void getTechArticleIncrementViewCount() { @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 익명 사용자가 아니면 예외가 발생한다.") void getTechArticleNotAnonymousUserException() { // given - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); UserPrincipal userPrincipal = UserPrincipal.createByEmailAndRoleAndSocialType(email, role, socialType); SecurityContext context = SecurityContextHolder.getContext(); @@ -197,7 +392,7 @@ void getTechArticleNotAnonymousUserException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) + assertThatThrownBy(() -> guestTechArticleService.getTechArticle(techArticleId, null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE); } @@ -206,60 +401,14 @@ void getTechArticleNotAnonymousUserException() { @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() { // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - when(authentication.getPrincipal()).thenReturn("anonymousUser"); - when(securityContext.getAuthentication()).thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); - - // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); - } - - @Test - @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() { - // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); - when(authentication.getPrincipal()).thenReturn("anonymousUser"); - when(securityContext.getAuthentication()).thenReturn(authentication); - SecurityContextHolder.setContext(securityContext); - - // when // then - assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(TechArticleException.class) - .hasMessage(NOT_FOUND_ELASTIC_ID_MESSAGE); - } - - @Test - @DisplayName("익명 사용자가 기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() { - // given - TechArticle techArticle = TechArticle.createTechArticle( - new Title("매일 1,000만 사용자의 데이터를 꿈파는 어떻게 처리할까?"), - new Url("https://example.com"), new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + Long id = techArticleId + 1; when(authentication.getPrincipal()).thenReturn("anonymousUser"); when(securityContext.getAuthentication()).thenReturn(authentication); @@ -268,7 +417,7 @@ void getTechArticleNotFoundElasticTechArticleException() { // when // then assertThatThrownBy(() -> guestTechArticleService.getTechArticle(id, null, authentication)) .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); + .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @Test @@ -279,10 +428,16 @@ void updateBookmarkAccessDeniedException() { when(securityContext.getAuthentication()).thenReturn(authentication); SecurityContextHolder.setContext(securityContext); - Long id = firstTechArticle.getId(); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // when // then - assertThatThrownBy(() -> guestTechArticleService.updateBookmark(id, authentication)) + assertThatThrownBy(() -> guestTechArticleService.updateBookmark(techArticleId, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -297,9 +452,17 @@ void createTechArticleRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + Count popularScore = techArticle.getPopularScore(); + Count recommendTotalCount = techArticle.getRecommendTotalCount(); // when TechArticleRecommendResponse techArticleRecommendResponse = guestTechArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); @@ -313,18 +476,18 @@ void createTechArticleRecommend() { assertThat(response.getRecommendTotalCount()).isEqualTo(recommendTotalCount.getCount() + 1); }); - TechArticle techArticle = techArticleRepository.findById(techArticleId).get(); - assertThat(techArticle) + TechArticle findTechArticle = techArticleRepository.findById(techArticleId).get(); + assertThat(findTechArticle) .satisfies(article -> { assertThat(article.getRecommendTotalCount().getCount()).isEqualTo(recommendTotalCount.getCount() + 1); assertThat(article.getPopularScore().getCount()).isEqualTo(popularScore.getCount() + 4); }); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(firstTechArticle, anonymousMember).get(); + TechArticleRecommend techArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(techArticle, anonymousMember).get(); assertThat(techArticleRecommend) .satisfies(recommend -> { - assertThat(recommend.getTechArticle().getId()).isEqualTo(firstTechArticle.getId()); + assertThat(recommend.getTechArticle().getId()).isEqualTo(techArticle.getId()); assertThat(recommend.getAnonymousMember()).isEqualTo(anonymousMember); assertThat(recommend.isRecommended()).isTrue(); }); @@ -340,13 +503,26 @@ void cancelTechArticleRecommend() { SecurityContextHolder.setContext(securityContext); String anonymousMemberId = "GA1.1.276672604.1715872960"; - Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, firstTechArticle); + TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(anonymousMember, techArticle); techArticleRecommendRepository.save(techArticleRecommend); + + // 추천 후 상태 저장 + em.flush(); + em.clear(); + + TechArticle updatedTechArticle = techArticleRepository.findById(techArticleId).get(); + Count popularScore = updatedTechArticle.getPopularScore(); + Count recommendTotalCount = updatedTechArticle.getRecommendTotalCount(); // when TechArticleRecommendResponse techArticleRecommendResponse = guestTechArticleService.updateRecommend(techArticleId, anonymousMemberId, authentication); @@ -362,20 +538,401 @@ void cancelTechArticleRecommend() { assertThat(response.getStatus()).isFalse(); }); - TechArticle techArticle = techArticleRepository.findById(techArticleId).get(); - assertThat(techArticle) + TechArticle findTechArticle = techArticleRepository.findById(techArticleId).get(); + assertThat(findTechArticle) .satisfies(article -> { assertThat(article.getRecommendTotalCount().getCount()).isEqualTo(recommendTotalCount.getCount() - 1L); assertThat(article.getPopularScore().getCount()).isEqualTo(popularScore.getCount() - 4L); }); AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); - TechArticleRecommend findTechArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(firstTechArticle, findAnonymousMember).get(); + TechArticleRecommend findTechArticleRecommend = techArticleRecommendRepository.findByTechArticleAndAnonymousMember(techArticle, findAnonymousMember).get(); assertThat(findTechArticleRecommend) .satisfies(recommend -> { - assertThat(recommend.getTechArticle().getId()).isEqualTo(firstTechArticle.getId()); + assertThat(recommend.getTechArticle().getId()).isEqualTo(techArticle.getId()); assertThat(recommend.getAnonymousMember()).isEqualTo(findAnonymousMember); assertThat(recommend.isRecommended()).isFalse(); }); } + + @ParameterizedTest + @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) + @DisplayName("익명 사용자가 다양한 정렬 기준으로 기술블로그를 조회한다.") + void getTechArticlesWithDifferentSorts(TechArticleSort sort) { + // given + Pageable pageable = PageRequest.of(0, 10); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, sort, null, null, null, authentication); + + // then + assertThat(techArticles).hasSize(pageable.getPageSize()); + + List articles = techArticles.getContent(); + + assertThat(articles).allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }); + + // 정렬 검증 + switch (sort) { + case LATEST -> assertThat(articles) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_VIEWED -> assertThat(articles) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_COMMENTED -> assertThat(articles) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case POPULAR -> assertThat(articles) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 최신순으로 조회한다.") + void getTechArticlesWithCursorOrderByLatest() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.LATEST, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.LATEST, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(date -> !date.isAfter(cursor.getRegDate())); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 조회순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostViewed() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 댓글순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostCommented() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_COMMENTED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); + } + + @Test + @DisplayName("익명 사용자가 커서 방식으로 다음 페이지의 기술블로그를 인기순으로 조회한다.") + void getTechArticlesWithCursorOrderByPopular() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // 첫 번째 페이지 조회 + Slice firstPage = guestTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.POPULAR, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = guestTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.POPULAR, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + }) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); + } + + @Test + @DisplayName("익명 사용자가 키워드로 기술블로그를 검색한다.") + void getTechArticlesWithKeyword() { + // given + Pageable pageable = PageRequest.of(0, 10); + String keyword = "내용"; + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, null, keyword, null, null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + boolean containsKeyword = article.getTitle().contains(keyword) || + article.getContents().contains(keyword); + assertThat(containsKeyword).isTrue(); + }); + } + + @Test + @DisplayName("익명 사용자가 특정 회사의 기술블로그만 필터링하여 조회한다.") + void getTechArticlesFilterByCompany() { + // given + Pageable pageable = PageRequest.of(0, 10); + + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + when(securityContext.getAuthentication()).thenReturn(authentication); + SecurityContextHolder.setContext(securityContext); + + // when + Slice techArticles = guestTechArticleService.getTechArticles( + pageable, null, TechArticleSort.LATEST, null, testCompany.getId(), null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull().isFalse(); + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + + private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialImageUrl(new Url(officialImageUrl)) + .careerUrl(new Url(careerUrl)) + .officialUrl(new Url(officialUrl)) + .build(); + } + + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(10)) + .build(); + } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now().minusDays(i)) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(10L *i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index ac0d746c..cf5a9af9 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java @@ -41,6 +41,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -107,18 +109,16 @@ void registerTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when // then assertThatThrownBy(() -> guestTechCommentService.registerMainTechComment( - id, registerCommentDto, authentication)) + techArticleId, registerCommentDto, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -137,8 +137,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -171,9 +170,7 @@ void recommendTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -202,9 +199,7 @@ void getTechCommentsSortByOLDEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -496,9 +491,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -665,9 +658,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -959,9 +950,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1107,8 +1096,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1283,8 +1271,7 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1406,4 +1393,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) ); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java index ff70ca5f..eff2bfc7 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechArticleServiceTest.java @@ -1,40 +1,44 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle; import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import com.dreamypatisiel.devdevdev.domain.entity.*; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; +import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; -import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.BookmarkResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleDetailResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleRecommendResponse; import jakarta.persistence.EntityManager; +import org.springframework.test.context.transaction.BeforeTransaction; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -42,14 +46,27 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; - -class MemberTechArticleServiceTest extends ElasticsearchSupportTest { - +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import java.sql.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +@SpringBootTest +@Transactional +@Testcontainers +class MemberTechArticleServiceTest { @Autowired MemberTechArticleService memberTechArticleService; @Autowired TechArticleRepository techArticleRepository; @Autowired + CompanyRepository companyRepository; + @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @@ -57,6 +74,20 @@ class MemberTechArticleServiceTest extends ElasticsearchSupportTest { TechArticleRecommendRepository techArticleRecommendRepository; @Autowired EntityManager em; + @Autowired + DataSource dataSource; + + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); String userId = "dreamy5patisiel"; String name = "꿈빛파티시엘"; @@ -66,6 +97,72 @@ class MemberTechArticleServiceTest extends ElasticsearchSupportTest { String socialType = SocialType.KAKAO.name(); String role = Role.ROLE_USER.name(); + private static final int TEST_ARTICLES_COUNT = 20; + private static Company testCompany; + private static List testTechArticles; + private static TechArticle firstTechArticle; + private static boolean indexesCreated = false; + + @BeforeTransaction + public void initIndexes() throws SQLException { + if (!indexesCreated) { + // 인덱스 생성 + createFulltextIndexesWithJDBC(); + indexesCreated = true; + + // 데이터 추가 + testCompany = createCompany("꿈빛 파티시엘", "https://example.com/company.png", + "https://example.com", "https://example.com"); + companyRepository.save(testCompany); + + testTechArticles = new ArrayList<>(); + for (int i = 0; i < TEST_ARTICLES_COUNT; i++) { + TechArticle techArticle = createTechArticle(i, testCompany); + testTechArticles.add(techArticle); + } + techArticleRepository.saveAll(testTechArticles); + firstTechArticle = testTechArticles.get(1); + } + } + + /** + * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 + */ + private void createFulltextIndexesWithJDBC() throws SQLException { + Connection connection = null; + try { + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__title ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__contents ON tech_article"); + statement.executeUpdate("DROP INDEX idx__ft__title_contents ON tech_article"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 (개별 + 복합) + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title ON tech_article (title) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__contents ON tech_article (contents) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__title_contents ON tech_article (title, contents) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + } + @Test @DisplayName("회원이 커서 방식으로 기술블로그를 조회하여 응답을 생성한다.") void getTechArticles() { @@ -88,7 +185,30 @@ void getTechArticles() { // then assertThat(techArticles) - .hasSize(pageable.getPageSize()); + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + // 모든 필드 검증 + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); // 기본 정렬은 최신순 } @@ -136,7 +256,24 @@ void getTechArticle() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getIsRecommended()).isNotNull(); }); } @@ -168,6 +305,22 @@ void getTechArticleWithRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); assertThat(article.getIsRecommended()).isTrue(); }); @@ -198,6 +351,22 @@ void getTechArticleWithoutRecommend() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); assertThat(article.getIsBookmarked()).isNotNull(); assertThat(article.getIsRecommended()).isFalse(); }); @@ -230,6 +399,22 @@ void getTechArticleIncrementViewCount() { .isNotNull() .isInstanceOf(TechArticleDetailResponse.class) .satisfies(article -> { + // 모든 필드 검증 + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getIsRecommended()).isNotNull(); assertThat(article.getViewTotalCount()).isEqualTo(prevViewTotalCount + 1); assertThat(article.getPopularScore()).isEqualTo(prevPopularScore + 2); }); @@ -257,11 +442,11 @@ void getTechArticleNotFoundMemberException() { @DisplayName("회원이 기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + TechArticle techArticle = createTechArticle(1, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -281,62 +466,6 @@ void getTechArticleNotFoundTechArticleException() { .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } - @Test - @DisplayName("회원이 기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - memberRepository.save(member); - - UserPrincipal userPrincipal = UserPrincipal.createByMember(member); - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), - userPrincipal.getSocialType().name())); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // when // then - assertThatThrownBy(() -> memberTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(TechArticleException.class) - .hasMessage(NOT_FOUND_ELASTIC_ID_MESSAGE); - } - - @Test - @DisplayName("회원이 기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - memberRepository.save(member); - - UserPrincipal userPrincipal = UserPrincipal.createByMember(member); - SecurityContext context = SecurityContextHolder.getContext(); - context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), - userPrincipal.getSocialType().name())); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // when // then - assertThatThrownBy(() -> memberTechArticleService.getTechArticle(id, null, authentication)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); - } - @Test @DisplayName("회원이 북마크 한 적 없는 기술블로그의 북마크를 요청하면 새로운 북마크가 생성된다.") void createBookmark() { @@ -453,12 +582,12 @@ void cancelTechArticleRecommend() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Long techArticleId = firstTechArticle.getId(); - Count popularScore = firstTechArticle.getPopularScore(); - Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); - TechArticleRecommend techArticleRecommend = TechArticleRecommend.create(member, firstTechArticle); techArticleRecommendRepository.save(techArticleRecommend); + Count popularScore = firstTechArticle.getPopularScore(); + Count recommendTotalCount = firstTechArticle.getRecommendTotalCount(); + // when TechArticleRecommendResponse techArticleRecommendResponse = memberTechArticleService.updateRecommend(techArticleId, null, authentication); @@ -487,6 +616,278 @@ void cancelTechArticleRecommend() { }); } + @ParameterizedTest + @EnumSource(value = TechArticleSort.class, names = {"LATEST", "MOST_VIEWED", "MOST_COMMENTED", "POPULAR"}) + @DisplayName("회원이 다양한 정렬 기준으로 기술블로그를 조회한다.") + void getTechArticlesWithDifferentSorts(TechArticleSort sort) { + // given + Pageable pageable = PageRequest.of(0, 10); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, sort, null, null, null, authentication); + + // then + assertThat(techArticles).hasSize(pageable.getPageSize()); + + List articles = techArticles.getContent(); + + // 모든 응답 객체의 필드 검증 + assertThat(articles).allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }); + + // 정렬 검증 + switch (sort) { + case LATEST -> assertThat(articles) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_VIEWED -> assertThat(articles) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case MOST_COMMENTED -> assertThat(articles) + .extracting(TechArticleMainResponse::getCommentTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()); + case POPULAR -> assertThat(articles) + .extracting(TechArticleMainResponse::getPopularScore) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + } + + @Test + @DisplayName("회원이 커서 방식으로 다음 페이지의 기술블로그를 최신순으로 조회한다.") + void getTechArticlesWithCursorOrderByLatest() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 첫 번째 페이지 조회 + Slice firstPage = memberTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.LATEST, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = memberTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.LATEST, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(date -> !date.isAfter(cursor.getRegDate())); + } + + @Test + @DisplayName("회원이 커서 방식으로 다음 페이지의 기술블로그를 조회순으로 조회한다.") + void getTechArticlesWithCursorOrderByMostViewed() { + // given + Pageable prevPageable = PageRequest.of(0, 1); + Pageable pageable = PageRequest.of(0, 5); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // 첫 번째 페이지 조회 + Slice firstPage = memberTechArticleService.getTechArticles( + prevPageable, null, TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + TechArticleMainResponse cursor = firstPage.getContent().get(0); + + // when + Slice secondPage = memberTechArticleService.getTechArticles( + pageable, cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null, authentication); + + // then + assertThat(secondPage) + .hasSize(pageable.getPageSize()) + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + }) + .extracting(TechArticleMainResponse::getViewTotalCount) + .isSortedAccordingTo(Comparator.reverseOrder()) + .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); + } + + @Test + @DisplayName("회원이 키워드로 기술블로그를 검색한다.") + void getTechArticlesWithKeyword() { + // given + Pageable pageable = PageRequest.of(0, 10); + String keyword = "내용"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, null, keyword, null, null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + boolean containsKeyword = article.getTitle().contains(keyword) || + article.getContents().contains(keyword); + assertThat(containsKeyword).isTrue(); + }); + } + + @Test + @DisplayName("회원이 특정 회사의 기술블로그만 필터링하여 조회한다.") + void getTechArticlesFilterByCompany() { + // given + Pageable pageable = PageRequest.of(0, 10); + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // when + Slice techArticles = memberTechArticleService.getTechArticles( + pageable, null, TechArticleSort.LATEST, null, testCompany.getId(), null, authentication); + + // then + assertThat(techArticles.getContent()) + .isNotEmpty() + .allSatisfy(article -> { + assertThat(article.getId()).isNotNull(); + assertThat(article.getTitle()).isNotNull().isNotEmpty(); + assertThat(article.getContents()).isNotNull(); + assertThat(article.getAuthor()).isNotNull().isNotEmpty(); + assertThat(article.getCompany()).isNotNull(); + assertThat(article.getCompany().getId()).isNotNull(); + assertThat(article.getCompany().getName()).isNotNull().isNotEmpty(); + assertThat(article.getCompany().getCareerUrl()).isNotNull(); + assertThat(article.getCompany().getOfficialImageUrl()).isNotNull(); + assertThat(article.getRegDate()).isNotNull(); + assertThat(article.getTechArticleUrl()).isNotNull().isNotEmpty(); + assertThat(article.getThumbnailUrl()).isNotNull(); + assertThat(article.getViewTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getRecommendTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getCommentTotalCount()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getPopularScore()).isNotNull().isGreaterThanOrEqualTo(0L); + assertThat(article.getIsLogoImage()).isNotNull(); + assertThat(article.getIsBookmarked()).isNotNull(); + assertThat(article.getCompany().getId()).isEqualTo(testCompany.getId()); + }) + .extracting(TechArticleMainResponse::getRegDate) + .isSortedAccordingTo(Comparator.reverseOrder()); + } + private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, String socialType, String role) { return SocialMemberDto.builder() @@ -507,4 +908,30 @@ private Bookmark createBookmark(Member member, TechArticle techArticle, boolean .status(status) .build(); } + + private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, + String careerUrl) { + return Company.builder() + .name(new CompanyName(companyName)) + .officialImageUrl(new Url(officialImageUrl)) + .careerUrl(new Url(careerUrl)) + .officialUrl(new Url(officialUrl)) + .build(); + } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now().minusDays(i)) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(10L *i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java index f47eb2c2..9b66f318 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/MemberTechCommentServiceTest.java @@ -51,6 +51,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -128,18 +130,16 @@ void registerTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when TechCommentResponse techCommentResponse = memberTechCommentService.registerMainTechComment( - id, registerCommentDto, authentication); + techArticleId, registerCommentDto, authentication); em.flush(); // then @@ -155,7 +155,7 @@ void registerTechComment() { () -> assertThat(findTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), () -> assertThat(findTechComment.getReplyTotalCount().getCount()).isEqualTo(0L), () -> assertThat(findTechComment.getCreatedBy().getId()).isEqualTo(member.getId()), - () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(id), + () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(techArticleId), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo(2L) ); @@ -169,11 +169,9 @@ void registerTechCommentNotFoundTechArticleException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId() + 1; SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); @@ -190,7 +188,7 @@ void registerTechCommentNotFoundTechArticleException() { // when // then assertThatThrownBy( - () -> memberTechCommentService.registerMainTechComment(id, registerCommentDto, authentication)) + () -> memberTechCommentService.registerMainTechComment(techArticleId, registerCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @@ -234,8 +232,7 @@ void modifyTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -313,9 +310,7 @@ void modifyTechCommentNotFoundTechArticleCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -349,8 +344,7 @@ void modifyTechCommentAlreadyDeletedException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -390,8 +384,7 @@ void deleteTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -436,8 +429,7 @@ void deleteTechCommentAlreadyDeletedException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -475,8 +467,7 @@ void deleteTechCommentNotFoundException() { userPrincipal.getSocialType().name())); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -510,8 +501,7 @@ void deleteTechCommentAdmin() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -560,8 +550,7 @@ void deleteTechCommentNotByMemberException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -613,8 +602,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -673,9 +661,7 @@ void registerRepliedTechCommentToRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -717,7 +703,7 @@ void registerRepliedTechCommentToRepliedTechComment() { 1L), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( - 3L) + 2L) ); } @@ -739,9 +725,7 @@ void registerRepliedTechCommentNotFoundTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -778,9 +762,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -850,9 +832,7 @@ void getTechCommentsSortByOLDEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1147,9 +1127,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1319,9 +1297,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1616,9 +1592,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1767,9 +1741,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1920,9 +1892,7 @@ void recommendTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1958,9 +1928,7 @@ void recommendTechCommentCancel() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1999,9 +1967,7 @@ void recommendTechCommentNotFoundTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2034,9 +2000,7 @@ void recommendTechCommentDeletedTechCommentException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2125,10 +2089,9 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // 댓글 생성 TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member1, @@ -2275,10 +2238,9 @@ void findTechBestCommentsExcludeLessThanOneRecommend() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); // 댓글 생성 TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member1, @@ -2371,4 +2333,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) ); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java index ee377deb..e0490c80 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategyTest.java @@ -7,18 +7,20 @@ import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleService; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticsearchSupportTest; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.annotation.Transactional; -class TechArticleServiceStrategyTest extends ElasticsearchSupportTest { - +@SpringBootTest +@Transactional +class TechArticleServiceStrategyTest { @Autowired TechArticleServiceStrategy techArticleServiceStrategy; @Mock diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java index 45f05d0d..7245680d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -4,7 +4,10 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechKeywordRepository; import com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword.TechKeywordService; import com.dreamypatisiel.devdevdev.global.utils.HangulUtils; -import com.dreamypatisiel.devdevdev.test.MySQLTestContainer; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,9 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.util.ArrayList; import java.util.List; @@ -26,7 +27,8 @@ @SpringBootTest @Transactional -class TechKeywordServiceTest extends MySQLTestContainer { +@Testcontainers +class TechKeywordServiceTest { @Autowired EntityManager em; @@ -40,6 +42,18 @@ class TechKeywordServiceTest extends MySQLTestContainer { @Autowired DataSource dataSource; + @Container + @ServiceConnection + static MySQLContainer mysql = new MySQLContainer<>("mysql:8.0") + .withDatabaseName("devdevdev_test") + .withUsername("test") + .withPassword("test") + .withCommand( + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_general_ci", + "--ngram_token_size=1" + ); + private static boolean indexesCreated = false; @BeforeTransaction @@ -66,24 +80,36 @@ public void initIndexes() throws SQLException { * JDBC를 사용하여 MySQL fulltext 인덱스를 생성 */ private void createFulltextIndexesWithJDBC() throws SQLException { - Connection connection = dataSource.getConnection(); - Statement statement = connection.createStatement(); - - connection.setAutoCommit(false); // 트랜잭션 시작 - + Connection connection = null; try { - // 기존 인덱스가 있다면 삭제 - statement.executeUpdate("DROP INDEX idx__ft__jamo_key ON tech_keyword"); - statement.executeUpdate("DROP INDEX idx__ft__chosung_key ON tech_keyword"); - } catch (Exception e) { - System.out.println("인덱스 없음 (정상): " + e.getMessage()); + // 현재 테스트 클래스의 컨테이너에 직접 연결 + connection = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword() + ); + connection.setAutoCommit(false); // 트랜잭션 시작 + + try (Statement statement = connection.createStatement()) { + try { + // 기존 인덱스가 있다면 삭제 + statement.executeUpdate("DROP INDEX idx__ft__jamo_key ON tech_keyword"); + statement.executeUpdate("DROP INDEX idx__ft__chosung_key ON tech_keyword"); + } catch (Exception e) { + System.out.println("인덱스 없음 (정상): " + e.getMessage()); + } + + // fulltext 인덱스 생성 + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__jamo_key ON tech_keyword (jamo_key) WITH PARSER ngram"); + statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__chosung_key ON tech_keyword (chosung_key) WITH PARSER ngram"); + + connection.commit(); // 트랜잭션 커밋 + } + } finally { + if (connection != null && !connection.isClosed()) { + connection.close(); + } } - - // fulltext 인덱스 생성 - statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__jamo_key ON tech_keyword (jamo_key) WITH PARSER ngram"); - statement.executeUpdate("CREATE FULLTEXT INDEX idx__ft__chosung_key ON tech_keyword (chosung_key) WITH PARSER ngram"); - - connection.commit(); // 트랜잭션 커밋 } @Test diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java index c345159d..1620b719 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -48,6 +48,8 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import jakarta.persistence.EntityManager; + +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.assertj.core.groups.Tuple; @@ -118,8 +120,7 @@ void getTechCommentsSortByOLDEST() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -433,8 +434,7 @@ void getTechCommentsSortByLATEST() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -608,9 +608,7 @@ void getTechCommentsSortByMostCommented() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -924,9 +922,7 @@ void getTechCommentsSortByMostRecommended() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1082,8 +1078,7 @@ void getTechCommentsByCursor() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1273,8 +1268,7 @@ void findTechBestComments() { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1422,8 +1416,7 @@ void registerTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 등록 요청 생성 @@ -1524,8 +1517,7 @@ void registerRepliedTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1589,8 +1581,7 @@ void registerRepliedTechCommentToRepliedTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(2L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1633,7 +1624,7 @@ void registerRepliedTechCommentToRepliedTechComment() { 1L), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( - 3L) + 2L) ); } @@ -1659,8 +1650,7 @@ void registerRepliedTechCommentNotFoundTechCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1704,8 +1694,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1777,8 +1766,7 @@ void modifyTechComment() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1857,8 +1845,7 @@ void modifyTechCommentNotFoundTechArticleCommentException() { companyRepository.save(company); // 기술블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1889,8 +1876,7 @@ void modifyTechCommentAlreadyDeletedException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1932,8 +1918,7 @@ void deleteTechComment() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -1977,8 +1962,7 @@ void deleteTechCommentAlreadyDeletedException() { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2014,8 +1998,7 @@ void deleteTechCommentNotFoundException() { Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -2045,4 +2028,20 @@ void deleteTechCommentIllegalStateException() { .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java index 5532be63..af162730 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/config/ContainerExtension.java @@ -1,43 +1,43 @@ -package com.dreamypatisiel.devdevdev.elastic.config; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.images.builder.ImageFromDockerfile; - -@Disabled -public class ContainerExtension implements BeforeAllCallback, AfterAllCallback { - - static GenericContainer esContainer; - private static boolean initialized = false; - - @Override - public void beforeAll(ExtensionContext extensionContext) throws Exception { - if (!initialized) { - esContainer = new GenericContainer<>( - new ImageFromDockerfile() - .withDockerfileFromBuilder(builder -> builder - .from("docker.elastic.co/elasticsearch/elasticsearch:7.17.10") - .run("bin/elasticsearch-plugin install analysis-nori") - .build())) - .withEnv("discovery.type", "single-node") - .withEnv("http.host", "0.0.0.0") - .withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY) - .withExposedPorts(9200) - .withReuse(true); - - esContainer.start(); - System.setProperty("spring.elasticsearch.rest.uris", esContainer.getHost()); - String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200)); - - initialized = true; - } - } - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { -// esContainer.stop(); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.config; +// +//import org.junit.jupiter.api.Disabled; +//import org.junit.jupiter.api.extension.AfterAllCallback; +//import org.junit.jupiter.api.extension.BeforeAllCallback; +//import org.junit.jupiter.api.extension.ExtensionContext; +//import org.testcontainers.containers.BindMode; +//import org.testcontainers.containers.GenericContainer; +//import org.testcontainers.images.builder.ImageFromDockerfile; +// +//@Disabled +//public class ContainerExtension implements BeforeAllCallback, AfterAllCallback { +// +// static GenericContainer esContainer; +// private static boolean initialized = false; +// +// @Override +// public void beforeAll(ExtensionContext extensionContext) throws Exception { +// if (!initialized) { +// esContainer = new GenericContainer<>( +// new ImageFromDockerfile() +// .withDockerfileFromBuilder(builder -> builder +// .from("docker.elastic.co/elasticsearch/elasticsearch:7.17.10") +// .run("bin/elasticsearch-plugin install analysis-nori") +// .build())) +// .withEnv("discovery.type", "single-node") +// .withEnv("http.host", "0.0.0.0") +// .withFileSystemBind("./dict", "/usr/share/elasticsearch/config/dict", BindMode.READ_ONLY) +// .withExposedPorts(9200) +// .withReuse(true); +// +// esContainer.start(); +// System.setProperty("spring.elasticsearch.rest.uris", esContainer.getHost()); +// String host = String.format("http://%s:%s", esContainer.getContainerIpAddress(), esContainer.getMappedPort(9200)); +// +// initialized = true; +// } +// } +// @Override +// public void afterAll(ExtensionContext extensionContext) throws Exception { +//// esContainer.stop(); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java index 03b9e4f0..bbdbe9f6 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticKeywordServiceTest.java @@ -1,165 +1,165 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; - -@Disabled -@SpringBootTest -class ElasticKeywordServiceTest { - - @Autowired - ElasticKeywordService elasticKeywordService; - @Autowired - ElasticKeywordRepository elasticKeywordRepository; - - @AfterEach - void afterEach() { - elasticKeywordRepository.deleteAll(); - } - - @Test - @DisplayName("검색어와 prefix가 일치하는 키워드를 조회한다.") - void autocompleteKeyword() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "자바"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("자바", "자바스크립트"); - } - - @ParameterizedTest - @ValueSource(strings = {"ㅈ", "자", "잡", "ㅈㅏ", "ㅈㅏㅂ", "ㅈㅏㅂㅏ"}) - @DisplayName("한글 검색어의 경우 자음, 모음을 분리하여 검색할 수 있다.") - void autocompleteKoreanKeywordBySeparatingConsonantsAndVowels(String prefix) throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("자바", "자바스크립트"); - } - - @Test - @DisplayName("한글 검색어의 경우 초성검색을 할 수 있다.") - void autocompleteKoreanKeywordByChosung() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "ㅅㅍㄹ"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("스프링", "스프링부트"); - } - - - @Test - @DisplayName("영어 대소문자 상관없이 키워드를 조회한다.") - void autocompleteKeywordRegardlessOfAlphaCase() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("JAVA"); - ElasticKeyword keyword2 = ElasticKeyword.create("JavaScript"); - ElasticKeyword keyword3 = ElasticKeyword.create("Spring"); - ElasticKeyword keyword4 = ElasticKeyword.create("SpringBoot"); - ElasticKeyword keyword5 = ElasticKeyword.create("ChatGPT"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "spr"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords) - .hasSize(2) - .contains("Spring", "SpringBoot"); - } - - @Test - @DisplayName("일치하는 키워드가 없을 경우 빈 리스트를 반환한다.") - void autocompleteKeywordNotFound() throws IOException { - // given - ElasticKeyword keyword1 = ElasticKeyword.create("자바"); - ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); - ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); - ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); - ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); - List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "엘라스틱서치"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords).isEmpty(); - } - - @ParameterizedTest - @ValueSource(ints = {19, 20, 21, 22}) - @DisplayName("검색어와 prefix가 일치하는 키워드를 최대 20개 조회한다.") - void autocompleteKeywordWithMax20(int n) throws IOException { - // given - List elasticKeywords = new ArrayList<>(); - for (int i = 0; i < n; i++) { - elasticKeywords.add(ElasticKeyword.create("키워드" + i)); - } - elasticKeywordRepository.saveAll(elasticKeywords); - - String prefix = "키워드"; - - // when - List keywords = elasticKeywordService.autocompleteKeyword(prefix); - - // then - assertThat(keywords).hasSizeLessThanOrEqualTo(20); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static org.assertj.core.api.Assertions.assertThat; +// +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; +//import java.io.IOException; +//import java.util.ArrayList; +//import java.util.List; +//import org.junit.jupiter.api.AfterEach; +//import org.junit.jupiter.api.Disabled; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.params.ParameterizedTest; +//import org.junit.jupiter.params.provider.ValueSource; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +// +//@Disabled +//@SpringBootTest +//class ElasticKeywordServiceTest { +// +// @Autowired +// ElasticKeywordService elasticKeywordService; +// @Autowired +// ElasticKeywordRepository elasticKeywordRepository; +// +// @AfterEach +// void afterEach() { +// elasticKeywordRepository.deleteAll(); +// } +// +// @Test +// @DisplayName("검색어와 prefix가 일치하는 키워드를 조회한다.") +// void autocompleteKeyword() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "자바"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("자바", "자바스크립트"); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"ㅈ", "자", "잡", "ㅈㅏ", "ㅈㅏㅂ", "ㅈㅏㅂㅏ"}) +// @DisplayName("한글 검색어의 경우 자음, 모음을 분리하여 검색할 수 있다.") +// void autocompleteKoreanKeywordBySeparatingConsonantsAndVowels(String prefix) throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("자바", "자바스크립트"); +// } +// +// @Test +// @DisplayName("한글 검색어의 경우 초성검색을 할 수 있다.") +// void autocompleteKoreanKeywordByChosung() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "ㅅㅍㄹ"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("스프링", "스프링부트"); +// } +// +// +// @Test +// @DisplayName("영어 대소문자 상관없이 키워드를 조회한다.") +// void autocompleteKeywordRegardlessOfAlphaCase() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("JAVA"); +// ElasticKeyword keyword2 = ElasticKeyword.create("JavaScript"); +// ElasticKeyword keyword3 = ElasticKeyword.create("Spring"); +// ElasticKeyword keyword4 = ElasticKeyword.create("SpringBoot"); +// ElasticKeyword keyword5 = ElasticKeyword.create("ChatGPT"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "spr"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords) +// .hasSize(2) +// .contains("Spring", "SpringBoot"); +// } +// +// @Test +// @DisplayName("일치하는 키워드가 없을 경우 빈 리스트를 반환한다.") +// void autocompleteKeywordNotFound() throws IOException { +// // given +// ElasticKeyword keyword1 = ElasticKeyword.create("자바"); +// ElasticKeyword keyword2 = ElasticKeyword.create("자바스크립트"); +// ElasticKeyword keyword3 = ElasticKeyword.create("스프링"); +// ElasticKeyword keyword4 = ElasticKeyword.create("스프링부트"); +// ElasticKeyword keyword5 = ElasticKeyword.create("챗지피티"); +// List elasticKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5); +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "엘라스틱서치"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords).isEmpty(); +// } +// +// @ParameterizedTest +// @ValueSource(ints = {19, 20, 21, 22}) +// @DisplayName("검색어와 prefix가 일치하는 키워드를 최대 20개 조회한다.") +// void autocompleteKeywordWithMax20(int n) throws IOException { +// // given +// List elasticKeywords = new ArrayList<>(); +// for (int i = 0; i < n; i++) { +// elasticKeywords.add(ElasticKeyword.create("키워드" + i)); +// } +// elasticKeywordRepository.saveAll(elasticKeywords); +// +// String prefix = "키워드"; +// +// // when +// List keywords = elasticKeywordService.autocompleteKeyword(prefix); +// +// // then +// assertThat(keywords).hasSizeLessThanOrEqualTo(20); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java index f0fa2586..3f4cbfc9 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticTechArticleServiceTest.java @@ -1,832 +1,832 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; -import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; -import org.springframework.data.elasticsearch.core.SearchHit; -import org.springframework.data.elasticsearch.core.SearchHits; - -public class ElasticTechArticleServiceTest extends ElasticsearchSupportTest { - - @Autowired - ElasticTechArticleService elasticTechArticleService; - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 조회한다. (기본정렬은 최신순)") - void getTechArticles() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, null, - null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") - void getTechArticlesOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") - void getTechArticlesOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") - void getTechArticlesOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") - void getTechArticlesOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") - void getTechArticlesOrderByHIGHEST_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회할 때, " + - "존재하지 않는 엘라스틱 기술블로그 ID라면 예외가 발생한다.") - void getTechArticlesWithCursorException() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, "dontExistElasticId", null, null, null, null)) - .isInstanceOf(NotFoundException.class) - .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") - void getTechArticlesWithCursorOrderByLATEST() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.LATEST, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.LATEST, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByMOST_VIEWED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByMOST_COMMENTED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithCursorOrderByPOPULAR_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.POPULAR, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") - void getTechArticlesWithCursorOrderByHIGHEST_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.HIGHEST_SCORE, null, null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색한다. (기본정렬은 정확도순)") - void getTechArticlesWithKeyword() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, null, - keyword, null, null); - List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticlesScores) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByHIGHEST_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticlesScores) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithKeywordOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, keyword, null, null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.HIGHEST_SCORE, keyword, null, cursorScore); - List elasticTechArticleScores2 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticleScores2) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + - "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") - void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.HIGHEST_SCORE, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, cursor.getId(), TechArticleSort.HIGHEST_SCORE, - keyword, null, null)) - .isInstanceOf(ElasticTechArticleException.class) - .hasMessage(NOT_FOUND_CURSOR_SCORE_MESSAGE); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByLATEST() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.LATEST, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.LATEST, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()) - .allMatch(date -> !date.isAfter(cursor.getRegDate())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByMOST_VIEWED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_VIEWED, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_VIEWED, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByMOST_COMMENTED() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.MOST_COMMENTED, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.MOST_COMMENTED, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") - void getTechArticlesWithKeywordWithCursorOrderByPOPULAR_SCORE() { - // given - Pageable prevPageable = PageRequest.of(0, 1); - Pageable pageable = PageRequest.of(0, 10); - String keyword = "타이틀"; - - SearchHits techArticles1 = elasticTechArticleService.getTechArticles( - prevPageable, null, - TechArticleSort.POPULAR, keyword, null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) - .toList(); - ElasticTechArticle cursor = elasticTechArticles1.getLast(); - Float cursorScore = elasticTechArticleScores1.getLast(); - - // when - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - cursor.getId(), TechArticleSort.POPULAR, keyword, null, cursorScore); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - assertThat(techArticles2.getSearchHits().size()) - .isEqualTo(pageable.getPageSize()); - - assertThat(elasticTechArticles2) - .hasSize(pageable.getPageSize()) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 최신순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByLATEST() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.LATEST, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getRegDate) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 조회수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByMOST_VIEWED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_VIEWED, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getViewTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 댓글수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByMOST_COMMENTED() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.MOST_COMMENTED, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getCommentTotalCount) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @Test - @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 인기점수 내림차순으로 조회한다.") - void getTechArticlesFilterByCompanyOrderByPOPULAR_SCORE() { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, - null, - TechArticleSort.POPULAR, null, company.getId(), null); - List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) - .toList(); - - // then - assertThat(elasticTechArticles) - .hasSize(pageable.getPageSize()) - .allSatisfy(article -> { - assertThat(article.getCompanyId()).isEqualTo(company.getId()); - }) - .extracting(ElasticTechArticle::getPopularScore) - .isSortedAccordingTo(Comparator.reverseOrder()); - } - - @ParameterizedTest - @ValueSource(strings = {"!", "^", "(", ")", "-", "+", "/", "[", "]", "{", "}", ":"}) - @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 있다면 예외가 발생한다.") - void getTechArticlesWithSpecialSymbolsException(String keyword) { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatThrownBy( - () -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) - .isInstanceOf(UncategorizedElasticsearchException.class); - } - - @ParameterizedTest - @ValueSource(strings = {"@", "=", "#", "$", "%", "&", "*", "_", "=", "<", ">", ",", ".", "?", ";", "", "'"}) - @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 아닌 문자들이 있다면 예외가 발생하지 않는다.") - void getTechArticlesWithSpecialSymbolsDoesNotThrowException(String keyword) { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - assertThatCode(() -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) - .doesNotThrowAnyException(); - } - - @Test - @DisplayName("엘라스틱서치 키워드 검색시 기본 쿼리 옵션은 AND로 동작한다.") - void test() { - // given - List elasticTechArticles = new ArrayList<>(); - elasticTechArticles.add(ElasticTechArticle.builder().title("자바").build()); - elasticTechArticles.add(ElasticTechArticle.builder().title("스프링").build()); - elasticTechArticles.add(ElasticTechArticle.builder().title("자바 스프링").build()); - elasticTechArticleRepository.saveAll(elasticTechArticles); - - Pageable pageable = PageRequest.of(0, 10); - - // when - SearchHits techArticles1 = elasticTechArticleService.getTechArticles(pageable, - null, null, "자바", null, null); - List elasticTechArticles1 = techArticles1.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, - null, null, "스프링", null, null); - List elasticTechArticles2 = techArticles2.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - SearchHits techArticles3 = elasticTechArticleService.getTechArticles(pageable, - null, null, "자바 스프링", null, null); - List elasticTechArticles3 = techArticles3.getSearchHits().stream() - .map(SearchHit::getContent) - .toList(); - - // then - // "자바" 키워드 검색 - assertThat(elasticTechArticles1) - .hasSize(2) - .allSatisfy(article -> - assertThat((article.getContents() != null && article.getContents().contains("자바")) || - (article.getTitle() != null && article.getTitle().contains("자바"))).isTrue()); - // "스프링" 키워드 검색 - assertThat(elasticTechArticles2) - .hasSize(2) - .allSatisfy(article -> - assertThat((article.getContents() != null && article.getContents().contains("스프링")) || - (article.getTitle() != null && article.getTitle().contains("스프링"))).isTrue()); - // "자바 스프링" 키워드 검색 - assertThat(elasticTechArticles3) - .hasSize(1) - .allSatisfy(article -> - assertThat((article.getContents() != null - && article.getContents().contains("자바") && article.getContents().contains("스프링")) - || (article.getTitle() != null - && article.getTitle().contains("자바") && article.getTitle().contains("스프링"))).isTrue()); - } -} \ No newline at end of file +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +//import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; +//import static org.assertj.core.api.Assertions.assertThat; +//import static org.assertj.core.api.Assertions.assertThatCode; +//import static org.assertj.core.api.Assertions.assertThatThrownBy; +// +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import com.dreamypatisiel.devdevdev.exception.ElasticTechArticleException; +//import com.dreamypatisiel.devdevdev.exception.NotFoundException; +//import java.util.ArrayList; +//import java.util.Comparator; +//import java.util.List; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.params.ParameterizedTest; +//import org.junit.jupiter.params.provider.ValueSource; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.data.domain.Pageable; +//import org.springframework.data.elasticsearch.UncategorizedElasticsearchException; +//import org.springframework.data.elasticsearch.core.SearchHit; +//import org.springframework.data.elasticsearch.core.SearchHits; +// +//public class ElasticTechArticleServiceTest extends ElasticsearchSupportTest { +// +// @Autowired +// ElasticTechArticleService elasticTechArticleService; +// @Autowired +// TechArticleRepository techArticleRepository; +// @Autowired +// ElasticTechArticleRepository elasticTechArticleRepository; +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 조회한다. (기본정렬은 최신순)") +// void getTechArticles() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, null, +// null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") +// void getTechArticlesOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") +// void getTechArticlesOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") +// void getTechArticlesOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") +// void getTechArticlesOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") +// void getTechArticlesOrderByHIGHEST_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회할 때, " + +// "존재하지 않는 엘라스틱 기술블로그 ID라면 예외가 발생한다.") +// void getTechArticlesWithCursorException() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, "dontExistElasticId", null, null, null, null)) +// .isInstanceOf(NotFoundException.class) +// .hasMessage(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 최신순으로 조회한다.") +// void getTechArticlesWithCursorOrderByLATEST() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.LATEST, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 조회수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByMOST_VIEWED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_VIEWED, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(viewCount -> viewCount <= cursor.getViewTotalCount()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 댓글수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByMOST_COMMENTED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_COMMENTED, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(commentCount -> commentCount <= cursor.getCommentTotalCount()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithCursorOrderByPOPULAR_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.POPULAR, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(popularScore -> popularScore <= cursor.getPopularScore()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그 메인을 정확도순으로 조회하면 최신순으로 조회된다.") +// void getTechArticlesWithCursorOrderByHIGHEST_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.HIGHEST_SCORE, null, null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색한다. (기본정렬은 정확도순)") +// void getTechArticlesWithKeyword() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, null, +// keyword, null, null); +// List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticlesScores) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByHIGHEST_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticlesScores = techArticles.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticlesScores) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, keyword, null, null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 정확도 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.HIGHEST_SCORE, keyword, null, cursorScore); +// List elasticTechArticleScores2 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticleScores2) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + +// "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") +// void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.HIGHEST_SCORE, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, cursor.getId(), TechArticleSort.HIGHEST_SCORE, +// keyword, null, null)) +// .isInstanceOf(ElasticTechArticleException.class) +// .hasMessage(NOT_FOUND_CURSOR_SCORE_MESSAGE); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 최신순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByLATEST() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.LATEST, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.LATEST, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()) +// .allMatch(date -> !date.isAfter(cursor.getRegDate())); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 조회순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByMOST_VIEWED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_VIEWED, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_VIEWED, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 댓글순 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByMOST_COMMENTED() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.MOST_COMMENTED, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.MOST_COMMENTED, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때 인기점수 내림차순으로 조회한다.") +// void getTechArticlesWithKeywordWithCursorOrderByPOPULAR_SCORE() { +// // given +// Pageable prevPageable = PageRequest.of(0, 1); +// Pageable pageable = PageRequest.of(0, 10); +// String keyword = "타이틀"; +// +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles( +// prevPageable, null, +// TechArticleSort.POPULAR, keyword, null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// List elasticTechArticleScores1 = techArticles1.getSearchHits().stream().map(SearchHit::getScore) +// .toList(); +// ElasticTechArticle cursor = elasticTechArticles1.getLast(); +// Float cursorScore = elasticTechArticleScores1.getLast(); +// +// // when +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// cursor.getId(), TechArticleSort.POPULAR, keyword, null, cursorScore); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(techArticles2.getSearchHits().size()) +// .isEqualTo(pageable.getPageSize()); +// +// assertThat(elasticTechArticles2) +// .hasSize(pageable.getPageSize()) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 최신순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByLATEST() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.LATEST, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getRegDate) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 조회수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByMOST_VIEWED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_VIEWED, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getViewTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 댓글수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByMOST_COMMENTED() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.MOST_COMMENTED, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getCommentTotalCount) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @Test +// @DisplayName("엘라스틱서치 기술블로그 메인을 회사로 필터링한 후 인기점수 내림차순으로 조회한다.") +// void getTechArticlesFilterByCompanyOrderByPOPULAR_SCORE() { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles = elasticTechArticleService.getTechArticles(pageable, +// null, +// TechArticleSort.POPULAR, null, company.getId(), null); +// List elasticTechArticles = techArticles.getSearchHits().stream().map(SearchHit::getContent) +// .toList(); +// +// // then +// assertThat(elasticTechArticles) +// .hasSize(pageable.getPageSize()) +// .allSatisfy(article -> { +// assertThat(article.getCompanyId()).isEqualTo(company.getId()); +// }) +// .extracting(ElasticTechArticle::getPopularScore) +// .isSortedAccordingTo(Comparator.reverseOrder()); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"!", "^", "(", ")", "-", "+", "/", "[", "]", "{", "}", ":"}) +// @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 있다면 예외가 발생한다.") +// void getTechArticlesWithSpecialSymbolsException(String keyword) { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatThrownBy( +// () -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) +// .isInstanceOf(UncategorizedElasticsearchException.class); +// } +// +// @ParameterizedTest +// @ValueSource(strings = {"@", "=", "#", "$", "%", "&", "*", "_", "=", "<", ">", ",", ".", "?", ";", "", "'"}) +// @DisplayName("엘라스틱서치로 키워드 검색을 할 때, 키워드에 특정 특수문자가 아닌 문자들이 있다면 예외가 발생하지 않는다.") +// void getTechArticlesWithSpecialSymbolsDoesNotThrowException(String keyword) { +// // given +// Pageable pageable = PageRequest.of(0, 10); +// +// // when // then +// assertThatCode(() -> elasticTechArticleService.getTechArticles(pageable, null, null, keyword, null, null)) +// .doesNotThrowAnyException(); +// } +// +// @Test +// @DisplayName("엘라스틱서치 키워드 검색시 기본 쿼리 옵션은 AND로 동작한다.") +// void test() { +// // given +// List elasticTechArticles = new ArrayList<>(); +// elasticTechArticles.add(ElasticTechArticle.builder().title("자바").build()); +// elasticTechArticles.add(ElasticTechArticle.builder().title("스프링").build()); +// elasticTechArticles.add(ElasticTechArticle.builder().title("자바 스프링").build()); +// elasticTechArticleRepository.saveAll(elasticTechArticles); +// +// Pageable pageable = PageRequest.of(0, 10); +// +// // when +// SearchHits techArticles1 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "자바", null, null); +// List elasticTechArticles1 = techArticles1.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// SearchHits techArticles2 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "스프링", null, null); +// List elasticTechArticles2 = techArticles2.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// SearchHits techArticles3 = elasticTechArticleService.getTechArticles(pageable, +// null, null, "자바 스프링", null, null); +// List elasticTechArticles3 = techArticles3.getSearchHits().stream() +// .map(SearchHit::getContent) +// .toList(); +// +// // then +// // "자바" 키워드 검색 +// assertThat(elasticTechArticles1) +// .hasSize(2) +// .allSatisfy(article -> +// assertThat((article.getContents() != null && article.getContents().contains("자바")) || +// (article.getTitle() != null && article.getTitle().contains("자바"))).isTrue()); +// // "스프링" 키워드 검색 +// assertThat(elasticTechArticles2) +// .hasSize(2) +// .allSatisfy(article -> +// assertThat((article.getContents() != null && article.getContents().contains("스프링")) || +// (article.getTitle() != null && article.getTitle().contains("스프링"))).isTrue()); +// // "자바 스프링" 키워드 검색 +// assertThat(elasticTechArticles3) +// .hasSize(1) +// .allSatisfy(article -> +// assertThat((article.getContents() != null +// && article.getContents().contains("자바") && article.getContents().contains("스프링")) +// || (article.getTitle() != null +// && article.getTitle().contains("자바") && article.getTitle().contains("스프링"))).isTrue()); +// } +//} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java index e3554bbd..802828bb 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/elastic/domain/service/ElasticsearchSupportTest.java @@ -1,104 +1,104 @@ -package com.dreamypatisiel.devdevdev.elastic.domain.service; - -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -//@ExtendWith(ContainerExtension.class) -//@SpringBootTest(classes = {ElasticsearchTestConfig.class}) -@SpringBootTest -@Transactional -public class ElasticsearchSupportTest { - - public static final int TEST_ARTICLES_COUNT = 20; - public static TechArticle firstTechArticle; - public static Company company; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { - company = createCompany("꿈빛 파티시엘", "https://example.net/image.png", "https://example.com", - "https://example.com"); - companyRepository.save(company); - - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - List techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); - } - - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } - - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialUrl(new Url(officialUrl)) - .careerUrl(new Url(careerUrl)) - .officialImageUrl(new Url(officialImageUrl)) - .build(); - } -} +//package com.dreamypatisiel.devdevdev.elastic.domain.service; +// +//import com.dreamypatisiel.devdevdev.domain.entity.Company; +//import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +//import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; +//import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; +//import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; +//import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +//import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; +//import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +//import java.time.LocalDate; +//import java.util.ArrayList; +//import java.util.List; +//import org.junit.jupiter.api.AfterAll; +//import org.junit.jupiter.api.BeforeAll; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.context.SpringBootTest; +//import org.springframework.transaction.annotation.Transactional; +// +////@ExtendWith(ContainerExtension.class) +////@SpringBootTest(classes = {ElasticsearchTestConfig.class}) +//@SpringBootTest +//@Transactional +//public class ElasticsearchSupportTest { +// +// public static final int TEST_ARTICLES_COUNT = 20; +// public static TechArticle firstTechArticle; +// public static Company company; +// +// @BeforeAll +// static void setup(@Autowired TechArticleRepository techArticleRepository, +// @Autowired CompanyRepository companyRepository, +// @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { +// company = createCompany("꿈빛 파티시엘", "https://example.net/image.png", "https://example.com", +// "https://example.com"); +// companyRepository.save(company); +// +// // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. +// LocalDate baseDate = LocalDate.of(2024, 8, 30); +// List elasticTechArticles = new ArrayList<>(); +// for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { +// ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, +// baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", +// company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, +// (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, +// (long) (TEST_ARTICLES_COUNT - i) * 10); +// elasticTechArticles.add(elasticTechArticle); +// } +// Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( +// elasticTechArticles); +// +// // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. +// List techArticles = new ArrayList<>(); +// for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { +// TechArticle techArticle = createTechArticle(elasticTechArticle, company); +// techArticles.add(techArticle); +// } +// List savedTechArticles = techArticleRepository.saveAll(techArticles); +// firstTechArticle = savedTechArticles.getFirst(); +// } +// +// @AfterAll +// static void tearDown(@Autowired TechArticleRepository techArticleRepository, +// @Autowired ElasticTechArticleRepository elasticTechArticleRepository, +// @Autowired CompanyRepository companyRepository) { +// elasticTechArticleRepository.deleteAll(); +// techArticleRepository.deleteAllInBatch(); +// companyRepository.deleteAllInBatch(); +// } +// +// private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, +// String contents, String techArticleUrl, +// String description, String thumbnailUrl, String author, +// String company, Long companyId, +// Long viewTotalCount, Long recommendTotalCount, +// Long commentTotalCount, Long popularScore) { +// return ElasticTechArticle.builder() +// .id(id) +// .title(title) +// .regDate(regDate) +// .contents(contents) +// .techArticleUrl(techArticleUrl) +// .description(description) +// .thumbnailUrl(thumbnailUrl) +// .author(author) +// .company(company) +// .companyId(companyId) +// .viewTotalCount(viewTotalCount) +// .recommendTotalCount(recommendTotalCount) +// .commentTotalCount(commentTotalCount) +// .popularScore(popularScore) +// .build(); +// } +// +// private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, +// String careerUrl) { +// return Company.builder() +// .name(new CompanyName(companyName)) +// .officialUrl(new Url(officialUrl)) +// .careerUrl(new Url(careerUrl)) +// .officialImageUrl(new Url(officialImageUrl)) +// .build(); +// } +//} diff --git a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java index 6884b394..8e981639 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.test; +import org.junit.jupiter.api.AfterAll; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; @@ -7,10 +8,13 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + /** * MySQL 테스트컨테이너를 제공하는 공통 클래스 - * 1. 테스트 클래스에서 이 클래스를 상속받거나 - * 2. @ExtendWith(MySQLTestContainer.class) 어노테이션을 사용 + * 각 테스트 클래스가 독립적인 컨테이너를 사용하도록 변경(현재 사용X) */ @Testcontainers public abstract class MySQLTestContainer { @@ -29,11 +33,67 @@ public abstract class MySQLTestContainer { @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", mysql::getJdbcUrl); + // 컨테이너 시작 확인 + if (!mysql.isRunning()) { + mysql.start(); + } + + // 컨테이너 준비 대기 + waitForContainer(); + + String jdbcUrl = mysql.getJdbcUrl(); + System.out.println("MySQL Container JDBC URL: " + jdbcUrl); + + registry.add("spring.datasource.url", () -> jdbcUrl); registry.add("spring.datasource.username", mysql::getUsername); registry.add("spring.datasource.password", mysql::getPassword); registry.add("spring.jpa.database-platform", () -> "org.hibernate.dialect.MySQLDialect"); registry.add("spring.jpa.hibernate.ddl-auto", () -> "create-drop"); registry.add("spring.jpa.show-sql", () -> "true"); + + // HikariCP 완전 비활성화 + registry.add("spring.datasource.type", () -> "org.springframework.jdbc.datasource.SimpleDriverDataSource"); + + // 로깅 레벨 설정 + registry.add("logging.level.com.zaxxer.hikari", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool.PoolBase", () -> "OFF"); + registry.add("logging.level.com.zaxxer.hikari.pool.ProxyLeakTask", () -> "OFF"); + } + + private static void waitForContainer() { + int maxRetries = 15; + int retryCount = 0; + + while (retryCount < maxRetries) { + try { + if (mysql.isRunning()) { + // 실제 커넥션 테스트 + try (Connection conn = DriverManager.getConnection( + mysql.getJdbcUrl(), + mysql.getUsername(), + mysql.getPassword())) { + System.out.println("MySQL Container 연결 성공: " + mysql.getJdbcUrl()); + return; + } catch (SQLException e) { + System.out.println("커넥션 대기 중... (" + retryCount + "/" + maxRetries + ")"); + } + } + Thread.sleep(1000); + retryCount++; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + throw new RuntimeException("MySQL 컴테이너 연결 실패"); + } + + @AfterAll + static void tearDownContainer() { + // 컨테이너 종료 + if (mysql != null && mysql.isRunning()) { + mysql.stop(); + } } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java index 8073c50e..cc884906 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java @@ -16,8 +16,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; @@ -72,7 +70,7 @@ class MyPageControllerTest extends SupportControllerTest { private static final int TEST_ARTICLES_COUNT = 20; - private static List techArticles; + private static final List techArticles = new ArrayList<>(); @Autowired ApplicationContext applicationContext; @@ -109,40 +107,21 @@ class MyPageControllerTest extends SupportControllerTest { @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } techArticleRepository.saveAll(techArticles); } @AfterAll - static void tearDown(@Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired TechArticleRepository techArticleRepository, + static void tearDown(@Autowired TechArticleRepository techArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -181,7 +160,6 @@ void getBookmarkedTechArticles() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -853,30 +831,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -898,4 +852,20 @@ private static Company createCompany(String companyName, String officialUrl, Str .industry(industry) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java index 7759f1c8..c540a873 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/notification/NotificationControllerTest.java @@ -172,7 +172,7 @@ void getNotifications() throws Exception { // given PageRequest pageable = PageRequest.of(0, 1); TechArticleMainResponse techArticleMainResponse = createTechArticleMainResponse( - 1L, "elasticId", "http://thumbnailUrl.com", false, + 1L, "http://thumbnailUrl.com", false, "http://techArticleUrl.com", "기술블로그 타이틀", "기술블로그 내용", 1L, "기업명", "http://careerUrl.com","http://officialImage.com", LocalDate.now(), "작성자", 0L, 0L, 0L, false, null @@ -200,7 +200,6 @@ void getNotifications() throws Exception { .andExpect(jsonPath("$.data.content.[0].isRead").isBoolean()) .andExpect(jsonPath("$.data.content.[0].techArticle").isNotEmpty()) .andExpect(jsonPath("$.data.content.[0].techArticle.id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].techArticle.elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].techArticle.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticle.isLogoImage").isBoolean()) .andExpect(jsonPath("$.data.content.[0].techArticle.techArticleUrl").isString()) @@ -243,14 +242,13 @@ void getNotifications() throws Exception { verify(notificationService, times(1)).getNotifications(any(), anyLong(), any()); } - private TechArticleMainResponse createTechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, long commentCount, long viewCount, Boolean isBookmarked, Float score) { return TechArticleMainResponse.builder() .id(id) - .elasticId(elasticId) .thumbnailUrl(thumbnailUrl) .isLogoImage(isLogoImage) .techArticleUrl(techArticleUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java index 8f61b87d..0381306a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentControllerTest.java @@ -41,6 +41,7 @@ import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.persistence.EntityManager; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Disabled; @@ -86,8 +87,7 @@ void registerTechCommentByAnonymous() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -113,11 +113,7 @@ void registerTechCommentByMember() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -151,8 +147,7 @@ void registerTechCommentByAnonymousMember() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -186,8 +181,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -220,8 +214,7 @@ void registerTechCommentNotFoundMemberException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId(); @@ -249,11 +242,7 @@ void registerTechCommentContentsIsNullException(String contents) throws Exceptio "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId(); @@ -286,9 +275,7 @@ void modifyTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -328,9 +315,7 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -368,9 +353,7 @@ void modifyTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -403,9 +386,7 @@ void modifyTechCommentAlreadyDeletedException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -446,9 +427,7 @@ void deleteTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -484,9 +463,7 @@ void deleteTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -513,8 +490,7 @@ void registerRepliedTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -567,8 +543,7 @@ void registerRepliedTechCommentContentsIsNullException(String contents) throws E "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -616,8 +591,7 @@ void getTechComments() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -733,9 +707,7 @@ void recommendTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -783,9 +755,7 @@ void getTechBestComments() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -871,9 +841,7 @@ void getTechBestCommentsAnonymous() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1003,4 +971,20 @@ private static TechComment createRepliedTechComment(CommentContents contents, Me .parent(parent) .build(); } + + private static TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/article.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java index 8fa978b6..f7e0f6c7 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleControllerTest.java @@ -3,8 +3,6 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_ID_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -12,116 +10,67 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.Member; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; -import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; +import com.dreamypatisiel.devdevdev.exception.MemberException; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.exception.TechArticleException; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; -import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import java.nio.charset.StandardCharsets; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; + class TechArticleControllerTest extends SupportControllerTest { - private static final int TEST_ARTICLES_COUNT = 20; - private static Company company; - private static TechArticle firstTechArticle; - private static List techArticles; - - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired - MemberRepository memberRepository; - @Autowired - BookmarkRepository bookmarkRepository; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { - company = createCompany("꿈빛 파티시엘", - "https://example.com/company.png", "https://example.com", "https://example.com"); - companyRepository.save(company); - - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); - } - - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } + @MockBean + GuestTechArticleService guestTechArticleService; + + @MockBean + MemberTechArticleService memberTechArticleService; @Test @DisplayName("익명 사용자가 기술블로그 메인을 조회한다.") void getTechArticlesByAnonymous() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, null, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.LATEST.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -132,7 +81,6 @@ void getTechArticlesByAnonymous() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -174,32 +122,30 @@ void getTechArticlesByAnonymous() throws Exception { @DisplayName("회원이 기술블로그 메인을 조회한다.") void getTechArticlesByMember() throws Exception { // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); - - List bookmarks = new ArrayList<>(); - for (TechArticle techArticle : techArticles) { - if (createRandomBoolean()) { - Bookmark bookmark = createBookmark(member, techArticle, true); - bookmarks.add(bookmark); - } - } - bookmarkRepository.saveAll(bookmarks); - Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, true, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(memberTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.LATEST.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -211,7 +157,6 @@ void getTechArticlesByMember() throws Exception { .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data.content").isArray()) .andExpect(jsonPath("$.data.content.[0].id").isNumber()) - .andExpect(jsonPath("$.data.content.[0].elasticId").isString()) .andExpect(jsonPath("$.data.content.[0].thumbnailUrl").isString()) .andExpect(jsonPath("$.data.content.[0].techArticleUrl").isString()) .andExpect(jsonPath("$.data.content.[0].title").isString()) @@ -251,42 +196,22 @@ void getTechArticlesByMember() throws Exception { } @Test - @DisplayName("기술블로그 메인을 조회할 때 존재하지 않는 엘라스틱서치 ID를 조회하면 에러가 발생한다.") - void getTechArticlesNotFoundElasticIdException() throws Exception { - // given - Pageable pageable = PageRequest.of(0, 10); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles") - .queryParam("size", String.valueOf(pageable.getPageSize())) - .queryParam("techArticleSort", TechArticleSort.LATEST.name()) - .queryParam("elasticId", "elasticId") - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + + @DisplayName("커서 방식으로 다음 페이지의 기술블로그를 검색어로 검색할 때," + "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() throws Exception { // given - Pageable prevPageable = PageRequest.of(0, 1); Pageable pageable = PageRequest.of(0, 10); - List elasticTechArticles = elasticTechArticleRepository.findAll(prevPageable).stream() - .toList(); - ElasticTechArticle cursor = elasticTechArticles.getLast(); + String techArticleId = "1"; String keyword = "타이틀"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) - .queryParam("elasticId", cursor.getId()) + .queryParam("techArticleId", techArticleId) .queryParam("keyword", keyword) .contentType(MediaType.APPLICATION_JSON) .characterEncoding(StandardCharsets.UTF_8)) @@ -304,6 +229,9 @@ void getTechArticlesSpecialSymbolException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); String keyword = "!"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles") @@ -323,8 +251,17 @@ void getTechArticlesSpecialSymbolException() throws Exception { @DisplayName("익명 사용자가 기술블로그 상세를 조회한다.") void getTechArticleByAnonymous() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; String anonymousMemberId = "GA1.1.276672604.1715872960"; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, false + ); + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -336,7 +273,6 @@ void getTechArticleByAnonymous() throws Exception { .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data").isMap()) - .andExpect(jsonPath("$.data.elasticId").isString()) .andExpect(jsonPath("$.data.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.techArticleUrl").isString()) .andExpect(jsonPath("$.data.title").isString()) @@ -358,13 +294,16 @@ void getTechArticleByAnonymous() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회한다.") void getTechArticleByMember() throws Exception { // given - Long id = firstTechArticle.getId(); - // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, true + ); + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -376,7 +315,6 @@ void getTechArticleByMember() throws Exception { .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) .andExpect(jsonPath("$.data").isNotEmpty()) .andExpect(jsonPath("$.data").isMap()) - .andExpect(jsonPath("$.data.elasticId").isString()) .andExpect(jsonPath("$.data.thumbnailUrl").isString()) .andExpect(jsonPath("$.data.techArticleUrl").isString()) .andExpect(jsonPath("$.data.title").isString()) @@ -399,7 +337,10 @@ void getTechArticleByMember() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회할 때 회원이 없으면 예외가 발생한다.") void getTechArticleNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -417,13 +358,10 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; + Long id = 999L; + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) @@ -436,61 +374,15 @@ void getTechArticleNotFoundTechArticleException() throws Exception { .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); } - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_ID_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.BAD_REQUEST.value())); - } - - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - mockMvc.perform(get("/devdevdev/api/v1/articles/{id}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) - .andExpect(jsonPath("$.message").value(NOT_FOUND_ELASTIC_TECH_ARTICLE_MESSAGE)) - .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); - } - @Test @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + BookmarkResponse response = new BookmarkResponse(id, true); + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willReturn(response); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -510,19 +402,10 @@ void updateBookmark() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateBookmarkNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -540,7 +423,10 @@ void updateBookmarkNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 북마크를 요청할 때 존재하지 않는 회원이라면 예외가 발생한다.") void updateBookmarkNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/bookmark", id) @@ -558,12 +444,11 @@ void updateBookmarkNotFoundMemberException() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청한다.") void updateRecommend() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleRecommendResponse response = new TechArticleRecommendResponse(id, true, 11L); + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willReturn(response); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -585,19 +470,10 @@ void updateRecommend() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void updateRecommendNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -615,7 +491,10 @@ void updateRecommendNotFoundTechArticleException() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청할 때 존재하지 않는 회원이라면 예외가 발생한다.") void updateRecommendNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); + Long id = 1L; + + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/recommend", id) @@ -629,73 +508,50 @@ void updateRecommendNotFoundMemberException() throws Exception { .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); } - private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, - String socialType, String role) { - return SocialMemberDto.builder() - .userId(userId) - .name(name) - .nickname(nickName) - .password(password) - .email(email) - .socialType(SocialType.valueOf(socialType)) - .role(Role.valueOf(role)) - .build(); - } - - private Bookmark createBookmark(Member member, TechArticle techArticle, boolean status) { - return Bookmark.builder() - .member(member) - .techArticle(techArticle) - .status(status) - .build(); - } - - private boolean createRandomBoolean() { - return new Random().nextBoolean(); - } - - private static LocalDate createRandomDate() { - LocalDate startDate = LocalDate.of(2024, 1, 1); - LocalDate endDate = LocalDate.of(2024, 3, 10); - - // 시작 날짜와 종료 날짜 사이의 차이 중 랜덤한 일 수 선택 - long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); - long randomDays = ThreadLocalRandom.current().nextLong(daysBetween + 1); - - return startDate.plusDays(randomDays); - } - - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, + String techArticleUrl, String title, String contents, + Long companyId, String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long recommendCount, + long commentCount, long viewCount, Boolean isBookmarked, Float score) { + return TechArticleMainResponse.builder() .id(id) + .thumbnailUrl(thumbnailUrl) + .isLogoImage(isLogoImage) + .techArticleUrl(techArticleUrl) .title(title) - .regDate(regDate) .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(0L) + .isBookmarked(isBookmarked) + .score(score) .build(); } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialImageUrl(new Url(officialImageUrl)) - .careerUrl(new Url(careerUrl)) - .officialUrl(new Url(officialUrl)) + + private TechArticleDetailResponse createTechArticleDetailResponse(String thumbnailUrl, String techArticleUrl, + String title, String contents, Long companyId, + String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long viewCount, + long recommendCount, long commentCount, long popularScore, + boolean isRecommended, boolean isBookmarked) { + return TechArticleDetailResponse.builder() + .thumbnailUrl(thumbnailUrl) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(popularScore) + .isRecommended(isRecommended) + .isBookmarked(isBookmarked) .build(); } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java index ca146955..1001baa4 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java @@ -69,8 +69,6 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; @@ -104,15 +102,13 @@ public class MyPageControllerDocsTest extends SupportControllerDocsTest { private static final int TEST_ARTICLES_COUNT = 20; - private static List techArticles; + private static final List techArticles = new ArrayList<>(); @Autowired TechArticleRepository techArticleRepository; @Autowired CompanyRepository companyRepository; @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired MemberRepository memberRepository; @Autowired BookmarkRepository bookmarkRepository; @@ -139,30 +135,13 @@ public class MyPageControllerDocsTest extends SupportControllerDocsTest { @BeforeAll static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { + @Autowired CompanyRepository companyRepository) { Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "https://example.com"); companyRepository.save(company); - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + for (int i = 0; i < 10; i++) { + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } techArticleRepository.saveAll(techArticles); @@ -170,9 +149,7 @@ static void setup(@Autowired TechArticleRepository techArticleRepository, @AfterAll static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); techArticleRepository.deleteAllInBatch(); companyRepository.deleteAllInBatch(); } @@ -226,8 +203,6 @@ void getBookmarkedTechArticles() throws Exception { fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("기술블로그 메인 배열"), fieldWithPath("data.content.[].id").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), - fieldWithPath("data.content.[].elasticId").type(JsonFieldType.STRING) - .description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.content.[].techArticleUrl").type(JsonFieldType.STRING) .description("기술블로그 Url"), fieldWithPath("data.content.[].thumbnailUrl").type(JsonFieldType.STRING) @@ -1003,30 +978,6 @@ private static LocalDate createRandomDate() { return startDate.plusDays(randomDays); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .build(); - } - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, String careerUrl) { return Company.builder() @@ -1048,4 +999,20 @@ private static Company createCompany(String companyName, String officialUrl, Str .industry(industry) .build(); } + + private static TechArticle createTechArticle(int i, Company company) { + return TechArticle.builder() + .title(new Title("타이틀 " + i)) + .contents("내용 " + i) + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(i)) + .recommendTotalCount(new Count(i)) + .viewTotalCount(new Count(i)) + .popularScore(new Count(i)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java index 64bf1939..76ad5b56 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/NotificationControllerDocsTest.java @@ -349,7 +349,7 @@ void getNotifications() throws Exception { // given PageRequest pageable = PageRequest.of(0, 1); TechArticleMainResponse techArticleMainResponse = createTechArticleMainResponse( - 1L, "elasticId", "http://thumbnailUrl.com", false, + 1L, "http://thumbnailUrl.com", false, "http://techArticleUrl.com", "기술블로그 타이틀", "기술블로그 내용", 1L, "기업명", "http://careerUrl.com", "http://officialImage.com", LocalDate.now(), "작성자", 0L, 0L, 0L, false, null @@ -392,7 +392,6 @@ void getNotifications() throws Exception { fieldWithPath("data.content[].isRead").type(BOOLEAN).description("회원의 알림 읽음 여부"), fieldWithPath("data.content[].techArticle").type(OBJECT).description("기술블로그 정보"), fieldWithPath("data.content[].techArticle.id").type(NUMBER).description("기술블로그 ID"), - fieldWithPath("data.content[].techArticle.elasticId").type(STRING).description("엘라스틱서치 ID"), fieldWithPath("data.content[].techArticle.thumbnailUrl").type(STRING).description("썸네일 URL"), fieldWithPath("data.content[].techArticle.isLogoImage").type(BOOLEAN).description("로고 이미지 여부"), fieldWithPath("data.content[].techArticle.techArticleUrl").type(STRING).description("기술블로그 URL"), @@ -507,14 +506,13 @@ void publishNotificationsNotFoundException() throws Exception { .andExpect(jsonPath("$.errorCode").isNumber()); } - private TechArticleMainResponse createTechArticleMainResponse(Long id, String elasticId, String thumbnailUrl, Boolean isLogoImage, + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, String techArticleUrl, String title, String contents, Long companyId, String companyName, String careerUrl, String officialImageUrl, LocalDate regDate, String author, long recommendCount, long commentCount, long viewCount, Boolean isBookmarked, Float score) { return TechArticleMainResponse.builder() .id(id) - .elasticId(elasticId) .thumbnailUrl(thumbnailUrl) .isLogoImage(isLogoImage) .techArticleUrl(techArticleUrl) diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java index 3d192657..0f48c41c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -60,6 +60,7 @@ import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.persistence.EntityManager; import java.nio.charset.StandardCharsets; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.Disabled; @@ -107,8 +108,7 @@ void registerTechCommentByAnonymous() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -149,8 +149,7 @@ void registerTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -206,8 +205,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId() + 1; @@ -255,11 +253,7 @@ void registerTechCommentNotFoundMemberException() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -303,8 +297,7 @@ void registerTechCommentContentsIsNullException(String contents) throws Exceptio "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -352,8 +345,7 @@ void modifyTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -416,8 +408,7 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -471,8 +462,7 @@ void modifyTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -521,8 +511,7 @@ void deleteTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -578,8 +567,7 @@ void deleteTechCommentNotFoundException() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -622,8 +610,7 @@ void registerTechReply() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -699,9 +686,7 @@ void registerTechReplyContentsIsNullException(String contents) throws Exception "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long techArticleId = savedTechArticle.getId(); @@ -762,8 +747,7 @@ void getTechComments() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -942,9 +926,7 @@ void recommendTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -1001,9 +983,7 @@ void recommendTechCommentNotFoundTechComment() throws Exception { "https://example.com"); companyRepository.save(company); - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); @@ -1065,9 +1045,7 @@ void getTechBestComments() throws Exception { companyRepository.save(company); // 기술 블로그 생성 - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(12L), new Count(1L), null, company); + TechArticle techArticle = createTechArticle(company); techArticleRepository.save(techArticle); // 댓글 생성 @@ -1168,4 +1146,20 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ) )); } + + private TechArticle createTechArticle(Company company) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") + .company(company) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(new Url("https://example.com/images/thumbnail.png")) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java index e41092db..4a9776aa 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleControllerDocsTest.java @@ -2,9 +2,13 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_CURSOR_SCORE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.techArticleSortType; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -22,38 +26,23 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import com.dreamypatisiel.devdevdev.domain.entity.Bookmark; -import com.dreamypatisiel.devdevdev.domain.entity.Company; -import com.dreamypatisiel.devdevdev.domain.entity.Member; -import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; -import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; -import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; -import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleSort; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticTechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.GuestTechArticleService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.MemberTechArticleService; +import com.dreamypatisiel.devdevdev.exception.MemberException; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.exception.TechArticleException; import com.dreamypatisiel.devdevdev.global.constant.SecurityConstant; -import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.*; import java.nio.charset.StandardCharsets; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.List; -import java.util.Random; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; @@ -64,93 +53,41 @@ public class TechArticleControllerDocsTest extends SupportControllerDocsTest { - private static final int TEST_ARTICLES_COUNT = 20; - private static Company company; - private static TechArticle firstTechArticle; - private static List techArticles; - - @Autowired - TechArticleRepository techArticleRepository; - @Autowired - CompanyRepository companyRepository; - @Autowired - ElasticTechArticleRepository elasticTechArticleRepository; - @Autowired - MemberRepository memberRepository; - @Autowired - BookmarkRepository bookmarkRepository; - - @BeforeAll - static void setup(@Autowired TechArticleRepository techArticleRepository, - @Autowired CompanyRepository companyRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository) { - company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", - "https://example.com"); - companyRepository.save(company); - - // 엘라스틱 기술블로그 데이터를 최신순->오래된순, 조회수많은순->적은순, 댓글많은순->적은순의 순서로 생성한다. - LocalDate baseDate = LocalDate.of(2024, 8, 30); - List elasticTechArticles = new ArrayList<>(); - for (int i = 1; i <= TEST_ARTICLES_COUNT; i++) { - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId_" + i, "타이틀_" + i, - baseDate.minusDays(i), "내용", "http://example.com/" + i, "설명", "http://example.com/", "작성자", - company.getName().getCompanyName(), company.getId(), (long) TEST_ARTICLES_COUNT - i, - (long) TEST_ARTICLES_COUNT - i, (long) TEST_ARTICLES_COUNT - i, - (long) (TEST_ARTICLES_COUNT - i) * 10); - elasticTechArticles.add(elasticTechArticle); - } - Iterable elasticTechArticleIterable = elasticTechArticleRepository.saveAll( - elasticTechArticles); - - // 엘라스틱 기술블로그를 토대로 RDB 기술블로그 데이터를 생성한다. - techArticles = new ArrayList<>(); - for (ElasticTechArticle elasticTechArticle : elasticTechArticleIterable) { - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); - } - List savedTechArticles = techArticleRepository.saveAll(techArticles); - firstTechArticle = savedTechArticles.getFirst(); - } + @MockBean + GuestTechArticleService guestTechArticleService; + + @MockBean + MemberTechArticleService memberTechArticleService; - @AfterAll - static void tearDown(@Autowired TechArticleRepository techArticleRepository, - @Autowired ElasticTechArticleRepository elasticTechArticleRepository, - @Autowired CompanyRepository companyRepository) { - elasticTechArticleRepository.deleteAll(); - techArticleRepository.deleteAllInBatch(); - companyRepository.deleteAllInBatch(); - } @Test @DisplayName("회원이 기술블로그 메인을 조회한다.") void getTechArticlesByMember() throws Exception { // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); - - List bookmarks = new ArrayList<>(); - for (TechArticle techArticle : techArticles) { - if (creatRandomBoolean()) { - Bookmark bookmark = createBookmark(member, techArticle, true); - bookmarks.add(bookmark); - } - } - bookmarkRepository.saveAll(bookmarks); - Pageable pageable = PageRequest.of(0, 1); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; - String companyId = company.getId().toString(); + String companyId = "1"; + + TechArticleMainResponse response = createTechArticleMainResponse( + 1L, "http://thumbnail.com", false, "http://article.com", "타이틀 1", "내용 1", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 10L, 5L, 100L, true, 10.0f + ); + + SliceCustom mockSlice = new SliceCustom<>( + List.of(response), pageable, false, 1L + ); + + given(memberTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willReturn(mockSlice); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) .queryParam("keyword", keyword) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("companyId", companyId) .queryParam("score", "10") .contentType(MediaType.APPLICATION_JSON) @@ -172,7 +109,7 @@ void getTechArticlesByMember() throws Exception { .attributes(techArticleSortType()), parameterWithName("keyword").optional().description("검색어"), parameterWithName("companyId").optional().description("회사 아이디"), - parameterWithName("elasticId").optional().description("마지막 데이터의 엘라스틱서치 아이디"), + parameterWithName("techArticleId").optional().description("마지막 데이터의 기술블로그 아이디"), parameterWithName("score").optional().description("마지막 데이터의 정확도 점수(정확도순 검색일 때에만 필수)") ), responseFields( @@ -181,8 +118,6 @@ void getTechArticlesByMember() throws Exception { fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("기술블로그 메인 배열"), fieldWithPath("data.content.[].id").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), - fieldWithPath("data.content.[].elasticId").type(JsonFieldType.STRING) - .description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.content.[].techArticleUrl").type(JsonFieldType.STRING) .description("기술블로그 Url"), fieldWithPath("data.content.[].thumbnailUrl").type(JsonFieldType.STRING) @@ -246,46 +181,22 @@ void getTechArticlesByMember() throws Exception { } @Test - @DisplayName("기술블로그 메인을 조회할 때 존재하지 않는 엘라스틱서치 ID를 조회하면 에러가 발생한다.") - void getTechArticlesNotFoundElasticIdException() throws Exception { - // given - Pageable pageable = PageRequest.of(0, 1); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") - .queryParam("size", String.valueOf(pageable.getPageSize())) - .queryParam("techArticleSort", TechArticleSort.LATEST.name()) - .queryParam("elasticId", "elasticId") - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8)) - .andDo(print()) - .andExpect(status().isNotFound()); - - // Docs - actions.andDo(document("not-found-elastic-tech-article-cursor-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - - @Test - @DisplayName("커서 방식으로 다음 페이지의 엘라스틱서치 기술블로그를 검색어로 검색할 때," + + @DisplayName("커서 방식으로 다음 페이지의 기술블로그를 검색어로 검색할 때," + "정확도 내림차순으로 조회하기 위한 점수가 없다면 예외가 발생한다.") void getTechArticlesWithKeywordWithCursorOrderByHIGHEST_SCOREWithoutScoreException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); - String elasticId = "elasticId_1"; + String techArticleId = "1"; String keyword = "타이틀"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(NOT_FOUND_CURSOR_SCORE_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles") .queryParam("size", String.valueOf(pageable.getPageSize())) .queryParam("techArticleSort", TechArticleSort.HIGHEST_SCORE.name()) - .queryParam("elasticId", elasticId) + .queryParam("techArticleId", techArticleId) .queryParam("keyword", keyword) .contentType(MediaType.APPLICATION_JSON) .characterEncoding(StandardCharsets.UTF_8)) @@ -309,6 +220,9 @@ void getTechArticlesSpecialSymbolException() throws Exception { // given Pageable pageable = PageRequest.of(0, 10); String keyword = "!"; + + given(guestTechArticleService.getTechArticles(any(), any(), any(), any(), any(), any(), any())) + .willThrow(new TechArticleException(KEYWORD_WITH_SPECIAL_SYMBOLS_EXCEPTION_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(MockMvcRequestBuilders.get("/devdevdev/api/v1/articles") @@ -337,12 +251,16 @@ void getTechArticlesSpecialSymbolException() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회한다.") void getTechArticleByMember() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleDetailResponse response = createTechArticleDetailResponse( + "http://thumbnail.com", "http://article.com", "기술블로그 제목", "기술블로그 내용", + 1L, "회사명", "http://career.com", "http://official.com", LocalDate.now(), "작성자", + 100L, 10L, 5L, 50L, false, true + ); + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) @@ -366,8 +284,6 @@ void getTechArticleByMember() throws Exception { responseFields( fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), fieldWithPath("data").type(JsonFieldType.OBJECT).description("응답 데이터"), - - fieldWithPath("data.elasticId").type(JsonFieldType.STRING).description("기술블로그 엘라스틱서치 아이디"), fieldWithPath("data.techArticleUrl").type(JsonFieldType.STRING).description("기술블로그 Url"), fieldWithPath("data.thumbnailUrl").type(JsonFieldType.STRING).description("기술블로그 썸네일 이미지"), fieldWithPath("data.title").type(JsonFieldType.STRING).description("기술블로그 제목"), @@ -398,12 +314,10 @@ void getTechArticleByMember() throws Exception { @DisplayName("회원이 기술블로그 상세를 조회할 때 회원이 없으면 예외가 발생한다.") void getTechArticleNotFoundMemberException() throws Exception { // given - Long id = firstTechArticle.getId(); - // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); + Long id = 1L; + + given(memberTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new MemberException(INVALID_MEMBER_NOT_FOUND_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) @@ -431,26 +345,20 @@ void getTechArticleNotFoundMemberException() throws Exception { @DisplayName("기술블로그 상세를 조회할 때 기술블로그가 존재하지 않으면 예외가 발생한다.") void getTechArticleNotFoundTechArticleException() throws Exception { // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId() + 1; - - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 999L; + + given(guestTechArticleService.getTechArticle(eq(id), any(), any())) + .willThrow(new NotFoundException(NOT_FOUND_TECH_ARTICLE_MESSAGE)); // when // then ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); + .characterEncoding(StandardCharsets.UTF_8)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) + .andExpect(jsonPath("$.message").value(NOT_FOUND_TECH_ARTICLE_MESSAGE)) + .andExpect(jsonPath("$.errorCode").value(HttpStatus.NOT_FOUND.value())); // Docs actions.andDo(document("not-found-tech-article-exception", @@ -463,76 +371,15 @@ void getTechArticleNotFoundTechArticleException() throws Exception { )); } - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱ID가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticIdException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), null, company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); - - // Docs - actions.andDo(document("not-found-elastic-id-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - - @Test - @DisplayName("기술블로그 상세를 조회할 때 엘라스틱 기술블로그가 존재하지 않으면 예외가 발생한다.") - void getTechArticleNotFoundElasticTechArticleException() throws Exception { - // given - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), - new Count(1L), - new Count(1L), "elasticId", company); - TechArticle savedTechArticle = techArticleRepository.save(techArticle); - Long id = savedTechArticle.getId(); - - // when // then - ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/articles/{techArticleId}", id) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding(StandardCharsets.UTF_8) - .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) - .andDo(print()); - - // Docs - actions.andDo(document("not-found-elastic-tech-article-exception", - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), - fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), - fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") - ) - )); - } - @Test @DisplayName("회원이 기술블로그 북마크를 요청한다.") void updateBookmark() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + BookmarkResponse response = new BookmarkResponse(id, true); + given(memberTechArticleService.updateBookmark(eq(id), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(post("/devdevdev/api/v1/articles/{techArticleId}/bookmark", id) @@ -565,12 +412,11 @@ void updateBookmark() throws Exception { @DisplayName("회원이 기술블로그 추천을 요청한다.") void updateRecommend() throws Exception { // given - Long id = firstTechArticle.getId(); - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); - Member member = Member.createMemberBy(socialMemberDto); - member.updateRefreshToken(refreshToken); - memberRepository.save(member); + Long id = 1L; + + TechArticleRecommendResponse response = new TechArticleRecommendResponse(id, true, 11L); + given(memberTechArticleService.updateRecommend(eq(id), any(), any())) + .willReturn(response); // when // then ResultActions actions = mockMvc.perform(post("/devdevdev/api/v1/articles/{techArticleId}/recommend", id) @@ -597,77 +443,54 @@ void updateRecommend() throws Exception { fieldWithPath("data.techArticleId").type(JsonFieldType.NUMBER).description("기술블로그 아이디"), fieldWithPath("data.status").type(JsonFieldType.BOOLEAN).description("추천 상태"), fieldWithPath("data.recommendTotalCount").type(JsonFieldType.NUMBER).description("기술블로그 총 추천수") - ) - )); + )) + ); } - private SocialMemberDto createSocialDto(String userId, String name, String nickName, String password, String email, - String socialType, String role) { - return SocialMemberDto.builder() - .userId(userId) - .name(name) - .nickname(nickName) - .password(password) - .email(email) - .socialType(SocialType.valueOf(socialType)) - .role(Role.valueOf(role)) - .build(); - } - - private Bookmark createBookmark(Member member, TechArticle techArticle, boolean status) { - return Bookmark.builder() - .member(member) - .techArticle(techArticle) - .status(status) - .build(); - } - - private boolean creatRandomBoolean() { - return new Random().nextBoolean(); - } - - private static LocalDate createRandomDate() { - LocalDate startDate = LocalDate.of(2024, 1, 1); - LocalDate endDate = LocalDate.of(2024, 3, 10); - - // 시작 날짜와 종료 날짜 사이의 차이 중 랜덤한 일 수 선택 - long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); - long randomDays = ThreadLocalRandom.current().nextLong(daysBetween + 1); - - return startDate.plusDays(randomDays); - } - - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() + private TechArticleMainResponse createTechArticleMainResponse(Long id, String thumbnailUrl, Boolean isLogoImage, + String techArticleUrl, String title, String contents, + Long companyId, String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long recommendCount, + long commentCount, long viewCount, Boolean isBookmarked, Float score) { + return TechArticleMainResponse.builder() .id(id) + .thumbnailUrl(thumbnailUrl) + .isLogoImage(isLogoImage) + .techArticleUrl(techArticleUrl) .title(title) - .regDate(regDate) .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) .author(author) - .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(0L) + .isBookmarked(isBookmarked) + .score(score) .build(); } - - private static Company createCompany(String companyName, String officialImageUrl, String officialUrl, - String careerUrl) { - return Company.builder() - .name(new CompanyName(companyName)) - .officialImageUrl(new Url(officialImageUrl)) - .careerUrl(new Url(careerUrl)) - .officialUrl(new Url(officialUrl)) + + private TechArticleDetailResponse createTechArticleDetailResponse(String thumbnailUrl, String techArticleUrl, + String title, String contents, Long companyId, + String companyName, String careerUrl, String officialImageUrl, + LocalDate regDate, String author, long viewCount, + long recommendCount, long commentCount, long popularScore, + boolean isRecommended, boolean isBookmarked) { + return TechArticleDetailResponse.builder() + .thumbnailUrl(thumbnailUrl) + .techArticleUrl(techArticleUrl) + .title(title) + .contents(contents) + .company(CompanyResponse.of(companyId, companyName, careerUrl, officialImageUrl)) + .regDate(regDate) + .author(author) + .viewTotalCount(viewCount) + .recommendTotalCount(recommendCount) + .commentTotalCount(commentCount) + .popularScore(popularScore) + .isRecommended(isRecommended) + .isBookmarked(isBookmarked) .build(); } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java index ae8b1691..ba79b0f5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechArticleMainResponseTest.java @@ -6,7 +6,6 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticTechArticle; import java.time.LocalDate; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -17,25 +16,15 @@ class TechArticleMainResponseTest { @Test - @DisplayName("ElasticTechArticle의 썸네일 이미지가 있다면 썸네일 이미지로 설정되어야 한다.") + @DisplayName("기술블로그의 썸네일 이미지가 있다면 썸네일 이미지로 설정되어야 한다.") public void setThumbnailImageWhenPresent() { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://officialUrl.com", "https://careerUrl.com"); - - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId", "타이틀", LocalDate.now(), - "내용", "http://example.com/", "설명", "http://thumbnailImage.com/image.png", "작성자", - company.getName().getCompanyName(), company.getId(), 0L, 0L, 0L, 0L); - - CompanyResponse companyResponse = CompanyResponse.from(company); + TechArticle techArticle = createTechArticle(company, new Url("http://thumbnailImage.com/image.png")); // when - TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse - .of(techArticle, elasticTechArticle, companyResponse); + TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse.of(techArticle); // then assertEquals("http://thumbnailImage.com/image.png", techArticleMainResponse.getThumbnailUrl()); @@ -43,25 +32,15 @@ public void setThumbnailImageWhenPresent() { } @Test - @DisplayName("ElasticTechArticle의 썸네일 이미지가 없다면 회사 로고 이미지로 대체하고, isLogoImage가 true로 설정되어야 한다.") + @DisplayName("기술블로그의 썸네일 이미지가 없다면 회사 로고 이미지로 대체하고, isLogoImage가 true로 설정되어야 한다.") public void setLogoImageWhenThumbnailIsAbsent() { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://officialUrl.com", "https://careerUrl.com"); - - TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); - - ElasticTechArticle elasticTechArticle = createElasticTechArticle("elasticId", "타이틀", LocalDate.now(), - "내용", "http://example.com/", "설명", null, "작성자", - company.getName().getCompanyName(), company.getId(), 0L, 0L, 0L, 0L); - - CompanyResponse companyResponse = CompanyResponse.from(company); + TechArticle techArticle = createTechArticle(company, null); // when - TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse - .of(techArticle, elasticTechArticle, companyResponse); + TechArticleMainResponse techArticleMainResponse = TechArticleMainResponse.of(techArticle); // then assertEquals(company.getOfficialImageUrl().getUrl(), techArticleMainResponse.getThumbnailUrl()); @@ -78,27 +57,19 @@ private static Company createCompany(String companyName, String officialImageUrl .build(); } - private static ElasticTechArticle createElasticTechArticle(String id, String title, LocalDate regDate, - String contents, String techArticleUrl, - String description, String thumbnailUrl, String author, - String company, Long companyId, - Long viewTotalCount, Long recommendTotalCount, - Long commentTotalCount, Long popularScore) { - return ElasticTechArticle.builder() - .id(id) - .title(title) - .regDate(regDate) - .contents(contents) - .techArticleUrl(techArticleUrl) - .description(description) - .thumbnailUrl(thumbnailUrl) - .author(author) + private TechArticle createTechArticle(Company company, Url thumbnailUrl) { + return TechArticle.builder() + .title(new Title("타이틀 ")) + .contents("내용 ") .company(company) - .companyId(companyId) - .viewTotalCount(viewTotalCount) - .recommendTotalCount(recommendTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) + .author("작성자") + .regDate(LocalDate.now()) + .techArticleUrl(new Url("https://example.com/article")) + .thumbnailUrl(thumbnailUrl) + .commentTotalCount(new Count(1)) + .recommendTotalCount(new Count(1)) + .viewTotalCount(new Count(1)) + .popularScore(new Count(1)) .build(); } } \ No newline at end of file