diff --git a/.github/workflows/cicd-light-sail-prod.yml b/.github/workflows/cicd-light-sail-prod.yml index 3ae85a22..05c7dbf1 100644 --- a/.github/workflows/cicd-light-sail-prod.yml +++ b/.github/workflows/cicd-light-sail-prod.yml @@ -237,7 +237,7 @@ jobs: uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} - author_name: "[PROD-TEST] 배포 결과를 알려드려요" + author_name: "[PROD] 배포 결과를 알려드려요" fields: repo,message,commit,author,eventName,ref,took env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/src/docs/asciidoc/api/mypage/can-change-nickname.adoc b/src/docs/asciidoc/api/mypage/can-change-nickname.adoc new file mode 100644 index 00000000..c2810804 --- /dev/null +++ b/src/docs/asciidoc/api/mypage/can-change-nickname.adoc @@ -0,0 +1,19 @@ +[[CanChangeNickname]] +== 닉네임 변경 가능 여부 API(GET: /devdevdev/api/v1/mypage/nickname/changeable) +* 회원은 닉네임 변경 가능 여부를 확인할 수 있다. +* 비회원은 닉네임 변경 가능 여부를 확인할 수 없다. + +=== 정상 요청/응답 +==== HTTP Request +include::{snippets}/can-change-nickname/http-request.adoc[] +==== HTTP Request Header Fields +include::{snippets}/can-change-nickname/request-headers.adoc[] + +==== HTTP Response +include::{snippets}/can-change-nickname/http-response.adoc[] +==== HTTP Response Fields +include::{snippets}/can-change-nickname/response-fields.adoc[] + +=== 예외 +==== HTTP Response +include::{snippets}/not-found-member-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/mypage/change-nickname.adoc b/src/docs/asciidoc/api/mypage/change-nickname.adoc new file mode 100644 index 00000000..7529bda3 --- /dev/null +++ b/src/docs/asciidoc/api/mypage/change-nickname.adoc @@ -0,0 +1,22 @@ +[[ChangeNickname]] +== 닉네임 변경 API(PATCH: /devdevdev/api/v1/mypage/nickname) +* 회원은 닉네임을 변경할 수 있다. +* 비회원은 닉네임을 변경할 수 없다. + +=== 정상 요청/응답 +==== HTTP Request +include::{snippets}/change-nickname/http-request.adoc[] +==== HTTP Request Header Fields +include::{snippets}/change-nickname/request-headers.adoc[] +==== HTTP Request Fields +include::{snippets}/change-nickname/request-fields.adoc[] + +==== HTTP Response +include::{snippets}/change-nickname/http-response.adoc[] +==== HTTP Response Fields +include::{snippets}/change-nickname/response-fields.adoc[] + +=== 예외 +==== HTTP Response +include::{snippets}/not-found-member-exception/response-body.adoc[] +include::{snippets}/change-nickname-within-24hours-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/mypage/mypage.adoc b/src/docs/asciidoc/api/mypage/mypage.adoc index b11e06bb..5dfd81ca 100644 --- a/src/docs/asciidoc/api/mypage/mypage.adoc +++ b/src/docs/asciidoc/api/mypage/mypage.adoc @@ -7,3 +7,6 @@ include::exit-survey.adoc[] include::record-exit-survey.adoc[] include::comment-get.adoc[] include::subscribed-companies.adoc[] +include::random-nickname.adoc[] +include::change-nickname.adoc[] +include::can-change-nickname.adoc[] diff --git a/src/docs/asciidoc/api/mypage/random-nickname.adoc b/src/docs/asciidoc/api/mypage/random-nickname.adoc new file mode 100644 index 00000000..55173e79 --- /dev/null +++ b/src/docs/asciidoc/api/mypage/random-nickname.adoc @@ -0,0 +1,20 @@ +[[GetRandomNickname]] +== 랜덤 닉네임 생성 API(GET: /devdevdev/api/v1/mypage/nickname/random) +* 회원은 랜덤 닉네임을 생성할 수 있다. +* 비회원은 랜덤 닉네임을 생성할 수 없다. + +=== 정상 요청/응답 +==== HTTP Request +include::{snippets}/random-nickname/http-request.adoc[] +==== HTTP Request Header Fields +include::{snippets}/random-nickname/request-headers.adoc[] + +==== HTTP Response +include::{snippets}/random-nickname/http-response.adoc[] +==== HTTP Response Fields +include::{snippets}/random-nickname/response-fields.adoc[] + + +=== 예외 +==== HTTP Response +include::{snippets}/not-found-member-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/pick-commnet/pick-comment-delete.adoc b/src/docs/asciidoc/api/pick-commnet/pick-comment-delete.adoc index 634111e6..892a4c4d 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-delete.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-delete.adoc @@ -2,7 +2,9 @@ == 픽픽픽 댓글/답글 삭제 API(DELETE: /devdevdev/api/v1/picks/{pickId}/comments/{pickCommentId}) * 픽픽픽 댓글/답글을 삭제한다. -* 회원 본인이 작성한 픽픽픽 댓글/답글만 삭제 할 수 있다. +* 본인이 작성한 픽픽픽 댓글/답글만 삭제 할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. * 삭제된 댓글/답글을 삭제 할 수 없다. * ##어드민 권한을 가진 회원은 모든 댓글/답글을 삭제##할 수 있다. @@ -34,7 +36,7 @@ include::{snippets}/delete-pick-comment/response-fields.adoc[] * `픽픽픽 댓글이 없습니다.`: 픽픽픽 댓글이 존재하지 않거나 본인이 작성하지 않았거나 픽픽픽 댓글 삭제된 경우 * `승인 상태가 아닌 픽픽픽에는 댓글을 삭제할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/delete-pick-comment-not-found-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc b/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc index 4904225c..9605aa83 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-modify.adoc @@ -2,7 +2,9 @@ == 픽픽픽 댓글/답글 수정 API(PATCH: /devdevdev/api/v1/picks/{pickId}/comments/{pickCommentId}) * 픽픽픽 댓글/답글을 수정한다. -* 회원 본인이 작성한 픽픽픽 댓글/답글을 수정 할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. +* 회원 또는 익명회원 본인이 작성한 픽픽픽 댓글/답글 만 수정 할 수 있다. * 픽픽픽 공개 여부는 수정 할 수 없다. * 삭제된 댓글/답글을 수정 할 수 없다. @@ -39,7 +41,7 @@ include::{snippets}/modify-pick-comment/response-fields.adoc[] * `내용을 작성해주세요.`: 댓글(contents)을 작성하지 않는 경우(공백 이거나 빈문자열) * `픽픽픽 댓글이 없습니다.`: 픽픽픽 댓글이 존재하지 않거나 본인이 작성하지 않았거나 픽픽픽 댓글 삭제된 경우 * `승인 상태가 아닌 픽픽픽에는 댓글을 수정할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/modify-pick-comment-bind-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/pick-commnet/pick-comment-register.adoc b/src/docs/asciidoc/api/pick-commnet/pick-comment-register.adoc index c2ff0f66..6be42480 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-register.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-register.adoc @@ -2,7 +2,9 @@ == 픽픽픽 댓글 작성 API(POST: /devdevdev/api/v1/picks/{pickId}/comments) * 픽픽픽 댓글을 작성한다. -* 회원만 픽픽픽 댓글을 작성 할 수 있다. +* 픽픽픽 댓글을 작성 할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. === 정상 요청/응답 @@ -39,7 +41,7 @@ include::{snippets}/register-pick-comment/response-fields.adoc[] * `픽픽픽 게시글이 없습니다.`: 픽픽픽 게시글이 존재하지 않는 경우 * `승인 상태가 아닌 픽픽픽에는 댓글을 작성할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 * `투표한 픽픽픽 선택지가 존재하지 않습니다.`: 투표한 픽픽픽 선택지가 존재하지 않는 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/register-pick-comment-bind-exception-pick-vote-public-is-null/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/pick-commnet/pick-comment-reply-register.adoc b/src/docs/asciidoc/api/pick-commnet/pick-comment-reply-register.adoc index cc322447..28433742 100644 --- a/src/docs/asciidoc/api/pick-commnet/pick-comment-reply-register.adoc +++ b/src/docs/asciidoc/api/pick-commnet/pick-comment-reply-register.adoc @@ -2,7 +2,8 @@ == 픽픽픽 답글 작성 API(POST: /devdevdev/api/v1/picks/{pickId}/comments/{pickOriginParentCommentId}/{pickParentCommentId}) * 픽픽픽 답글을 작성한다. -* 회원만 픽픽픽 답글을 작성 할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. * #픽픽픽 댓글이 삭제 상태# 이면 답글을 작성 할 수 없다. * 최초 댓글에 대한 답글을 작성할 경우 `pickCommentOriginParentId` 값과 `pickParentCommentId` 값이 동일하다. @@ -40,7 +41,7 @@ include::{snippets}/register-pick-comment-reply/response-fields.adoc[] * `픽픽픽 댓글이 없습니다.`: 픽픽픽 댓글이 존재하지 않는 경우 * `삭제된 픽픽픽 댓글에는 답글을 작성할 수 없습니다.`: 픽픽픽 댓글이 삭제된 경우 * `승인 상태가 아닌 픽픽픽에는 답글을 작성할 수 없습니다.`: 픽픽픽이 승인 상태가 아닌 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/register-pick-comment-reply-bind-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-delete.adoc b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-delete.adoc index a9a6d42f..141615ff 100644 --- a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-delete.adoc +++ b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-delete.adoc @@ -2,7 +2,9 @@ == 기술블로그 댓글 삭제 API(DELETE: /devdevdev/api/v1/articles/{techArticleId}/comments/{techCommentId}) * 기술블로그 댓글을 삭제한다. -* 회원 본인이 작성한 기술블로그 댓글을 삭제할 수 있다. +* 본인이 작성한 기술블로그 댓글을 삭제할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. * 어드민 권한을 가진 회원은 모든 댓글을 삭제할 수 있다. === 정상 요청/응답 @@ -33,7 +35,7 @@ include::{snippets}/delete-tech-article-comments/response-fields.adoc[] * `존재하지 않는 기술블로그입니다.`: 기술블로그가 존재하지 않는 경우 * `존재하지 않는 기술블로그 댓글입니다`: 기술블로그 댓글이 존재하지 않거나, 삭제된 댓글이거나, 본인이 작성한 댓글이 아닐 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원인 경우 * `회원을 찾을 수 없습니다.`: 회원이 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/delete-tech-article-comments-not-found-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-modify.adoc b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-modify.adoc index 438af5f7..524b44e0 100644 --- a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-modify.adoc +++ b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-modify.adoc @@ -2,8 +2,10 @@ == 기술블로그 댓글 수정 API(PATCH: /devdevdev/api/v1/articles/{techArticleId}/comments/{techCommentId}) * 기술블로그 댓글을 수정한다. -* 회원 본인이 작성한 기술블로그 댓글을 수정할 수 있다. -* 삭제된 댓글을 수정할 수 없다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. +* 회원 또는 익명회원 본인이 작성한 기술블로그 댓글/답글 만 수정 할 수 있다. +* 삭제된 댓글/답글을 수정할 수 없다. === 정상 요청/응답 diff --git a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-register.adoc b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-register.adoc index db0b3fec..4c60b769 100644 --- a/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-register.adoc +++ b/src/docs/asciidoc/api/tech-article-comment/tech-article-comment-register.adoc @@ -1,8 +1,9 @@ [[Tech-Article-Comments-Register]] == 기술블로그 댓글 작성 API(POST: /devdevdev/api/v1/articles/{techArticleId}/comments) -* 회원은 기술블로그에 댓글을 작성할 수 있다. -* 익명회원은 댓글을 작성할 수 없다. +* 기술블로그에 댓글을 작성할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. === 정상 요청/응답 @@ -36,10 +37,7 @@ include::{snippets}/tech-article-comments/response-fields.adoc[] * `댓글 내용을 작성해주세요.`: 댓글(contents)을 작성하지 않는 경우(공백 이거나 빈문자열) * `회원을 찾을 수 없습니다.`: 회원 정보가 없을 경우 -* `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원이 사용할 수 없는 기능일 경우 * `존재하지 않는 기술블로그입니다.`: 기술블로그가 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 -include::{snippets}/tech-article-comments-anonymous-exception/response-body.adoc[] -include::{snippets}/tech-article-comments-not-found-member-exception/response-body.adoc[] -include::{snippets}/tech-article-comments-not-found-tech-article-exception/response-body.adoc[] -include::{snippets}/tech-article-comments-null-exception/response-body.adoc[] +include::{snippets}/tech-article-comments-not-found-member-exception/response-body.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/tech-article-comment/tech-article-reply-register.adoc b/src/docs/asciidoc/api/tech-article-comment/tech-article-reply-register.adoc index 44e8d65c..a4255810 100644 --- a/src/docs/asciidoc/api/tech-article-comment/tech-article-reply-register.adoc +++ b/src/docs/asciidoc/api/tech-article-comment/tech-article-reply-register.adoc @@ -1,8 +1,9 @@ [[Tech-Article-Reply-Register]] == 기술블로그 답글 작성 API(POST: /devdevdev/api/v1/articles/{techArticleId}/comments/{originParentTechCommentId}/{parentTechCommentId} -* 회원은 기술블로그에 댓글에 답글을 작성할 수 있다. -* 익명회원은 답글을 작성할 수 없다. +* 기술블로그에 댓글에 답글을 작성할 수 있다. +** 회원인 경우 토큰을 `Authorization` 헤더에 포함시켜야 한다. +** 익명 회원인 경우 `Anonymous-Member-Id` 헤더에 익명 회원 아이디를 포함시켜야 한다. * 삭제된 댓글에는 답글을 작성할 수 없다. * 최초 댓글에 대한 답글을 작성할 경우 `techCommentOriginParentId` 값과 `techParentCommentId` 값이 동일하다. @@ -40,5 +41,6 @@ include::{snippets}/register-tech-article-reply/response-fields.adoc[] * `회원을 찾을 수 없습니다.`: 회원 정보가 없을 경우 * `익명 회원은 사용할 수 없는 기능 입니다.`: 익명 회원이 사용할 수 없는 기능일 경우 * `존재하지 않는 기술블로그입니다.`: 기술블로그가 존재하지 않는 경우 +* `익명 사용자가 아닙니다. 잘못된 메소드 호출 입니다.`: 회원이 익명 회원 메소드를 호출한 경우 include::{snippets}/register-tech-article-reply-null-exception/response-body.adoc[] 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 a3dc2f7e..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,17 +203,11 @@ 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(); - int count = 0; - for (ElasticTechArticle elasticTechArticle : elasticTechArticles) { - count++; - Company company = companyIdMap.get(elasticTechArticle.getCompanyId()); - if (company == null) { - log.info("company가 null 이다. elasticTechArticleId={} count={}", elasticTechArticle.getId(), count); - } - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + for (int i = 0; i < companies.size(); i++) { + Company company = companies.get(i); + TechArticle techArticle = createTechArticle(i, company); techArticles.add(techArticle); } return techArticles; @@ -366,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/AnonymousMember.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java index ae22a1cb..b7cef6b2 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/AnonymousMember.java @@ -27,14 +27,17 @@ public class AnonymousMember extends BasicTime { @Column(length = 30, nullable = false, unique = true) private String anonymousMemberId; + private String nickname; + @Builder private AnonymousMember(String anonymousMemberId) { this.anonymousMemberId = anonymousMemberId; } - public static AnonymousMember create(String anonymousMemberId) { + public static AnonymousMember create(String anonymousMemberId, String nickname) { AnonymousMember anonymousMember = new AnonymousMember(); anonymousMember.anonymousMemberId = anonymousMemberId; + anonymousMember.nickname = nickname; return anonymousMember; } @@ -42,4 +45,16 @@ public static AnonymousMember create(String anonymousMemberId) { public boolean isEqualAnonymousMemberId(Long id) { return this.id.equals(id); } + + public boolean hasNickName() { + return nickname != null && !nickname.isBlank(); + } + + public void changeNickname(String nickname) { + this.nickname = nickname; + } + + public boolean isEqualsId(Long id) { + return this.id.equals(id); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java index 88f31f84..62e69672 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/Member.java @@ -20,6 +20,7 @@ import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; @@ -96,6 +97,8 @@ public class Member extends BasicTime { private LocalDateTime deletedAt; + private LocalDateTime nicknameUpdatedAt; + @OneToMany(mappedBy = "member") private List interestedCompanies = new ArrayList<>(); @@ -110,7 +113,7 @@ public class Member extends BasicTime { @OneToMany(mappedBy = "member") private List recommends = new ArrayList<>(); - + public Member(Long id) { this.id = id; } @@ -187,4 +190,14 @@ public void deleteMember(LocalDateTime now) { this.isDeleted = true; this.deletedAt = now; } + + public void changeNickname(String nickname, LocalDateTime now) { + this.nickname = new Nickname(nickname); + this.nicknameUpdatedAt = now; + } + + public boolean canChangeNickname(int restrictionMinutes, LocalDateTime now) { + return nicknameUpdatedAt == null + || ChronoUnit.MINUTES.between(nicknameUpdatedAt, now) >= restrictionMinutes; + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java index 5f59fd63..c00c3446 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/PickComment.java @@ -7,6 +7,7 @@ import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -18,6 +19,8 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -27,10 +30,10 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(indexes = { - @Index(name = "idx__created_by__pick__deleted_at", columnList = "created_by, pick_id, deletedAt"), - @Index(name = "idx__comment__created_by__pick__deleted_at", columnList = "id, created_by, pick_id, deletedAt"), - @Index(name = "idx__parent__origin_parent__deleted_at", columnList = "parent_id, origin_parent_id, deletedAt"), - @Index(name = "idx__comment_01", + @Index(name = "idx_pick_comment_01", columnList = "created_by, pick_id, deletedAt"), + @Index(name = "idx_pick_comment_02", columnList = "id, created_by, pick_id, deletedAt"), + @Index(name = "idx_pick_comment_03", columnList = "parent_id, origin_parent_id, deletedAt"), + @Index(name = "idx_pick_comment_04", columnList = "id, pick_id, parent_id, origin_parent_id, isPublic, recommendTotalCount, replyTotalCount") }) public class PickComment extends BasicTime { @@ -70,27 +73,35 @@ public class PickComment extends BasicTime { private LocalDateTime contentsLastModifiedAt; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id", referencedColumnName = "id") + @JoinColumn(name = "parent_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "fk_pick_comment_01")) private PickComment parent; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "origin_parent_id", referencedColumnName = "id") + @JoinColumn(name = "origin_parent_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "fk_pick_comment_02")) private PickComment originParent; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "created_by", nullable = false) + @JoinColumn(name = "created_by", foreignKey = @ForeignKey(name = "fk_pick_comment_03")) private Member createdBy; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "deleted_by") + @JoinColumn(name = "deleted_by", foreignKey = @ForeignKey(name = "fk_pick_comment_04")) private Member deletedBy; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "pick_id", nullable = false) + @JoinColumn(name = "created_anonymous_by", foreignKey = @ForeignKey(name = "fk_pick_comment_05")) + private AnonymousMember createdAnonymousBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "deleted_anonymous_by", foreignKey = @ForeignKey(name = "fk_pick_comment_06")) + private AnonymousMember deletedAnonymousBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "pick_id", nullable = false, foreignKey = @ForeignKey(name = "fk_pick_comment_07")) private Pick pick; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "pick_vote_id") + @JoinColumn(name = "pick_vote_id", foreignKey = @ForeignKey(name = "fk_pick_comment_08")) private PickVote pickVote; @OneToMany(mappedBy = "pickComment") @@ -99,7 +110,7 @@ public class PickComment extends BasicTime { @Builder private PickComment(CommentContents contents, Count blameTotalCount, Count recommendTotalCount, Count replyTotalCount, Boolean isPublic, PickComment parent, PickComment originParent, - Member createdBy, Pick pick, PickVote pickVote) { + Member createdBy, AnonymousMember createdAnonymousBy, Pick pick, PickVote pickVote) { this.contents = contents; this.blameTotalCount = blameTotalCount; this.recommendTotalCount = recommendTotalCount; @@ -108,31 +119,25 @@ private PickComment(CommentContents contents, Count blameTotalCount, Count recom this.parent = parent; this.originParent = originParent; this.createdBy = createdBy; + this.createdAnonymousBy = createdAnonymousBy; this.pick = pick; this.pickVote = pickVote; } - public static PickComment createPrivateVoteComment(CommentContents content, Member createdBy, Pick pick) { - PickComment pickComment = new PickComment(); - pickComment.contents = content; + public static PickComment createPrivateVoteCommentByMember(CommentContents content, Member createdBy, Pick pick) { + PickComment pickComment = createPickComment(content, null, null); pickComment.isPublic = false; - pickComment.blameTotalCount = Count.defaultCount(); - pickComment.recommendTotalCount = Count.defaultCount(); - pickComment.replyTotalCount = Count.defaultCount(); pickComment.createdBy = createdBy; pickComment.changePick(pick); return pickComment; } - public static PickComment createPublicVoteComment(CommentContents content, Member createdBy, Pick pick, - PickVote pickVote) { - PickComment pickComment = new PickComment(); - pickComment.contents = content; + public static PickComment createPublicVoteCommentByMember(CommentContents content, Member createdBy, Pick pick, + PickVote pickVote) { + + PickComment pickComment = createPickComment(content, null, null); pickComment.isPublic = true; - pickComment.blameTotalCount = Count.defaultCount(); - pickComment.recommendTotalCount = Count.defaultCount(); - pickComment.replyTotalCount = Count.defaultCount(); pickComment.createdBy = createdBy; pickComment.changePick(pick); pickComment.pickVote = pickVote; @@ -141,18 +146,60 @@ public static PickComment createPublicVoteComment(CommentContents content, Membe } // 답글 생성 - public static PickComment createRepliedComment(CommentContents content, PickComment parent, - PickComment originParent, Member createdBy, Pick pick) { + public static PickComment createRepliedCommentByMember(CommentContents content, PickComment parent, + PickComment originParent, Member createdBy, Pick pick) { + PickComment pickComment = createPickComment(content, parent, originParent); + pickComment.isPublic = false; + pickComment.createdBy = createdBy; + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createPrivateVoteCommentByAnonymousMember(CommentContents content, + AnonymousMember createdAnonymousBy, Pick pick) { + PickComment pickComment = createPickComment(content, null, null); + pickComment.isPublic = false; + pickComment.createdAnonymousBy = createdAnonymousBy; + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createPublicVoteCommentByAnonymousMember(CommentContents content, + AnonymousMember createdAnonymousBy, Pick pick, + PickVote pickVote) { + PickComment pickComment = createPickComment(content, null, null); + pickComment.isPublic = true; + pickComment.createdAnonymousBy = createdAnonymousBy; + pickComment.changePick(pick); + pickComment.pickVote = pickVote; + + return pickComment; + } + + // 답글 생성 + public static PickComment createRepliedCommentByAnonymousMember(CommentContents content, PickComment parent, + PickComment originParent, AnonymousMember createdAnonymousBy, + Pick pick) { + PickComment pickComment = createPickComment(content, parent, originParent); + pickComment.isPublic = false; + pickComment.createdAnonymousBy = createdAnonymousBy; + pickComment.changePick(pick); + + return pickComment; + } + + private static PickComment createPickComment(@Nonnull CommentContents content, + @Nullable PickComment parent, + @Nullable PickComment originParent) { PickComment pickComment = new PickComment(); pickComment.contents = content; - pickComment.isPublic = false; pickComment.blameTotalCount = Count.defaultCount(); pickComment.recommendTotalCount = Count.defaultCount(); pickComment.replyTotalCount = Count.defaultCount(); pickComment.parent = parent; pickComment.originParent = originParent; - pickComment.createdBy = createdBy; - pickComment.changePick(pick); return pickComment; } @@ -163,11 +210,16 @@ public void changePick(Pick pick) { this.pick = pick; } - public void changeDeletedAt(LocalDateTime now, Member deletedBy) { + public void changeDeletedAtByMember(LocalDateTime now, Member deletedBy) { this.deletedAt = now; this.deletedBy = deletedBy; } + public void changeDeletedAtByAnonymousMember(LocalDateTime now, AnonymousMember deletedAnonymousBy) { + this.deletedAt = now; + this.deletedAnonymousBy = deletedAnonymousBy; + } + // 댓글 수정 public void modifyCommentContents(CommentContents contents, LocalDateTime lastModifiedContentsAt) { this.contents = contents; @@ -175,11 +227,19 @@ public void modifyCommentContents(CommentContents contents, LocalDateTime lastMo } public boolean isModified() { - return contentsLastModifiedAt != null; + return this.contentsLastModifiedAt != null; } public boolean isDeleted() { - return deletedAt != null; + return this.deletedAt != null; + } + + public boolean isDeletedByMember() { + return this.deletedBy != null && this.deletedAnonymousBy == null; + } + + public boolean isDeletedByAnonymousMember() { + return this.deletedBy == null && this.deletedAnonymousBy != null; } public boolean isEqualsId(Long id) { @@ -205,4 +265,20 @@ public void decrementRecommendTotalCount() { public boolean isVotePrivate() { return this.isPublic.equals(false); } + + public boolean isCreatedAnonymousMember() { + return this.createdBy == null && this.createdAnonymousBy != null; + } + + public boolean isCreatedMember() { + return this.createdBy != null && this.createdAnonymousBy == null; + } + + public boolean isDeletedMemberByMySelf() { + return this.createdBy.isEqualsId(this.deletedBy.getId()); + } + + public boolean isDeletedAnonymousMemberByMySelf() { + return this.createdAnonymousBy.isEqualsId(this.deletedAnonymousBy.getId()); + } } 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/entity/TechComment.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java index 8fdd6499..33637cca 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechComment.java @@ -2,12 +2,23 @@ import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; -import jakarta.persistence.*; - +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; - import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -53,23 +64,31 @@ public class TechComment extends BasicTime { private LocalDateTime contentsLastModifiedAt; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id", referencedColumnName = "id") + @JoinColumn(name = "parent_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "fk_tech_comment_01")) private TechComment parent; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "origin_parent_id", referencedColumnName = "id") + @JoinColumn(name = "origin_parent_id", referencedColumnName = "id", foreignKey = @ForeignKey(name = "fk_tech_comment_02")) private TechComment originParent; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "created_by", nullable = false) + @JoinColumn(name = "created_by", foreignKey = @ForeignKey(name = "fk_tech_comment_03")) private Member createdBy; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "deleted_by") + @JoinColumn(name = "deleted_by", foreignKey = @ForeignKey(name = "fk_tech_comment_04")) private Member deletedBy; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "tech_article_id", nullable = false) + @JoinColumn(name = "created_anonymous_by", foreignKey = @ForeignKey(name = "fk_tech_comment_05")) + private AnonymousMember createdAnonymousBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "deleted_anonymous_by", foreignKey = @ForeignKey(name = "fk_tech_comment_06")) + private AnonymousMember deletedAnonymousBy; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tech_article_id", nullable = false, foreignKey = @ForeignKey(name = "fk_tech_comment_07")) private TechArticle techArticle; @OneToMany(mappedBy = "techComment") @@ -78,7 +97,8 @@ public class TechComment extends BasicTime { @Builder private TechComment(CommentContents contents, Count blameTotalCount, Count recommendTotalCount, Count replyTotalCount, TechComment parent, TechComment originParent, Member createdBy, Member deletedBy, - TechArticle techArticle, LocalDateTime deletedAt) { + AnonymousMember createdAnonymousBy, AnonymousMember deletedAnonymousBy, TechArticle techArticle, + LocalDateTime deletedAt) { this.contents = contents; this.blameTotalCount = blameTotalCount; this.recommendTotalCount = recommendTotalCount; @@ -87,12 +107,14 @@ private TechComment(CommentContents contents, Count blameTotalCount, Count recom this.originParent = originParent; this.createdBy = createdBy; this.deletedBy = deletedBy; + this.createdAnonymousBy = createdAnonymousBy; + this.deletedAnonymousBy = deletedAnonymousBy; this.techArticle = techArticle; this.deletedAt = deletedAt; } - public static TechComment createMainTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle) { + public static TechComment createMainTechCommentByMember(CommentContents contents, Member createdBy, + TechArticle techArticle) { return TechComment.builder() .contents(contents) .createdBy(createdBy) @@ -103,9 +125,21 @@ public static TechComment createMainTechComment(CommentContents contents, Member .build(); } - public static TechComment createRepliedTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, TechComment originParent, - TechComment parent) { + public static TechComment createMainTechCommentByAnonymousMember(CommentContents contents, AnonymousMember createdAnonymousBy, + TechArticle techArticle) { + return TechComment.builder() + .contents(contents) + .createdAnonymousBy(createdAnonymousBy) + .techArticle(techArticle) + .blameTotalCount(Count.defaultCount()) + .recommendTotalCount(Count.defaultCount()) + .replyTotalCount(Count.defaultCount()) + .build(); + } + + public static TechComment createRepliedTechCommentByMember(CommentContents contents, Member createdBy, + TechArticle techArticle, TechComment originParent, + TechComment parent) { return TechComment.builder() .contents(contents) .createdBy(createdBy) @@ -118,11 +152,32 @@ public static TechComment createRepliedTechComment(CommentContents contents, Mem .build(); } + public static TechComment createRepliedTechCommentByAnonymousMember(CommentContents contents, + AnonymousMember createdAnonymousBy, + TechArticle techArticle, TechComment originParent, + TechComment parent) { + return TechComment.builder() + .contents(contents) + .createdAnonymousBy(createdAnonymousBy) + .techArticle(techArticle) + .blameTotalCount(Count.defaultCount()) + .recommendTotalCount(Count.defaultCount()) + .replyTotalCount(Count.defaultCount()) + .originParent(originParent) + .parent(parent) + .build(); + } + public void changeDeletedAt(LocalDateTime deletedAt, Member deletedBy) { this.deletedAt = deletedAt; this.deletedBy = deletedBy; } + public void changeDeletedAt(LocalDateTime deletedAt, AnonymousMember deletedAnonymousBy) { + this.deletedAt = deletedAt; + this.deletedAnonymousBy = deletedAnonymousBy; + } + public void modifyCommentContents(CommentContents contents, LocalDateTime contentsLastModifiedAt) { this.contents = contents; this.contentsLastModifiedAt = contentsLastModifiedAt; @@ -159,4 +214,24 @@ public void incrementBlameTotalCount() { public boolean isEqualsId(Long id) { return this.id.equals(id); } + + public boolean isCreatedAnonymousMember() { + return this.createdBy == null && this.createdAnonymousBy != null; + } + + public boolean isCreatedMember() { + return this.createdBy != null && this.createdAnonymousBy == null; + } + + public boolean isDeletedByMember() { + return this.deletedBy != null; + } + + public boolean isDeletedByAnonymousMember() { + return this.deletedAnonymousBy != null; + } + + public boolean isDeletedByAdmin() { + return this.deletedBy != null && this.deletedBy.isAdmin(); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java new file mode 100644 index 00000000..81b2af22 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/entity/TechKeyword.java @@ -0,0 +1,36 @@ +package com.dreamypatisiel.devdevdev.domain.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(indexes = { + @Index(name = "idx_tech_keyword_01", columnList = "chosung_key"), + @Index(name = "idx_tech_keyword_02", columnList = "jamo_key") +}) +public class TechKeyword extends BasicTime { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100, columnDefinition = "varchar(100) COLLATE utf8mb4_bin") + private String keyword; + + @Column(nullable = false, length = 300, columnDefinition = "varchar(300) COLLATE utf8mb4_bin") + private String jamoKey; + + @Column(nullable = false, length = 150, columnDefinition = "varchar(150) COLLATE utf8mb4_bin") + private String chosungKey; + + @Builder + private TechKeyword(String keyword, String jamoKey, String chosungKey) { + this.keyword = keyword; + this.jamoKey = jamoKey; + this.chosungKey = chosungKey; + } +} \ No newline at end of file diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/exception/NicknameExceptionMessage.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/exception/NicknameExceptionMessage.java new file mode 100644 index 00000000..62eee050 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/exception/NicknameExceptionMessage.java @@ -0,0 +1,5 @@ +package com.dreamypatisiel.devdevdev.domain.exception; + +public class NicknameExceptionMessage { + public static final String NICKNAME_CHANGE_RATE_LIMIT_MESSAGE = "닉네임은 24시간에 한 번만 변경할 수 있습니다."; +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java new file mode 100644 index 00000000..059e0cb5 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/policy/NicknameChangePolicy.java @@ -0,0 +1,14 @@ +package com.dreamypatisiel.devdevdev.domain.policy; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class NicknameChangePolicy { + @Value("${nickname.change.interval.minutes:1440}") + private int nicknameChangeIntervalMinutes; + + public int getNicknameChangeIntervalMinutes() { + return nicknameChangeIntervalMinutes; + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickCommentRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickCommentRepository.java index 85c9ebb9..4dc22c42 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickCommentRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickCommentRepository.java @@ -16,14 +16,18 @@ public interface PickCommentRepository extends JpaRepository, Optional findWithPickByIdAndPickIdAndCreatedByIdAndDeletedAtIsNull(Long id, Long pickId, Long createdById); + @EntityGraph(attributePaths = {"pick"}) + Optional findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull(Long id, Long pickId, + Long createdAnonymousById); + Optional findByIdAndPickIdAndDeletedAtIsNull(Long id, Long pickId); @EntityGraph(attributePaths = {"pick"}) Optional findWithPickByIdAndPickId(Long id, Long pickId); - @EntityGraph(attributePaths = {"createdBy", "deletedBy", "pickVote", "pick", "pick.member", - "pickCommentRecommends"}) - List findWithMemberWithPickWithPickVoteWithPickCommentRecommendsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + @EntityGraph(attributePaths = {"createdBy", "deletedBy", "createdAnonymousBy", "deletedAnonymousBy", "pickVote", "pick", + "pick.member", "pickCommentRecommends"}) + List findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( Set originParentIds); @Modifying diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java index f1deb0a6..d38d138b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/custom/PickCommentRepositoryImpl.java @@ -1,11 +1,13 @@ package com.dreamypatisiel.devdevdev.domain.repository.pick.custom; -import com.dreamypatisiel.devdevdev.domain.entity.PickComment; +import static com.dreamypatisiel.devdevdev.domain.entity.QAnonymousMember.anonymousMember; import static com.dreamypatisiel.devdevdev.domain.entity.QMember.member; import static com.dreamypatisiel.devdevdev.domain.entity.QPick.pick; import static com.dreamypatisiel.devdevdev.domain.entity.QPickComment.pickComment; import static com.dreamypatisiel.devdevdev.domain.entity.QPickOption.pickOption; import static com.dreamypatisiel.devdevdev.domain.entity.QPickVote.pickVote; + +import com.dreamypatisiel.devdevdev.domain.entity.PickComment; import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.repository.comment.MyWrittenCommentDto; @@ -41,7 +43,8 @@ public Slice findOriginParentPickCommentsByCursor(Pageable pageable List contents = query.selectFrom(pickComment) .innerJoin(pickComment.pick, pick).on(pick.id.eq(pickId)) - .innerJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdAnonymousBy, anonymousMember).fetchJoin() .leftJoin(pickComment.pickVote, pickVote).fetchJoin() .leftJoin(pickVote.pickOption, pickOption).fetchJoin() .where(pick.contentStatus.eq(ContentStatus.APPROVAL) @@ -62,7 +65,8 @@ public List findOriginParentPickBestCommentsByPickIdAndOffset(Long return query.selectFrom(pickComment) .innerJoin(pickComment.pick, pick).on(pick.id.eq(pickId)) - .innerJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdBy, member).fetchJoin() + .leftJoin(pickComment.createdAnonymousBy, anonymousMember).fetchJoin() .leftJoin(pickComment.pickVote, pickVote).fetchJoin() .leftJoin(pickVote.pickOption, pickOption).fetchJoin() .where(pick.contentStatus.eq(ContentStatus.APPROVAL) @@ -92,7 +96,8 @@ public SliceCustom findMyWrittenPickCommentsByCursor(Long m Pageable pageable) { // 회원이 작성한 픽픽픽 댓글 조회 List contents = query.select( - new QMyWrittenCommentDto(pick.id, + new QMyWrittenCommentDto( + pick.id, pick.title.title, pickComment.id, Expressions.constant(MyWrittenCommentFilter.PICK.name()), @@ -100,7 +105,9 @@ public SliceCustom findMyWrittenPickCommentsByCursor(Long m pickComment.recommendTotalCount.count, pickComment.createdAt, pickOption.title.title, - pickOption.pickOptionType.stringValue())) + pickOption.pickOptionType.stringValue() + ) + ) .from(pickComment) .leftJoin(pickComment.pickVote, pickVote) .leftJoin(pickVote.pickOption, pickOption) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java new file mode 100644 index 00000000..e2d13a7f --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechArticleDto.java @@ -0,0 +1,16 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class TechArticleDto { + private final TechArticle techArticle; + private final Double score; + + public static TechArticleDto of(TechArticle techArticle, Double score) { + return new TechArticleDto(techArticle, score); + } +} 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/TechCommentRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechCommentRepository.java index ef20d71c..7951ede7 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechCommentRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechCommentRepository.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.domain.repository.techArticle; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.custom.TechCommentRepositoryCustom; import java.util.List; @@ -16,11 +17,13 @@ public interface TechCommentRepository extends JpaRepository, Optional findByIdAndTechArticleIdAndCreatedByIdAndDeletedAtIsNull(Long id, Long techArticleId, Long createdById); + Optional findByIdAndTechArticleIdAndCreatedAnonymousByAndDeletedAtIsNull(Long id, Long techArticleId, + AnonymousMember createdAnonymousBy); + Optional findByIdAndTechArticleIdAndDeletedAtIsNull(Long id, Long techArticleId); - @EntityGraph(attributePaths = {"createdBy", "deletedBy", "techArticle"}) - List findWithMemberWithTechArticleByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( - Set originParentIds); + @EntityGraph(attributePaths = {"createdBy", "deletedBy", "createdAnonymousBy", "deletedAnonymousBy", "techArticle"}) + List findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull(Set originParentIds); Long countByTechArticleIdAndOriginParentIsNullAndParentIsNullAndDeletedAtIsNull(Long techArticleId); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechKeywordRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechKeywordRepository.java new file mode 100644 index 00000000..52d7bc15 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/TechKeywordRepository.java @@ -0,0 +1,8 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.TechKeyword; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.custom.TechKeywordRepositoryCustom; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TechKeywordRepository extends JpaRepository, TechKeywordRepositoryCustom { +} \ No newline at end of file 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..e9341776 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,17 @@ 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 com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; 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, Double 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..2b9439b4 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,43 @@ 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.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; +import com.querydsl.core.Tuple; 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 +51,101 @@ 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, Double score + ) { + // 키워드가 있는 경우 FULLTEXT 검색, 없는 경우 일반 조회 + if (StringUtils.hasText(keyword)) { + return findTechArticlesByCursorWithKeyword(pageable, techArticleId, techArticleSort, companyId, keyword, score); + } + return findTechArticlesByCursorWithoutKeyword(pageable, techArticleId, techArticleSort, companyId); + } + + // 키워드 검색 + public SliceCustom findTechArticlesByCursorWithKeyword(Pageable pageable, Long techArticleId, + TechArticleSort techArticleSort, Long companyId, + String keyword, Double 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, + "(LEAST({0}, 100000) * 2.0) + LEAST({1}, 100000)", titleScore, contentsScore + ); + + // TechArticle과 score를 함께 조회 + List results = query.select(techArticle, totalScore) + .from(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(); + + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of( + result.get(techArticle), result.get(totalScore))) + .toList(); + + // 키워드 검색 결과 총 갯수 + 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 results = query.selectFrom(techArticle) + .where(getCursorConditionFromTechArticleSort(techArticleSort, techArticleId)) + .where(getCompanyIdCondition(companyId)) + .orderBy(techArticleSort(techArticleSort), techArticle.id.desc()) + .limit(pageable.getPageSize()) + .fetch(); + + // Tuple을 TechArticleDto로 변환 + List contents = results.stream() + .map(result -> TechArticleDto.of(result, null)) + .toList(); + + // 기술블로그 총 갯수 + 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 +176,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, + Double score, NumberTemplate totalScore) { + if (ObjectUtils.isEmpty(techArticleId) || ObjectUtils.isEmpty(score)) { + return null; + } + + // HIGHEST_SCORE(정확도순)인 경우 스코어 기반 커서 사용 + if (techArticleSort == TechArticleSort.HIGHEST_SCORE || ObjectUtils.isEmpty(techArticleSort)) { + 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 || ObjectUtils.isEmpty(techArticleSort)) { + return totalScore.desc(); + } + + // 다른 정렬 방식인 경우 기존 정렬 사용 + return Optional.ofNullable(techArticleSort) + .orElse(TechArticleSort.LATEST).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/TechCommentRepositoryImpl.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechCommentRepositoryImpl.java index 581eed00..5d09c48c 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechCommentRepositoryImpl.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechCommentRepositoryImpl.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.domain.repository.techArticle.custom; +import static com.dreamypatisiel.devdevdev.domain.entity.QAnonymousMember.anonymousMember; import static com.dreamypatisiel.devdevdev.domain.entity.QMember.member; import static com.dreamypatisiel.devdevdev.domain.entity.QTechArticle.techArticle; import static com.dreamypatisiel.devdevdev.domain.entity.QTechComment.techComment; @@ -34,7 +35,8 @@ public Slice findOriginParentTechCommentsByCursor(Long techArticleI TechCommentSort techCommentSort, Pageable pageable) { List contents = query.selectFrom(techComment) .innerJoin(techComment.techArticle, techArticle).on(techArticle.id.eq(techArticleId)) - .innerJoin(techComment.createdBy, member).fetchJoin() + .leftJoin(techComment.createdBy, member).fetchJoin() + .leftJoin(techComment.createdAnonymousBy, anonymousMember).fetchJoin() .where(techComment.parent.isNull() .and(techComment.originParent.isNull()) .and(getCursorCondition(techCommentSort, techCommentId)) @@ -51,7 +53,8 @@ public List findOriginParentTechBestCommentsByTechArticleIdAndOffse return query.selectFrom(techComment) .innerJoin(techComment.techArticle, techArticle).on(techArticle.id.eq(techArticleId)) - .innerJoin(techComment.createdBy, member).fetchJoin() + .leftJoin(techComment.createdBy, member).fetchJoin() + .leftJoin(techComment.createdAnonymousBy, anonymousMember).fetchJoin() .where(techComment.parent.isNull() .and(techComment.originParent.isNull()) .and(techComment.deletedAt.isNull()) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryCustom.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryCustom.java new file mode 100644 index 00000000..6e85b03a --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryCustom.java @@ -0,0 +1,10 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle.custom; + +import com.dreamypatisiel.devdevdev.domain.entity.TechKeyword; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface TechKeywordRepositoryCustom { + List searchKeyword(String inputJamo, String inputChosung, Pageable pageable); +} 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 new file mode 100644 index 00000000..044fe3a3 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java @@ -0,0 +1,58 @@ +package com.dreamypatisiel.devdevdev.domain.repository.techArticle.custom; + +import com.dreamypatisiel.devdevdev.domain.entity.TechKeyword; +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 lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static com.dreamypatisiel.devdevdev.domain.entity.QTechKeyword.techKeyword; + +@RequiredArgsConstructor +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 + public List searchKeyword(String inputJamo, String inputChosung, Pageable pageable) { + BooleanExpression jamoMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techKeyword.jamoKey, inputJamo + ); + + BooleanExpression chosungMatch = Expressions.booleanTemplate( + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1}) > 0.0", + techKeyword.chosungKey, inputChosung + ); + + // 스코어 계산을 위한 expression (Natural Language Mode) + NumberTemplate jamoScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", + techKeyword.jamoKey, inputJamo + ); + NumberTemplate chosungScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_NL_FUNCTION + "', {0}, {1})", + techKeyword.chosungKey, inputChosung + ); + + return query + .selectFrom(techKeyword) + .where(jamoMatch.or(chosungMatch)) + .orderBy( + // 더 높은 스코어를 우선으로 정렬 + Expressions.numberTemplate(Double.class, + "GREATEST({0}, {1})", jamoScore, chosungScore).desc(), + // 동일한 스코어라면 키워드 길이가 짧은 것을 우선으로 정렬 + techKeyword.keyword.length().asc() + ) + .limit(pageable.getPageSize()) + .fetch(); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java index 0cd23599..08bd6dfa 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/AnonymousMemberService.java @@ -1,16 +1,17 @@ package com.dreamypatisiel.devdevdev.domain.service.member; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_ANONYMOUS_MEMBER_ID_MESSAGE; + import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.repository.member.AnonymousMemberRepository; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; -import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_ANONYMOUS_MEMBER_ID_MESSAGE; - -@Transactional(readOnly = true) @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class AnonymousMemberService { @@ -21,9 +22,27 @@ public AnonymousMember findOrCreateAnonymousMember(String anonymousMemberId) { // 익명 사용자 검증 validateAnonymousMemberId(anonymousMemberId); - // 익명회원 조회 또는 생성 - return anonymousMemberRepository.findByAnonymousMemberId(anonymousMemberId) - .orElseGet(() -> anonymousMemberRepository.save(AnonymousMember.create(anonymousMemberId))); + // 익명회원 조회 + Optional optionalAnonymousMember = anonymousMemberRepository.findByAnonymousMemberId(anonymousMemberId); + + // 익명 사용자 닉네임 생성 + String anonymousNickName = "익명의 댑댑이 " + System.nanoTime() % 100_000L; + + // 익명 사용자가 존재하지 않으면 + if (optionalAnonymousMember.isEmpty()) { + // 익명 사용자 생성 + AnonymousMember anonymousMember = AnonymousMember.create(anonymousMemberId, anonymousNickName); + return anonymousMemberRepository.save(anonymousMember); + } + + AnonymousMember anonymousMember = optionalAnonymousMember.get(); + + // 익명 사용자가 존재하지만 닉네임이 없다면 + if (!anonymousMember.hasNickName()) { + anonymousMember.changeNickname(anonymousNickName); + } + + return anonymousMember; } private void validateAnonymousMemberId(String anonymousMemberId) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberNicknameDictionaryService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberNicknameDictionaryService.java index 0cfc4c1a..466769a6 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberNicknameDictionaryService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/member/MemberNicknameDictionaryService.java @@ -20,6 +20,7 @@ public class MemberNicknameDictionaryService { public static final String NOT_FOUND_WORD_EXCEPTION_MESSAGE = "랜덤 닉네임 생성을 위한 단어가 없습니다."; + public static final String SPACE = " "; private final MemberNicknameDictionaryRepository memberNicknameDictionaryRepository; @@ -44,6 +45,6 @@ private MemberNicknameDictionary findRandomWordByWordType(WordType wordType) { private String concatNickname(List words) { return words.stream() .map(word -> word.getWord().getWord()) - .collect(Collectors.joining(" ")); + .collect(Collectors.joining(SPACE)); } } \ No newline at end of file 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 eeb92828..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 @@ -11,6 +11,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.SurveyVersionQuestionMapper; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.embedded.CustomSurveyAnswer; +import com.dreamypatisiel.devdevdev.domain.policy.NicknameChangePolicy; import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.comment.CommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.comment.MyWrittenCommentDto; @@ -21,8 +22,7 @@ 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; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; @@ -36,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; @@ -51,6 +49,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -61,12 +61,12 @@ 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; private final CommentRepository commentRepository; private final CompanyRepository companyRepository; + private final NicknameChangePolicy nicknameChangePolicy; /** * 회원 탈퇴 회원의 북마크와 회원 정보를 삭제합니다. @@ -213,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()); } /** @@ -295,4 +278,31 @@ public SliceCustom findMySubscribedCompanies(Pageable return new SliceCustom<>(subscribedCompanyResponses, pageable, subscribedCompanies.getTotalElements()); } + + /** + * @Note: 유저의 닉네임을 변경합니다. 설정된 제한 시간 이내에 변경한 이력이 있다면 닉네임 변경이 불가능합니다. + * @Author: 유소영 + * @Since: 2025.07.03 + */ + @Transactional + public String changeNickname(String nickname, Authentication authentication) { + Member member = memberProvider.getMemberByAuthentication(authentication); + + if (!member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow())) { + throw new NicknameException(NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); + } + + member.changeNickname(nickname, timeProvider.getLocalDateTimeNow()); + return member.getNicknameAsString(); + } + + /** + * @Note: 유저가 닉네임을 변경할 수 있는지 여부를 반환합니다. + * @Author: 유소영 + * @Since: 2025.07.06 + */ + public boolean canChangeNickname(Authentication authentication) { + Member member = memberProvider.getMemberByAuthentication(authentication); + return member.canChangeNickname(nicknameChangePolicy.getNicknameChangeIntervalMinutes(), timeProvider.getLocalDateTimeNow()); + } } 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/pick/GuestPickCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentService.java index 94de9ce2..67ae7ce3 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentService.java @@ -4,21 +4,22 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.policy.PickBestCommentsPolicy; +import com.dreamypatisiel.devdevdev.domain.policy.PickPopularScorePolicy; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; import java.util.EnumSet; import java.util.List; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; @@ -32,24 +33,24 @@ public class GuestPickCommentService extends PickCommonService implements PickCo public GuestPickCommentService(EmbeddingsService embeddingsService, PickBestCommentsPolicy pickBestCommentsPolicy, + TimeProvider timeProvider, PickRepository pickRepository, + PickPopularScorePolicy pickPopularScorePolicy, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); } @Override - public PickCommentResponse registerPickComment(Long pickId, RegisterPickCommentRequest pickMainCommentRequest, - Authentication authentication) { + public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @Override public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, - Long pickId, - RegisterPickRepliedCommentRequest pickSubCommentRequest, + Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); @@ -57,14 +58,15 @@ public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, @Override public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, - ModifyPickCommentRequest modifyPickCommentRequest, + PickCommentDto pickModifyCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @Override - public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { + public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, + @Nullable String anonymousMemberId, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -77,18 +79,18 @@ public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Au public SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, PickCommentSort pickCommentSort, EnumSet pickOptionTypes, + String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); // 픽픽픽 댓글/답글 조회 - return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, null); + return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, null, null); } @Override - public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, - Authentication authentication) { + public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -99,11 +101,11 @@ public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickC * @Since: 2024.10.09 */ @Override - public List findPickBestComments(int size, Long pickId, + public List findPickBestComments(int size, Long pickId, String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - return super.findPickBestComments(size, pickId, null); + return super.findPickBestComments(size, pickId, null, null); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java new file mode 100644 index 00000000..3c033729 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2.java @@ -0,0 +1,240 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick; + +import static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_VOTE_MESSAGE; + +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.PickComment; +import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; +import com.dreamypatisiel.devdevdev.domain.policy.PickBestCommentsPolicy; +import com.dreamypatisiel.devdevdev.domain.policy.PickPopularScorePolicy; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRecommendRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; +import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; +import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; +import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentRecommendResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; +import java.util.EnumSet; +import java.util.List; +import javax.annotation.Nullable; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class GuestPickCommentServiceV2 extends PickCommonService implements PickCommentService { + + private final AnonymousMemberService anonymousMemberService; + + private final PickVoteRepository pickVoteRepository; + + public GuestPickCommentServiceV2(EmbeddingsService embeddingsService, + PickBestCommentsPolicy pickBestCommentsPolicy, + TimeProvider timeProvider, + PickRepository pickRepository, + PickCommentRepository pickCommentRepository, + PickCommentRecommendRepository pickCommentRecommendRepository, + AnonymousMemberService anonymousMemberService, + PickPopularScorePolicy pickPopularScorePolicy, + PickVoteRepository pickVoteRepository) { + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); + this.anonymousMemberService = anonymousMemberService; + this.pickVoteRepository = pickVoteRepository; + } + + @Override + @Transactional + public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); + String contents = pickCommentDto.getContents(); + Boolean isPickVotePublic = pickCommentDto.getIsPickVotePublic(); + + // 익명 회원 추출 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 조회 + Pick findPick = pickRepository.findById(pickId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_MESSAGE)); + + // 댓글 갯수 증가 및 인기점수 반영 + findPick.incrementCommentTotalCount(); + findPick.changePopularScore(pickPopularScorePolicy); + + // 픽픽픽 게시글의 승인 상태 검증 + validateIsApprovalPickContentStatus(findPick, INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, REGISTER); + + // 픽픽픽 선택지 투표 공개인 경우 + if (isPickVotePublic) { + // 익명회원이 투표한 픽픽픽 투표 조회 + PickVote findPickVote = pickVoteRepository.findWithPickAndPickOptionByPickIdAndAnonymousMemberAndDeletedAtIsNull( + pickId, findAnonymousMember) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE)); + + // 픽픽픽 투표한 픽 옵션의 댓글 작성 + PickComment pickComment = PickComment.createPublicVoteCommentByAnonymousMember(new CommentContents(contents), + findAnonymousMember, findPick, findPickVote); + pickCommentRepository.save(pickComment); + + return new PickCommentResponse(pickComment.getId()); + } + + // 픽픽픽 선택지 투표 비공개인 경우 + PickComment pickComment = PickComment.createPrivateVoteCommentByAnonymousMember(new CommentContents(contents), + findAnonymousMember, findPick); + pickCommentRepository.save(pickComment); + + return new PickCommentResponse(pickComment.getId()); + } + + @Override + @Transactional + public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, + Long pickId, PickCommentDto pickRegisterRepliedCommentDto, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String contents = pickRegisterRepliedCommentDto.getContents(); + String anonymousMemberId = pickRegisterRepliedCommentDto.getAnonymousMemberId(); + + // 익명 회원 추출 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 댓글 로직 수행 + PickReplyContext pickReplyContext = prepareForReplyRegistration(pickParentCommentId, pickCommentOriginParentId, pickId); + + PickComment findParentPickComment = pickReplyContext.parentPickComment(); + PickComment findOriginParentPickComment = pickReplyContext.originParentPickComment(); + Pick findPick = pickReplyContext.pick(); + + // 픽픽픽 서브 댓글(답글) 생성 + PickComment pickRepliedComment = PickComment.createRepliedCommentByAnonymousMember(new CommentContents(contents), + findParentPickComment, findOriginParentPickComment, findAnonymousMember, findPick); + pickCommentRepository.save(pickRepliedComment); + + return new PickCommentResponse(pickRepliedComment.getId()); + } + + @Override + @Transactional + public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickCommentDto pickCommentDto, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String contents = pickCommentDto.getContents(); + String anonymousMemberId = pickCommentDto.getAnonymousMemberId(); + + // 익명 회원 추출 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 댓글 조회(익명 회원 본인이 댓글 작성, 삭제되지 않은 댓글) + PickComment findPickComment = pickCommentRepository.findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull( + pickCommentId, pickId, findAnonymousMember.getId()) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); + + // 픽픽픽 게시글의 승인 상태 검증 + validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); + + // 댓글 수정 + findPickComment.modifyCommentContents(new CommentContents(contents), timeProvider.getLocalDateTimeNow()); + + return new PickCommentResponse(findPickComment.getId()); + } + + @Override + @Transactional + public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, @Nullable String anonymousMemberId, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명 회원 추출 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 댓글 조회(회원 본인이 댓글 작성, 삭제되지 않은 댓글) + PickComment findPickComment = pickCommentRepository.findWithPickByIdAndPickIdAndCreatedAnonymousByIdAndDeletedAtIsNull( + pickCommentId, pickId, findAnonymousMember.getId()) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); + + // 픽픽픽 게시글의 승인 상태 검증 + validateIsApprovalPickContentStatus(findPickComment.getPick(), INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, DELETE); + + // 소프트 삭제 + findPickComment.changeDeletedAtByAnonymousMember(timeProvider.getLocalDateTimeNow(), findAnonymousMember); + + return new PickCommentResponse(findPickComment.getId()); + } + + /** + * @Note: 정렬 조건에 따라서 커서 방식으로 픽픽픽 댓글/답글을 조회한다. + * @Author: 장세웅 + * @Since: 2025.07.13 + */ + @Override + @Transactional + public SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, + PickCommentSort pickCommentSort, + EnumSet pickOptionTypes, + String anonymousMemberId, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명 회원 추출 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 픽픽픽 댓글/답글 조회 + return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, null, anonymousMember); + } + + @Override + public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, Authentication authentication) { + + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + /** + * @Note: 익명회윈이 픽픽픽 베스트 댓글을 조회한다. + * @Author: 장세웅 + * @Since: 2024.10.09 + */ + @Override + @Transactional + public List findPickBestComments(int size, Long pickId, String anonymousMemberId, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명 회원 추출 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + return super.findPickBestComments(size, pickId, null, findAnonymousMember); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickService.java index 9274b365..afc2f82d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickService.java @@ -55,9 +55,7 @@ public class GuestPickService extends PickCommonService implements PickService { public static final String INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE = "비회원은 현재 해당 기능을 이용할 수 없습니다."; - private final PickPopularScorePolicy pickPopularScorePolicy; private final PickVoteRepository pickVoteRepository; - private final TimeProvider timeProvider; private final AnonymousMemberService anonymousMemberService; public GuestPickService(PickRepository pickRepository, EmbeddingsService embeddingsService, @@ -67,12 +65,10 @@ public GuestPickService(PickRepository pickRepository, EmbeddingsService embeddi PickPopularScorePolicy pickPopularScorePolicy, PickVoteRepository pickVoteRepository, TimeProvider timeProvider, AnonymousMemberService anonymousMemberService) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); - this.pickPopularScorePolicy = pickPopularScorePolicy; + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.pickVoteRepository = pickVoteRepository; this.anonymousMemberService = anonymousMemberService; - this.timeProvider = timeProvider; } @Transactional diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentService.java index 2e9cede0..60139b5b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentService.java @@ -1,9 +1,7 @@ package com.dreamypatisiel.devdevdev.domain.service.pick; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_CAN_NOT_ACTION_DELETED_PICK_COMMENT_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_VOTE_MESSAGE; @@ -22,20 +20,19 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; import java.util.EnumSet; import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; @@ -45,14 +42,7 @@ @Transactional(readOnly = true) public class MemberPickCommentService extends PickCommonService implements PickCommentService { - public static final String MODIFY = "수정"; - public static final String REGISTER = "작성"; - public static final String DELETE = "삭제"; - public static final String RECOMMEND = "추천"; - - private final TimeProvider timeProvider; private final MemberProvider memberProvider; - private final PickPopularScorePolicy pickPopularScorePolicy; private final PickRepository pickRepository; private final PickVoteRepository pickVoteRepository; @@ -64,11 +54,9 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member PickRepository pickRepository, PickVoteRepository pickVoteRepository, PickCommentRepository pickCommentRepository, PickCommentRecommendRepository pickCommentRecommendRepository) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, - pickCommentRecommendRepository); - this.timeProvider = timeProvider; + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.memberProvider = memberProvider; - this.pickPopularScorePolicy = pickPopularScorePolicy; this.pickRepository = pickRepository; this.pickVoteRepository = pickVoteRepository; this.pickCommentRepository = pickCommentRepository; @@ -79,13 +67,12 @@ public MemberPickCommentService(TimeProvider timeProvider, MemberProvider member * @Author: 장세웅 * @Since: 2024.08.23 */ + @Override @Transactional - public PickCommentResponse registerPickComment(Long pickId, - RegisterPickCommentRequest pickMainCommentRequest, - Authentication authentication) { + public PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickCommentDto, Authentication authentication) { - String contents = pickMainCommentRequest.getContents(); - Boolean isPickVotePublic = pickMainCommentRequest.getIsPickVotePublic(); + String contents = pickCommentDto.getContents(); + Boolean isPickVotePublic = pickCommentDto.getIsPickVotePublic(); // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -93,6 +80,7 @@ public PickCommentResponse registerPickComment(Long pickId, // 픽픽픽 조회 Pick findPick = pickRepository.findById(pickId) .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_MESSAGE)); + // 댓글 갯수 증가 및 인기점수 반영 findPick.incrementCommentTotalCount(); findPick.changePopularScore(pickPopularScorePolicy); @@ -108,7 +96,7 @@ public PickCommentResponse registerPickComment(Long pickId, .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE)); // 픽픽픽 투표한 픽 옵션의 댓글 작성 - PickComment pickComment = PickComment.createPublicVoteComment(new CommentContents(contents), + PickComment pickComment = PickComment.createPublicVoteCommentByMember(new CommentContents(contents), findMember, findPick, findPickVote); pickCommentRepository.save(pickComment); @@ -116,7 +104,7 @@ public PickCommentResponse registerPickComment(Long pickId, } // 픽픽픽 선택지 투표 비공개인 경우 - PickComment pickComment = PickComment.createPrivateVoteComment(new CommentContents(contents), findMember, + PickComment pickComment = PickComment.createPrivateVoteCommentByMember(new CommentContents(contents), findMember, findPick); pickCommentRepository.save(pickComment); @@ -128,77 +116,46 @@ public PickCommentResponse registerPickComment(Long pickId, * @Author: 장세웅 * @Since: 2024.08.24 */ + @Override @Transactional public PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, Long pickId, - RegisterPickRepliedCommentRequest pickSubCommentRequest, + PickCommentDto pickCommentDto, Authentication authentication) { - String contents = pickSubCommentRequest.getContents(); + String contents = pickCommentDto.getContents(); // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); - // 답글 대상의 픽픽픽 댓글 조회 - PickComment findParentPickComment = pickCommentRepository.findWithPickByIdAndPickId(pickParentCommentId, pickId) - .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); - - // 픽픽픽 게시글의 승인 상태 검증 - Pick findPick = findParentPickComment.getPick(); - validateIsApprovalPickContentStatus(findPick, INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE, - REGISTER); - // 댓글 총 갯수 증가 및 인기점수 반영 - findPick.incrementCommentTotalCount(); - findPick.changePopularScore(pickPopularScorePolicy); + // 픽픽픽 댓글 로직 수행 + PickReplyContext pickReplyContext = prepareForReplyRegistration(pickParentCommentId, pickCommentOriginParentId, pickId); - // 픽픽픽 최초 댓글 검증 및 반환 - PickComment findOriginParentPickComment = getAndValidateOriginParentPickComment( - pickCommentOriginParentId, findParentPickComment); - // 픽픽픽 최초 댓글의 답글 갯수 증가 - findOriginParentPickComment.incrementReplyTotalCount(); + PickComment findParentPickComment = pickReplyContext.parentPickComment(); + PickComment findOriginParentPickComment = pickReplyContext.originParentPickComment(); + Pick findPick = pickReplyContext.pick(); // 픽픽픽 서브 댓글(답글) 생성 - PickComment pickRepliedComment = PickComment.createRepliedComment(new CommentContents(contents), + PickComment pickRepliedComment = PickComment.createRepliedCommentByMember(new CommentContents(contents), findParentPickComment, findOriginParentPickComment, findMember, findPick); pickCommentRepository.save(pickRepliedComment); return new PickCommentResponse(pickRepliedComment.getId()); } - private PickComment getAndValidateOriginParentPickComment(Long pickCommentOriginParentId, - PickComment parentPickComment) { - - // 픽픽픽 답글 대상의 댓글이 삭제 상태이면 - validateIsDeletedPickComment(parentPickComment, INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); - - // 픽픽픽 답글 대상의 댓글이 최초 댓글이면 - if (parentPickComment.isEqualsId(pickCommentOriginParentId)) { - return parentPickComment; - } - - // 픽픽픽 답글 대상의 댓글의 메인 댓글 조회 - PickComment findOriginParentPickComment = pickCommentRepository.findById(pickCommentOriginParentId) - .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); - - // 픽픽픽 최초 댓글이 삭제 상태이면 - validateIsDeletedPickComment(findOriginParentPickComment, INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, - REGISTER); - - return findOriginParentPickComment; - } - /** * @Note: 회원 자신이 작성한 픽픽픽 댓글/답글을 수정한다. 픽픽픽 공개 여부는 수정할 수 없다. * @Author: 장세웅 * @Since: 2024.08.10 */ + @Override @Transactional public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, - ModifyPickCommentRequest modifyPickCommentRequest, + PickCommentDto pickModifyCommentDto, Authentication authentication) { - String contents = modifyPickCommentRequest.getContents(); + String contents = pickModifyCommentDto.getContents(); // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -223,8 +180,10 @@ public PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, * @Author: 장세웅 * @Since: 2024.08.11 */ + @Override @Transactional - public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication) { + public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, @Nullable String anonymousMemberId, + Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -237,7 +196,7 @@ public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Au .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); // 소프트 삭제 - findPickComment.changeDeletedAt(timeProvider.getLocalDateTimeNow(), findMember); + findPickComment.changeDeletedAtByMember(timeProvider.getLocalDateTimeNow(), findMember); return new PickCommentResponse(findPickComment.getId()); } @@ -252,7 +211,7 @@ public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Au DELETE); // 소프트 삭제 - findPickComment.changeDeletedAt(timeProvider.getLocalDateTimeNow(), findMember); + findPickComment.changeDeletedAtByMember(timeProvider.getLocalDateTimeNow(), findMember); return new PickCommentResponse(findPickComment.getId()); } @@ -263,17 +222,19 @@ public PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Au * @Author: 장세웅 * @Since: 2024.08.25 */ + @Override public SliceCommentCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, PickCommentSort pickCommentSort, EnumSet pickOptionTypes, + String anonymousMemberId, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); // 픽픽픽 댓글/답글 조회 - return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, findMember); + return super.findPickComments(pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, findMember, null); } /** @@ -281,6 +242,7 @@ public SliceCommentCustom findPickComments(Pageable pageab * @Author: 장세웅 * @Since: 2024.09.07 */ + @Override @Transactional public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, Authentication authentication) { @@ -307,12 +269,13 @@ public PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickC * @Since: 2024.10.09 */ @Override - public List findPickBestComments(int size, Long pickId, Authentication authentication) { + public List findPickBestComments(int size, Long pickId, String anonymousMemberId, + Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); - return super.findPickBestComments(size, pickId, findMember); + return super.findPickBestComments(size, pickId, findMember, null); } private PickCommentRecommendResponse toggleOrCreatePickCommentRecommend(PickComment pickComment, Member member) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickService.java index 3692fa9c..d907a0fb 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickService.java @@ -85,8 +85,6 @@ public class MemberPickService extends PickCommonService implements PickService private final PickOptionRepository pickOptionRepository; private final PickOptionImageRepository pickOptionImageRepository; private final PickVoteRepository pickVoteRepository; - private final PickPopularScorePolicy pickPopularScorePolicy; - private final TimeProvider timeProvider; public MemberPickService(EmbeddingsService embeddingsService, PickRepository pickRepository, AwsS3Properties awsS3Properties, AwsS3Uploader awsS3Uploader, @@ -96,7 +94,8 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic PickCommentRecommendRepository pickCommentRecommendRepository, PickPopularScorePolicy pickPopularScorePolicy, PickBestCommentsPolicy pickBestCommentsPolicy, TimeProvider timeProvider) { - super(embeddingsService, pickBestCommentsPolicy, pickRepository, pickCommentRepository, + super(embeddingsService, pickBestCommentsPolicy, pickPopularScorePolicy, timeProvider, pickRepository, + pickCommentRepository, pickCommentRecommendRepository); this.awsS3Properties = awsS3Properties; this.awsS3Uploader = awsS3Uploader; @@ -104,8 +103,6 @@ public MemberPickService(EmbeddingsService embeddingsService, PickRepository pic this.pickOptionRepository = pickOptionRepository; this.pickOptionImageRepository = pickOptionImageRepository; this.pickVoteRepository = pickVoteRepository; - this.pickPopularScorePolicy = pickPopularScorePolicy; - this.timeProvider = timeProvider; } /** @@ -129,14 +126,12 @@ public Slice findPicksMain(Pageable pageable, Long pickId, Pic } /** - * @Note: 이미지 업로드와 DB 저장을 하나의 작업(Transcation)으로 묶어서, 데이터 정합성을 유지한다.
이때 pick_option_id는 null 인 상태 - * 입니다.

+ * @Note: 이미지 업로드와 DB 저장을 하나의 작업(Transcation)으로 묶어서, 데이터 정합성을 유지한다.
이때 pick_option_id는 null 인 상태 입니다.

*

- * 이미지 업로드 실패시 IOException이 발생할 수 있는데, 이때 catch로 처리하여 데이터 정합성 유지합니다.
즉, IOException이 발생해도 rollback하지 않는다는 의미 - * 입니다.

+ * 이미지 업로드 실패시 IOException이 발생할 수 있는데, 이때 catch로 처리하여 데이터 정합성 유지합니다.
즉, IOException이 발생해도 rollback하지 않는다는 의미 입니다. + *

*

- * 단, Transcation이 길게 유지되면 추후 DB Connection을 오랫동안 유지하기 때문에 많은 트래픽이 발생할 때 DB Connection이 부족해지는 현상이 발생할 수 - * 있습니다.

+ * 단, Transcation이 길게 유지되면 추후 DB Connection을 오랫동안 유지하기 때문에 많은 트래픽이 발생할 때 DB Connection이 부족해지는 현상이 발생할 수 있습니다.

*

* (Transcation은 기본적으로 RuntimeException에 대해서만 Rollback 합니다. AmazonClient의 putObject(...)는 RuntimeException을 * 발생시킵니다.)

@@ -282,8 +277,8 @@ public PickDetailResponse findPickDetail(Long pickId, String anonymousMemberId, } /** - * @Note: member 1:N pick 1:N pickOption 1:N pickVote
pick 1:N pickVote N:1 member
연관관계가 다소 복잡하니, 직접 - * ERD를 확인하는 것을 권장합니다.
투표 이력이 있는 경우 - 투표 이력이 없는 경우 + * @Note: member 1:N pick 1:N pickOption 1:N pickVote
pick 1:N pickVote N:1 member
연관관계가 다소 복잡하니, 직접 ERD를 확인하는 것을 + * 권장합니다.
투표 이력이 있는 경우 - 투표 이력이 없는 경우 * @Author: ralph * @Since: 2024.05.29 */ diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommentService.java index 885acdaa..c08533ad 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommentService.java @@ -2,42 +2,41 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; import java.util.EnumSet; import java.util.List; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.security.core.Authentication; public interface PickCommentService { - PickCommentResponse registerPickComment(Long pickId, - RegisterPickCommentRequest pickMainCommentRequest, - Authentication authentication); - - PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, - Long pickCommentOriginParentId, - Long pickId, - RegisterPickRepliedCommentRequest pickSubCommentRequest, + String MODIFY = "수정"; + String REGISTER = "작성"; + String DELETE = "삭제"; + String RECOMMEND = "추천"; + + PickCommentResponse registerPickComment(Long pickId, PickCommentDto pickRegisterCommentDto, Authentication authentication); + + PickCommentResponse registerPickRepliedComment(Long pickParentCommentId, Long pickCommentOriginParentId, + Long pickId, PickCommentDto pickRegisterRepliedCommentDto, Authentication authentication); - PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, - ModifyPickCommentRequest modifyPickCommentRequest, + PickCommentResponse modifyPickComment(Long pickCommentId, Long pickId, PickCommentDto pickModifyCommentDto, Authentication authentication); - PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, Authentication authentication); + PickCommentResponse deletePickComment(Long pickCommentId, Long pickId, @Nullable String anonymousMemberId, + Authentication authentication); - SliceCustom findPickComments(Pageable pageable, Long pickId, - Long pickCommentId, PickCommentSort pickCommentSort, - EnumSet pickOptionTypes, - Authentication authentication); + SliceCustom findPickComments(Pageable pageable, Long pickId, Long pickCommentId, + PickCommentSort pickCommentSort, EnumSet pickOptionTypes, + String anonymousMemberId, Authentication authentication); - PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, - Authentication authentication); + PickCommentRecommendResponse recommendPickComment(Long pickId, Long pickCommendId, Authentication authentication); - List findPickBestComments(int size, Long pickId, Authentication authentication); + List findPickBestComments(int size, Long pickId, String anonymousMemberId, + Authentication authentication); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommonService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommonService.java index ff11ab6d..2ee55318 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommonService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickCommonService.java @@ -1,20 +1,27 @@ package com.dreamypatisiel.devdevdev.domain.service.pick; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickCommentService.REGISTER; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.Pick; import com.dreamypatisiel.devdevdev.domain.entity.PickComment; import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.policy.PickBestCommentsPolicy; +import com.dreamypatisiel.devdevdev.domain.policy.PickPopularScorePolicy; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.exception.InternalServerException; import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.openai.data.response.PickWithSimilarityDto; import com.dreamypatisiel.devdevdev.openai.embeddings.EmbeddingsService; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; @@ -45,7 +52,9 @@ public class PickCommonService { private final EmbeddingsService embeddingsService; private final PickBestCommentsPolicy pickBestCommentsPolicy; + protected final PickPopularScorePolicy pickPopularScorePolicy; + protected final TimeProvider timeProvider; protected final PickRepository pickRepository; protected final PickCommentRepository pickCommentRepository; protected final PickCommentRecommendRepository pickCommentRecommendRepository; @@ -102,7 +111,8 @@ protected SliceCommentCustom findPickComments(Pageable pag Long pickCommentId, PickCommentSort pickCommentSort, EnumSet pickOptionTypes, - @Nullable Member member) { + @Nullable Member member, + @Nullable AnonymousMember anonymousMember) { // 픽픽픽 최상위 댓글 조회 Slice findOriginParentPickComments = pickCommentRepository.findOriginParentPickCommentsByCursor( @@ -116,14 +126,14 @@ protected SliceCommentCustom findPickComments(Pageable pag // 픽픽픽 최상위 댓글의 답글 조회(최상위 댓글의 아이디가 key) Map> pickCommentReplies = pickCommentRepository - .findWithMemberWithPickWithPickVoteWithPickCommentRecommendsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + .findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( originParentIds).stream() .collect(Collectors.groupingBy(pickCommentReply -> pickCommentReply.getOriginParent().getId())); // 픽픽픽 댓글/답글 응답 생성 List pickCommentsResponse = originParentPickComments.stream() - .map(originParentPickComment -> getPickCommentsResponse(member, originParentPickComment, - pickCommentReplies)) + .map(originParentPickComment -> getPickCommentsResponse(member, anonymousMember, + originParentPickComment, pickCommentReplies)) .toList(); // 픽픽픽 최상위 댓글 추출 @@ -143,7 +153,8 @@ protected SliceCommentCustom findPickComments(Pageable pag // pickOptionTypes 필터링이 없으면 if (ObjectUtils.isEmpty(pickOptionTypes)) { // 픽픽픽에서 전체 댓글 추출 - Long pickCommentTotalCount = originParentPickComment.getPick().getCommentTotalCount().getCount(); + Pick pick = originParentPickComment.getPick(); + Long pickCommentTotalCount = pick.getCommentTotalCount().getCount(); return new SliceCommentCustom<>(pickCommentsResponse, pageable, findOriginParentPickComments.hasNext(), pickCommentTotalCount, pickOriginCommentsIsNotDeleted); @@ -170,7 +181,8 @@ private Long getPickCommentTotalCountBy(Long pickId, EnumSet pic return allOriginParentPickCommentIds.size() + childCommentCount; } - private PickCommentsResponse getPickCommentsResponse(Member member, PickComment originPickComment, + private PickCommentsResponse getPickCommentsResponse(Member member, AnonymousMember anonymousMember, + PickComment originPickComment, Map> pickCommentReplies) { // 최상위 댓글 아이디 추출 @@ -179,24 +191,24 @@ private PickCommentsResponse getPickCommentsResponse(Member member, PickComment // 답글의 최상위 댓글이 존재하면 if (pickCommentReplies.containsKey(originPickCommentId)) { // 답글 만들기 - List pickRepliedComments = getPickRepliedComments(member, pickCommentReplies, - originPickCommentId); + List pickRepliedComments = getPickRepliedComments(member, anonymousMember, + pickCommentReplies, originPickCommentId); // 답글이 존재하는 댓글 응답 생성 - return PickCommentsResponse.of(member, originPickComment, pickRepliedComments); + return PickCommentsResponse.of(member, anonymousMember, originPickComment, pickRepliedComments); } // 답글이 없는 댓글 응답 생성 - return PickCommentsResponse.of(member, originPickComment, Collections.emptyList()); + return PickCommentsResponse.of(member, anonymousMember, originPickComment, Collections.emptyList()); } - private List getPickRepliedComments(Member member, + private List getPickRepliedComments(Member member, AnonymousMember anonymousMember, Map> pickCommentReplies, Long originPickCommentId) { return pickCommentReplies.get(originPickCommentId).stream() .sorted(Comparator.comparing(PickComment::getCreatedAt)) // 오름차순 - .map(repliedPickComment -> PickRepliedCommentsResponse.of(member, repliedPickComment)) + .map(repliedPickComment -> PickRepliedCommentsResponse.of(member, anonymousMember, repliedPickComment)) .toList(); } @@ -205,7 +217,8 @@ private List getPickRepliedComments(Member member, * @Author: 장세웅 * @Since: 2024.10.09 */ - protected List findPickBestComments(int size, Long pickId, @Nullable Member member) { + protected List findPickBestComments(int size, Long pickId, @Nullable Member member, + @Nullable AnonymousMember anonymousMember) { // 베스트 댓글 offset 정책 적용 int offset = pickBestCommentsPolicy.applySize(size); @@ -221,14 +234,63 @@ protected List findPickBestComments(int size, Long pickId, // 픽픽픽 최상위 댓글의 답글 조회(최상위 댓글의 아이디가 key) Map> pickBestCommentReplies = pickCommentRepository - .findWithMemberWithPickWithPickVoteWithPickCommentRecommendsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( - originParentIds).stream() + .findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull(originParentIds).stream() .collect(Collectors.groupingBy(pickCommentReply -> pickCommentReply.getOriginParent().getId())); // 픽픽픽 댓글/답글 응답 생성 return findOriginPickBestComments.stream() - .map(originParentPickComment -> getPickCommentsResponse(member, originParentPickComment, - pickBestCommentReplies)) + .map(originParentPickComment -> getPickCommentsResponse(member, anonymousMember, + originParentPickComment, pickBestCommentReplies)) .toList(); } + + protected PickComment getAndValidateOriginParentPickComment(Long pickCommentOriginParentId, + PickComment parentPickComment) { + + // 픽픽픽 답글 대상의 댓글이 삭제 상태이면 + validateIsDeletedPickComment(parentPickComment, INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); + + // 픽픽픽 답글 대상의 댓글이 최초 댓글이면 + if (parentPickComment.isEqualsId(pickCommentOriginParentId)) { + return parentPickComment; + } + + // 픽픽픽 답글 대상의 댓글의 메인 댓글 조회 + PickComment findOriginParentPickComment = pickCommentRepository.findById(pickCommentOriginParentId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); + + // 픽픽픽 최초 댓글이 삭제 상태이면 + validateIsDeletedPickComment(findOriginParentPickComment, INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, + REGISTER); + + return findOriginParentPickComment; + } + + @Transactional + protected PickReplyContext prepareForReplyRegistration(Long pickParentCommentId, Long pickCommentOriginParentId, + Long pickId) { + // 답글 대상의 픽픽픽 댓글 조회 + PickComment findParentPickComment = pickCommentRepository.findWithPickByIdAndPickId(pickParentCommentId, pickId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE)); + + // 픽픽픽 게시글의 승인 상태 검증 + Pick findPick = findParentPickComment.getPick(); + validateIsApprovalPickContentStatus(findPick, INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE, REGISTER); + + // 댓글 총 갯수 증가 및 인기점수 반영 + findPick.incrementCommentTotalCount(); + findPick.changePopularScore(pickPopularScorePolicy); + + // 픽픽픽 최초 댓글 검증 및 반환 + PickComment findOriginParentPickComment = getAndValidateOriginParentPickComment( + pickCommentOriginParentId, findParentPickComment); + + // 픽픽픽 최초 댓글의 답글 갯수 증가 + findOriginParentPickComment.incrementReplyTotalCount(); + + return new PickReplyContext(findPick, findOriginParentPickComment, findParentPickComment); + } + + public record PickReplyContext(Pick pick, PickComment originParentPickComment, PickComment parentPickComment) { + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceStrategy.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceStrategy.java index 2f8ba778..17b4d73f 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceStrategy.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickServiceStrategy.java @@ -20,7 +20,7 @@ public PickService getPickService() { public PickCommentService pickCommentService() { if (AuthenticationMemberUtils.isAnonymous()) { - return applicationContext.getBean(GuestPickCommentService.class); + return applicationContext.getBean(GuestPickCommentServiceV2.class); } return applicationContext.getBean(MemberPickCommentService.class); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java new file mode 100644 index 00000000..9e348b05 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/pick/dto/PickCommentDto.java @@ -0,0 +1,46 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick.dto; + +import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; +import lombok.Builder; +import lombok.Data; + +@Data +public class PickCommentDto { + private final String contents; + private Boolean isPickVotePublic; + private final String anonymousMemberId; + + @Builder + public PickCommentDto(String contents, Boolean isPickVotePublic, String anonymousMemberId) { + this.contents = contents; + this.isPickVotePublic = isPickVotePublic; + this.anonymousMemberId = anonymousMemberId; + } + + public static PickCommentDto createRegisterCommentDto(RegisterPickCommentRequest registerPickCommentRequest, + String anonymousMemberId) { + return PickCommentDto.builder() + .contents(registerPickCommentRequest.getContents()) + .isPickVotePublic(registerPickCommentRequest.getIsPickVotePublic()) + .anonymousMemberId(anonymousMemberId) + .build(); + } + + public static PickCommentDto createRepliedCommentDto(RegisterPickRepliedCommentRequest registerPickRepliedCommentRequest, + String anonymousMemberId) { + return PickCommentDto.builder() + .contents(registerPickRepliedCommentRequest.getContents()) + .anonymousMemberId(anonymousMemberId) + .build(); + } + + public static PickCommentDto createModifyCommentDto(ModifyPickCommentRequest modifyPickCommentRequest, + String anonymousMemberId) { + return PickCommentDto.builder() + .contents(modifyPickCommentRequest.getContents()) + .anonymousMemberId(anonymousMemberId) + .build(); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategy.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategy.java index e1dbb80f..f141b02b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategy.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechArticleServiceStrategy.java @@ -6,7 +6,7 @@ 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.domain.service.techArticle.techComment.GuestTechCommentService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.GuestTechCommentServiceV2; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.MemberTechCommentService; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.TechCommentService; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; @@ -29,7 +29,7 @@ public TechArticleService getTechArticleService() { public TechCommentService getTechCommentService() { if (AuthenticationMemberUtils.isAnonymous()) { - return applicationContext.getBean(GuestTechCommentService.class); + return applicationContext.getBean(GuestTechCommentServiceV2.class); } return applicationContext.getBean(MemberTechCommentService.class); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/dto/TechCommentDto.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/dto/TechCommentDto.java new file mode 100644 index 00000000..b8559632 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/dto/TechCommentDto.java @@ -0,0 +1,27 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.dto; + +import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; +import lombok.Data; + +@Data +public class TechCommentDto { + private String anonymousMemberId; + private String contents; + + public static TechCommentDto createRegisterCommentDto(RegisterTechCommentRequest registerTechCommentRequest, + String anonymousMemberId) { + TechCommentDto techCommentDto = new TechCommentDto(); + techCommentDto.setContents(registerTechCommentRequest.getContents()); + techCommentDto.setAnonymousMemberId(anonymousMemberId); + return techCommentDto; + } + + public static TechCommentDto createModifyCommentDto(ModifyTechCommentRequest modifyTechCommentRequest, + String anonymousMemberId) { + TechCommentDto techCommentDto = new TechCommentDto(); + techCommentDto.setContents(modifyTechCommentRequest.getContents()); + techCommentDto.setAnonymousMemberId(anonymousMemberId); + return techCommentDto; + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/keyword/TechKeywordService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/keyword/TechKeywordService.java new file mode 100644 index 00000000..0911485c --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/keyword/TechKeywordService.java @@ -0,0 +1,61 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword; + +import com.dreamypatisiel.devdevdev.domain.entity.TechKeyword; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechKeywordRepository; +import com.dreamypatisiel.devdevdev.global.utils.HangulUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class TechKeywordService { + private final TechKeywordRepository techKeywordRepository; + + /** + * @Note: + * @Author: 유소영 + * @Since: 2025.08.13 + * @param prefix + * @return 검색어(최대 20개) + */ + public List autocompleteKeyword(String prefix) { + String processedInput = prefix; + + // 한글이 포함되어 있다면 자/모음 분리 + if (HangulUtils.hasHangul(prefix)) { + processedInput = HangulUtils.convertToJamo(prefix); + } + + // 불리언 검색을 위해 토큰 사이에 '+' 연산자 추가 + String booleanPrefix = convertToBooleanSearch(processedInput); + Pageable pageable = PageRequest.of(0, 20); + List techKeywords = techKeywordRepository.searchKeyword(booleanPrefix, booleanPrefix, pageable); + + // 응답 데이터 가공 + return techKeywords.stream() + .map(TechKeyword::getKeyword) + .toList(); + } + + /** + * 불리언 검색을 위해 각 토큰 사이에 '+' 연산자를 추가하는 메서드 + */ + private String convertToBooleanSearch(String searchTerm) { + if (searchTerm == null || searchTerm.trim().isEmpty()) { + return searchTerm; + } + + // 공백을 기준으로 토큰을 분리하고 각 토큰 앞에 '+' 추가 + String[] tokens = searchTerm.trim().split("\\s+"); + for (int i = 0; i < tokens.length; i++) { + tokens[i] = "+" + tokens[i]; + } + return String.join(" ", tokens); + } +} 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..67ed3597 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,21 @@ 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 com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleDto; 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 +31,42 @@ 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) { + Long companyId, Double 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(techArticle -> TechArticleMainResponse.of( + techArticle.getTechArticle(), techArticle.getScore() + )) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -86,17 +77,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 +143,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..1d3ba278 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,79 +1,66 @@ 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; import com.dreamypatisiel.devdevdev.domain.entity.TechArticleRecommend; import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; -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.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.domain.repository.techArticle.*; 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) { + Long companyId, Double 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.getTechArticle(), member, techArticle.getScore())) + .toList(); - return new SliceCustom<>(techArticlesResponse, pageable, hasNextPage(techArticlesResponse, pageable), - elasticTechArticleSearchHits.getTotalHits()); + return new SliceCustom<>(techArticlesResponse, pageable, techArticles.hasNext(), techArticles.getTotalElements()); } @Override @@ -84,15 +71,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 +159,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..91213745 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,8 +10,8 @@ import org.springframework.security.core.Authentication; public interface TechArticleService { - Slice getTechArticles(Pageable pageable, String elasticId, TechArticleSort techArticleSort, - String keyword, Long companyId, Float score, + Slice getTechArticles(Pageable pageable, Long techArticleId, TechArticleSort techArticleSort, + String keyword, Long companyId, Double score, Authentication authentication); TechArticleDetailResponse getTechArticle(Long techArticleId, String anonymousMemberId, Authentication authentication); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java index 93f2d7cb..1c5bc16e 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentService.java @@ -2,18 +2,18 @@ import static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; import com.dreamypatisiel.devdevdev.domain.policy.TechBestCommentsPolicy; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; -import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; import java.util.List; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; @@ -25,13 +25,14 @@ public class GuestTechCommentService extends TechCommentCommonService implements TechCommentService { public GuestTechCommentService(TechCommentRepository techCommentRepository, - TechBestCommentsPolicy techBestCommentsPolicy) { - super(techCommentRepository, techBestCommentsPolicy); + TechBestCommentsPolicy techBestCommentsPolicy, + TechArticlePopularScorePolicy techArticlePopularScorePolicy) { + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); } @Override public TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + TechCommentDto techCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -39,20 +40,19 @@ public TechCommentResponse registerMainTechComment(Long techArticleId, @Override public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, Long parentTechCommentId, - RegisterTechCommentRequest registerRepliedTechCommentRequest, + TechCommentDto registerRepliedTechCommentRequest, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @Override - public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, - ModifyTechCommentRequest modifyTechCommentRequest, + public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, TechCommentDto modifyTechCommentDto, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @Override - public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, + public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, @Nullable String anonymousMemberId, Authentication authentication) { throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -60,12 +60,12 @@ public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommen @Override public SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, TechCommentSort techCommentSort, Pageable pageable, - Authentication authentication) { + String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); // 기술블로그 댓글/답글 조회 - return super.getTechComments(techArticleId, techCommentId, techCommentSort, pageable, null); + return super.getTechComments(techArticleId, techCommentId, techCommentSort, pageable, null, null); } @Override @@ -80,12 +80,12 @@ public TechCommentRecommendResponse recommendTechComment(Long techArticleId, Lon * @Since: 2024.10.27 */ @Override - public List findTechBestComments(int size, Long techArticleId, + public List findTechBestComments(int size, Long techArticleId, String anonymousMemberId, Authentication authentication) { // 익명 회원인지 검증 AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); - return super.findTechBestComments(size, techArticleId, null); + return super.findTechBestComments(size, techArticleId, null, null); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java new file mode 100644 index 00000000..c507b674 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2.java @@ -0,0 +1,212 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; + +import static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE; + +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import com.dreamypatisiel.devdevdev.domain.entity.TechComment; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; +import com.dreamypatisiel.devdevdev.domain.policy.TechBestCommentsPolicy; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.member.AnonymousMemberService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; +import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentRecommendResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class GuestTechCommentServiceV2 extends TechCommentCommonService implements TechCommentService { + + private final TimeProvider timeProvider; + + private final AnonymousMemberService anonymousMemberService; + private final TechArticleCommonService techArticleCommonService; + + public GuestTechCommentServiceV2(TimeProvider timeProvider, TechCommentRepository techCommentRepository, + TechBestCommentsPolicy techBestCommentsPolicy, + AnonymousMemberService anonymousMemberService, + TechArticlePopularScorePolicy techArticlePopularScorePolicy, + TechArticleCommonService techArticleCommonService) { + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); + this.timeProvider = timeProvider; + this.anonymousMemberService = anonymousMemberService; + this.techArticleCommonService = techArticleCommonService; + } + + @Override + @Transactional + public TechCommentResponse registerMainTechComment(Long techArticleId, TechCommentDto registerTechCommentDto, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String anonymousMemberId = registerTechCommentDto.getAnonymousMemberId(); + String contents = registerTechCommentDto.getContents(); + + // 회원 조회 또는 생성 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 기술블로그 조회 + TechArticle techArticle = techArticleCommonService.findTechArticle(techArticleId); + + // 댓글 엔티티 생성 및 저장 + TechComment techComment = TechComment.createMainTechCommentByAnonymousMember(new CommentContents(contents), + findAnonymousMember, techArticle); + techCommentRepository.save(techComment); + + // 기술블로그 댓글수 증가 + techArticle.incrementCommentCount(); + + // 데이터 가공 + return new TechCommentResponse(techComment.getId()); + } + + @Override + @Transactional + public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, + Long parentTechCommentId, + TechCommentDto registerRepliedTechCommentDto, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String anonymousMemberId = registerRepliedTechCommentDto.getAnonymousMemberId(); + String contents = registerRepliedTechCommentDto.getContents(); + + // 회원 조회 또는 생성 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 답글 대상의 기술블로그 댓글 조회 + TechComment findParentTechComment = techCommentRepository.findWithTechArticleByIdAndTechArticleId( + parentTechCommentId, techArticleId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); + + // 답글 엔티티 생성 및 저장 + TechComment findOriginParentTechComment = super.getAndValidateOriginParentTechComment(originParentTechCommentId, + findParentTechComment); + TechArticle findTechArticle = findParentTechComment.getTechArticle(); + + TechComment repliedTechComment = TechComment.createRepliedTechCommentByAnonymousMember(new CommentContents(contents), + findAnonymousMember, findTechArticle, findOriginParentTechComment, findParentTechComment); + techCommentRepository.save(repliedTechComment); + + // 아티클의 댓글수 증가 + findTechArticle.incrementCommentCount(); + findTechArticle.changePopularScore(techArticlePopularScorePolicy); + + // origin 댓글의 답글수 증가 + findOriginParentTechComment.incrementReplyTotalCount(); + + // 데이터 가공 + return new TechCommentResponse(repliedTechComment.getId()); + } + + @Override + @Transactional + public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, TechCommentDto modifyTechCommentDto, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + String contents = modifyTechCommentDto.getContents(); + String anonymousMemberId = modifyTechCommentDto.getAnonymousMemberId(); + + // 회원 조회 또는 생성 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 기술블로그 댓글 조회 + TechComment findTechComment = techCommentRepository.findByIdAndTechArticleIdAndCreatedAnonymousByAndDeletedAtIsNull( + techCommentId, techArticleId, findAnonymousMember) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); + + // 댓글 수정 + findTechComment.modifyCommentContents(new CommentContents(contents), timeProvider.getLocalDateTimeNow()); + + // 데이터 가공 + return new TechCommentResponse(findTechComment.getId()); + } + + @Override + @Transactional + public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, String anonymousMemberId, + Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명회원 조회 또는 생성 + AnonymousMember findAnonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 기술블로그 댓글 조회 + TechComment findTechComment = techCommentRepository.findByIdAndTechArticleIdAndCreatedAnonymousByAndDeletedAtIsNull( + techCommentId, techArticleId, findAnonymousMember) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); + + // 소프트 삭제 + findTechComment.changeDeletedAt(timeProvider.getLocalDateTimeNow(), findAnonymousMember); + + // 데이터 가공 + return new TechCommentResponse(findTechComment.getId()); + } + + /** + * @Note: 익명 회원이 기술블로그 댓글/답글을 조회한다. + * @Author: 장세웅 + * @Since: 2025.07.20 + */ + @Override + public SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, + TechCommentSort techCommentSort, Pageable pageable, + String anonymousMemberId, + Authentication authentication) { + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명회원 추출 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + // 기술블로그 댓글/답글 조회 + return super.getTechComments(techArticleId, techCommentId, techCommentSort, pageable, null, anonymousMember); + } + + @Override + public TechCommentRecommendResponse recommendTechComment(Long techArticleId, Long techCommentId, + Authentication authentication) { + throw new AccessDeniedException(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); + } + + /** + * @Note: 익명 회원이 기술블로그 베스트 댓글을 조회한다. + * @Author: 장세웅 + * @Since: 2025.07.20 + */ + @Override + @Transactional + public List findTechBestComments(int size, Long techArticleId, + String anonymousMemberId, Authentication authentication) { + + // 익명 회원인지 검증 + AuthenticationMemberUtils.validateAnonymousMethodCall(authentication); + + // 익명회원 추출 + AnonymousMember anonymousMember = anonymousMemberService.findOrCreateAnonymousMember(anonymousMemberId); + + return super.findTechBestComments(size, techArticleId, null, anonymousMember); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/MemberTechCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/MemberTechCommentService.java index 5edff37d..fc3e36a6 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/MemberTechCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/MemberTechCommentService.java @@ -1,5 +1,8 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_CAN_NOT_RECOMMEND_DELETED_TECH_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE; + import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; @@ -10,27 +13,23 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; +import java.util.List; +import java.util.Optional; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; 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; - -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.*; -import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService.validateIsDeletedTechComment; - @Service @Transactional(readOnly = true) public class MemberTechCommentService extends TechCommentCommonService implements TechCommentService { @@ -38,24 +37,18 @@ public class MemberTechCommentService extends TechCommentCommonService implement private final TechArticleCommonService techArticleCommonService; private final MemberProvider memberProvider; private final TimeProvider timeProvider; - private final TechArticlePopularScorePolicy techArticlePopularScorePolicy; - - private final TechCommentRepository techCommentRepository; private final TechCommentRecommendRepository techCommentRecommendRepository; public MemberTechCommentService(TechCommentRepository techCommentRepository, TechArticleCommonService techArticleCommonService, MemberProvider memberProvider, TimeProvider timeProvider, TechArticlePopularScorePolicy techArticlePopularScorePolicy, - TechCommentRepository techCommentRepository1, TechCommentRecommendRepository techCommentRecommendRepository, TechBestCommentsPolicy techBestCommentsPolicy) { - super(techCommentRepository, techBestCommentsPolicy); + super(techCommentRepository, techBestCommentsPolicy, techArticlePopularScorePolicy); this.techArticleCommonService = techArticleCommonService; this.memberProvider = memberProvider; this.timeProvider = timeProvider; - this.techArticlePopularScorePolicy = techArticlePopularScorePolicy; - this.techCommentRepository = techCommentRepository1; this.techCommentRecommendRepository = techCommentRecommendRepository; } @@ -65,8 +58,9 @@ public MemberTechCommentService(TechCommentRepository techCommentRepository, * @Since: 2024.08.06 */ @Transactional + @Override public TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + TechCommentDto techCommentDto, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -75,8 +69,8 @@ public TechCommentResponse registerMainTechComment(Long techArticleId, TechArticle techArticle = techArticleCommonService.findTechArticle(techArticleId); // 댓글 엔티티 생성 및 저장 - String contents = registerTechCommentRequest.getContents(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents(contents), findMember, + String contents = techCommentDto.getContents(); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents(contents), findMember, techArticle); techCommentRepository.save(techComment); @@ -96,7 +90,7 @@ public TechCommentResponse registerMainTechComment(Long techArticleId, public TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, Long parentTechCommentId, - RegisterTechCommentRequest registerRepliedTechCommentRequest, + TechCommentDto requestedRepliedTechCommentDto, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -107,12 +101,12 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); // 답글 엔티티 생성 및 저장 - TechComment findOriginParentTechComment = getAndValidateOriginParentTechComment(originParentTechCommentId, + TechComment findOriginParentTechComment = super.getAndValidateOriginParentTechComment(originParentTechCommentId, findParentTechComment); TechArticle findTechArticle = findParentTechComment.getTechArticle(); - String contents = registerRepliedTechCommentRequest.getContents(); - TechComment repliedTechComment = TechComment.createRepliedTechComment(new CommentContents(contents), findMember, + String contents = requestedRepliedTechCommentDto.getContents(); + TechComment repliedTechComment = TechComment.createRepliedTechCommentByMember(new CommentContents(contents), findMember, findTechArticle, findOriginParentTechComment, findParentTechComment); techCommentRepository.save(repliedTechComment); @@ -127,41 +121,14 @@ public TechCommentResponse registerRepliedTechComment(Long techArticleId, return new TechCommentResponse(repliedTechComment.getId()); } - /** - * @Note: 답글 대상의 댓글을 조회하고, 답글 대상의 댓글이 최초 댓글이면 답글 대상으로 반환한다. - * @Author: 유소영 - * @Since: 2024.09.06 - */ - private TechComment getAndValidateOriginParentTechComment(Long originParentTechCommentId, - TechComment parentTechComment) { - - // 삭제된 댓글에는 답글 작성 불가 - validateIsDeletedTechComment(parentTechComment, INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE, null); - - // 답글 대상의 댓글이 최초 댓글이면 답글 대상으로 반환 - if (parentTechComment.isEqualsId(originParentTechCommentId)) { - return parentTechComment; - } - - // 답글 대상의 댓글의 메인 댓글 조회 - TechComment findOriginParentTechComment = techCommentRepository.findById(originParentTechCommentId) - .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); - - // 최초 댓글이 삭제 상태이면 답글 작성 불가 - validateIsDeletedTechComment(findOriginParentTechComment, INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE, - null); - - return findOriginParentTechComment; - } - /** * @Note: 기술블로그 댓글을 수정한다. 단, 본인이 작성한 댓글만 수정할 수 있다. * @Author: 유소영 * @Since: 2024.08.11 */ + @Override @Transactional - public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, - ModifyTechCommentRequest modifyTechCommentRequest, + public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, TechCommentDto modifyTechCommentDto, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -172,7 +139,7 @@ public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommen .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); // 댓글 수정 - String contents = modifyTechCommentRequest.getContents(); + String contents = modifyTechCommentDto.getContents(); findTechComment.modifyCommentContents(new CommentContents(contents), timeProvider.getLocalDateTimeNow()); // 데이터 가공 @@ -184,10 +151,9 @@ public TechCommentResponse modifyTechComment(Long techArticleId, Long techCommen * @Author: 유소영 * @Since: 2024.08.13 */ - @Transactional - public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, + @Override + public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, @Nullable String anonymousMemberId, Authentication authentication) { - // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); @@ -224,12 +190,13 @@ public TechCommentResponse deleteTechComment(Long techArticleId, Long techCommen */ public SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, TechCommentSort techCommentSort, Pageable pageable, + String anonymousMemberId, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); // 기술블로그 댓글/답글 조회 - return super.getTechComments(techArticleId, techCommentId, techCommentSort, pageable, findMember); + return super.getTechComments(techArticleId, techCommentId, techCommentSort, pageable, findMember, null); } /** @@ -263,12 +230,12 @@ public TechCommentRecommendResponse recommendTechComment(Long techArticleId, Lon */ @Override public List findTechBestComments(int size, Long techArticleId, - Authentication authentication) { + String anonymousMemberId, Authentication authentication) { // 회원 조회 Member findMember = memberProvider.getMemberByAuthentication(authentication); - return super.findTechBestComments(size, techArticleId, findMember); + return super.findTechBestComments(size, techArticleId, findMember, null); } private TechCommentRecommendResponse toggleTechCommentRecommend(TechComment techComment, Member member) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentCommonService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentCommonService.java index 95eb83e6..a8753f63 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentCommonService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentCommonService.java @@ -1,11 +1,18 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.techArticle.TechArticleCommonService.validateIsDeletedTechComment; + +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.BasicTime; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; +import com.dreamypatisiel.devdevdev.domain.policy.TechArticlePopularScorePolicy; import com.dreamypatisiel.devdevdev.domain.policy.TechBestCommentsPolicy; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechRepliedCommentsResponse; @@ -28,8 +35,9 @@ @Transactional(readOnly = true) public class TechCommentCommonService { - private final TechCommentRepository techCommentRepository; - private final TechBestCommentsPolicy techBestCommentsPolicy; + protected final TechCommentRepository techCommentRepository; + protected final TechBestCommentsPolicy techBestCommentsPolicy; + protected final TechArticlePopularScorePolicy techArticlePopularScorePolicy; /** * @Note: 정렬 조건에 따라 커서 방식으로 기술블로그 댓글 목록을 조회한다. @@ -37,8 +45,9 @@ public class TechCommentCommonService { * @Since: 2024.09.05 */ public SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, - TechCommentSort techCommentSort, Pageable pageable, - @Nullable Member member) { + TechCommentSort techCommentSort, Pageable pageable, + @Nullable Member member, + @Nullable AnonymousMember anonymousMember) { // 기술블로그 최상위 댓글 조회 Slice findOriginParentTechComments = techCommentRepository.findOriginParentTechCommentsByCursor( techArticleId, techCommentId, techCommentSort, pageable); @@ -51,13 +60,13 @@ public SliceCommentCustom getTechComments(Long techArticle // 최상위 댓글 아이디들의 댓글 답글 조회(최상위 댓글의 아이디가 key) Map> techCommentReplies = techCommentRepository - .findWithMemberWithTechArticleByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + .findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( originParentIds).stream() .collect(Collectors.groupingBy(techCommentReply -> techCommentReply.getOriginParent().getId())); // 기술블로그 댓글/답글 응답 생성 List techCommentsResponse = originParentTechComments.stream() - .map(originParentTechComment -> getTechCommentsResponse(member, originParentTechComment, + .map(originParentTechComment -> getTechCommentsResponse(member, anonymousMember, originParentTechComment, techCommentReplies)) .toList(); @@ -75,14 +84,17 @@ public SliceCommentCustom getTechComments(Long techArticle long originTechCommentTotalCount = firstTechComment.getTechArticle().getCommentTotalCount().getCount(); // 기술블로그 부모 댓글 개수 추출 - long originParentTechCommentTotalCount = techCommentRepository.countByTechArticleIdAndOriginParentIsNullAndParentIsNullAndDeletedAtIsNull(techArticleId); + long originParentTechCommentTotalCount = techCommentRepository.countByTechArticleIdAndOriginParentIsNullAndParentIsNullAndDeletedAtIsNull( + techArticleId); // 데이터 가공 return new SliceCommentCustom<>(techCommentsResponse, pageable, findOriginParentTechComments.hasNext(), originTechCommentTotalCount, originParentTechCommentTotalCount); } - private TechCommentsResponse getTechCommentsResponse(@Nullable Member member, TechComment originParentTechComment, + private TechCommentsResponse getTechCommentsResponse(@Nullable Member member, + @Nullable AnonymousMember anonymousMember, + TechComment originParentTechComment, Map> techCommentReplies) { // 최상위 댓글의 아이디 추출 Long originParentTechCommentId = originParentTechComment.getId(); @@ -92,18 +104,19 @@ private TechCommentsResponse getTechCommentsResponse(@Nullable Member member, Te // 답글이 없을 경우 if (ObjectUtils.isEmpty(replies)) { - return TechCommentsResponse.of(member, originParentTechComment, Collections.emptyList()); + return TechCommentsResponse.of(member, anonymousMember, originParentTechComment, Collections.emptyList()); } // 답글 응답 만들기 - List techRepliedComments = getTechRepliedComments(member, replies); - return TechCommentsResponse.of(member, originParentTechComment, techRepliedComments); + List techRepliedComments = getTechRepliedComments(member, anonymousMember, replies); + return TechCommentsResponse.of(member, anonymousMember, originParentTechComment, techRepliedComments); } - private List getTechRepliedComments(Member member, List replies) { + private List getTechRepliedComments(Member member, AnonymousMember anonymousMember, + List replies) { return replies.stream() .sorted(Comparator.comparing(BasicTime::getCreatedAt)) - .map(repliedTechComment -> TechRepliedCommentsResponse.of(member, repliedTechComment)) + .map(repliedTechComment -> TechRepliedCommentsResponse.of(member, anonymousMember, repliedTechComment)) .toList(); } @@ -112,7 +125,8 @@ private List getTechRepliedComments(Member member, * @Author: 장세웅 * @Since: 2024.10.27 */ - protected List findTechBestComments(int size, Long techArticleId, @Nullable Member member) { + protected List findTechBestComments(int size, Long techArticleId, @Nullable Member member, + @Nullable AnonymousMember anonymousMember) { // 베스트 댓글 offset 정책 적용 int offset = techBestCommentsPolicy.applySize(size); @@ -127,14 +141,40 @@ protected List findTechBestComments(int size, Long techArt .collect(Collectors.toSet()); // 베스트 댓글의 답글 조회(베스트 댓글의 아이디가 key) - Map> techBestCommentReplies = techCommentRepository.findWithMemberWithTechArticleByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( + Map> techBestCommentReplies = techCommentRepository.findWithDetailsByOriginParentIdInAndParentIsNotNullAndOriginParentIsNotNull( originParentIds).stream() .collect(Collectors.groupingBy(techCommentReply -> techCommentReply.getOriginParent().getId())); // 기술블로그 댓글/답글 응답 생성 return findOriginTechBestComments.stream() - .map(originParentTechComment -> getTechCommentsResponse(member, originParentTechComment, + .map(originParentTechComment -> getTechCommentsResponse(member, anonymousMember, originParentTechComment, techBestCommentReplies)) .toList(); } + + /** + * @Note: 답글 대상의 댓글을 조회하고, 답글 대상의 댓글이 최초 댓글이면 답글 대상으로 반환한다. + * @Author: 유소영 + * @Since: 2024.09.06 + */ + protected TechComment getAndValidateOriginParentTechComment(Long originParentTechCommentId, TechComment parentTechComment) { + + // 삭제된 댓글에는 답글 작성 불가 + validateIsDeletedTechComment(parentTechComment, INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE, null); + + // 답글 대상의 댓글이 최초 댓글이면 답글 대상으로 반환 + if (parentTechComment.isEqualsId(originParentTechCommentId)) { + return parentTechComment; + } + + // 답글 대상의 댓글의 메인 댓글 조회 + TechComment findOriginParentTechComment = techCommentRepository.findById(originParentTechCommentId) + .orElseThrow(() -> new NotFoundException(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE)); + + // 최초 댓글이 삭제 상태이면 답글 작성 불가 + validateIsDeletedTechComment(findOriginParentTechComment, INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE, + null); + + return findOriginParentTechComment; + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentService.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentService.java index 7ca722c4..d151c4ef 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/TechCommentService.java @@ -1,41 +1,40 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; -import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; import java.util.List; +import javax.annotation.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.security.core.Authentication; public interface TechCommentService { TechCommentResponse registerMainTechComment(Long techArticleId, - RegisterTechCommentRequest registerTechCommentRequest, + TechCommentDto registerTechCommentDto, Authentication authentication); TechCommentResponse registerRepliedTechComment(Long techArticleId, Long originParentTechCommentId, Long parentTechCommentId, - RegisterTechCommentRequest registerRepliedTechCommentRequest, + TechCommentDto registerRepliedTechCommentDto, Authentication authentication); - TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, - ModifyTechCommentRequest modifyTechCommentRequest, + TechCommentResponse modifyTechComment(Long techArticleId, Long techCommentId, TechCommentDto modifyTechCommentDto, Authentication authentication); - TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, Authentication authentication); + TechCommentResponse deleteTechComment(Long techArticleId, Long techCommentId, @Nullable String anonymousMemberId, + Authentication authentication); SliceCommentCustom getTechComments(Long techArticleId, Long techCommentId, TechCommentSort techCommentSort, Pageable pageable, - Authentication authentication); + String anonymousMemberId, Authentication authentication); - TechCommentRecommendResponse recommendTechComment(Long techArticleId, Long techCommentId, - Authentication authentication); + TechCommentRecommendResponse recommendTechComment(Long techArticleId, Long techCommentId, Authentication authentication); - List findTechBestComments(int size, Long techArticleId, Authentication authentication); + List findTechBestComments(int size, Long techArticleId, String anonymousMemberId, + 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 new file mode 100644 index 00000000..6cbd1275 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java @@ -0,0 +1,25 @@ +package com.dreamypatisiel.devdevdev.global.config; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.FunctionContributor; + +import static org.hibernate.type.StandardBasicTypes.DOUBLE; + +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/security/jwt/model/JwtCookieConstant.java b/src/main/java/com/dreamypatisiel/devdevdev/global/security/jwt/model/JwtCookieConstant.java index 1c14122f..cea3f392 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/security/jwt/model/JwtCookieConstant.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/security/jwt/model/JwtCookieConstant.java @@ -7,4 +7,5 @@ public class JwtCookieConstant { public static final String DEVDEVDEV_MEMBER_NICKNAME = "DEVDEVDEV_MEMBER_NICKNAME"; public static final String DEVDEVDEV_MEMBER_EMAIL = "DEVDEVDEV_MEMBER_EMAIL"; public static final String DEVDEVDEV_MEMBER_IS_ADMIN = "DEVDEVDEV_MEMBER_IS_ADMIN"; + public static final String DEVDEVDEV_MEMBER_IS_NEW = "DEVDEVDEV_MEMBER_IS_NEW"; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandler.java b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandler.java index a1c634e9..57401d4a 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandler.java @@ -3,15 +3,19 @@ import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; +import com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant; import com.dreamypatisiel.devdevdev.global.security.jwt.model.Token; import com.dreamypatisiel.devdevdev.global.security.jwt.service.JwtMemberService; import com.dreamypatisiel.devdevdev.global.security.jwt.service.TokenService; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.OAuth2UserProvider; +import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; import com.dreamypatisiel.devdevdev.global.utils.UriUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.Collection; + import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -53,8 +57,9 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo CookieUtils.configJwtCookie(response, token); // 유저 정보 쿠키에 저장 + UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal(); Member member = memberProvider.getMemberByAuthentication(authentication); - CookieUtils.configMemberCookie(response, member); + CookieUtils.configMemberCookie(response, member, userPrincipal.isNewMember()); // 리다이렉트 설정 String redirectUri = UriUtils.createUriByDomainAndEndpoint(domain, endpoint); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipal.java b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipal.java index be13efd5..2327d8be 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipal.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipal.java @@ -34,6 +34,8 @@ public class UserPrincipal implements OAuth2User, UserDetails { private final Collection authorities; @Setter(value = AccessLevel.PRIVATE) private Map attributes = new HashMap<>(); + @Getter + private boolean isNewMember; public static UserPrincipal createByMember(Member member) { List simpleGrantedAuthorities = Collections.singletonList( @@ -51,9 +53,10 @@ public static UserPrincipal createByMember(Member member) { ); } - public static UserPrincipal createByMemberAndAttributes(Member member, Map attributes) { + public static UserPrincipal createByMemberAndAttributes(Member member, Map attributes, boolean isNewMember) { UserPrincipal userPrincipal = UserPrincipal.createByMember(member); userPrincipal.setAttributes(attributes); + userPrincipal.isNewMember = isNewMember; return userPrincipal; } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/OAuth2MemberService.java b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/OAuth2MemberService.java index 2e2472e5..795002cc 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/OAuth2MemberService.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/OAuth2MemberService.java @@ -27,7 +27,7 @@ public class OAuth2MemberService { public UserPrincipal register(OAuth2UserProvider oAuth2UserProvider, OAuth2User oAuth2User) { Optional optionalMember = findMemberByOAuth2UserProvider(oAuth2UserProvider); if (optionalMember.isPresent()) { - return UserPrincipal.createByMemberAndAttributes(optionalMember.get(), oAuth2User.getAttributes()); + return UserPrincipal.createByMemberAndAttributes(optionalMember.get(), oAuth2User.getAttributes(), false); } // 데이터베이스 회원이 없으면 회원가입 시킨다. @@ -40,7 +40,7 @@ public UserPrincipal register(OAuth2UserProvider oAuth2UserProvider, OAuth2User Member newMember = memberRepository.save(Member.createMemberBy(socialMemberDto)); - return UserPrincipal.createByMemberAndAttributes(newMember, oAuth2User.getAttributes()); + return UserPrincipal.createByMemberAndAttributes(newMember, oAuth2User.getAttributes(), true); } private Optional findMemberByOAuth2UserProvider(OAuth2UserProvider oAuth2UserProvider) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/AuthenticationMemberUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/AuthenticationMemberUtils.java index 60d345db..ae57c331 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/AuthenticationMemberUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/AuthenticationMemberUtils.java @@ -5,7 +5,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -public class AuthenticationMemberUtils { +public abstract class AuthenticationMemberUtils { public static final String ANONYMOUS_USER = "anonymousUser"; public static final String INVALID_TYPE_CAST_USER_PRINCIPAL_MESSAGE = "인증객체 타입에 문제가 발생했습니다."; @@ -14,7 +14,7 @@ public class AuthenticationMemberUtils { public static UserPrincipal getUserPrincipal() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if(!isUserPrincipalClass(principal)) { + if (!isUserPrincipalClass(principal)) { throw new UserPrincipalException(INVALID_TYPE_CAST_USER_PRINCIPAL_MESSAGE); } @@ -22,7 +22,7 @@ public static UserPrincipal getUserPrincipal() { } public static void validateAnonymousMethodCall(Authentication authentication) { - if(!isAnonymous(authentication)) { + if (!isAnonymous(authentication)) { throw new IllegalStateException(INVALID_METHODS_CALL_MESSAGE); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/BigDecimalUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/BigDecimalUtils.java index ab7788d9..7ee7d502 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/BigDecimalUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/BigDecimalUtils.java @@ -3,7 +3,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; -public class BigDecimalUtils { +public abstract class BigDecimalUtils { private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); public static final int DEFAULT_SCALE = 2; @@ -11,7 +11,7 @@ public class BigDecimalUtils { // 퍼센트 계산 public static BigDecimal toPercentageOf(BigDecimal value, BigDecimal total) { - if(BigDecimal.ZERO.equals(total)) { + if (BigDecimal.ZERO.equals(total)) { return BigDecimal.ZERO; } return value.divide(total, DEFAULT_SCALE, RoundingMode.HALF_UP).multiply(ONE_HUNDRED); @@ -19,7 +19,7 @@ public static BigDecimal toPercentageOf(BigDecimal value, BigDecimal total) { // 퍼센트의 값 계산 public static BigDecimal percentOf(BigDecimal percentage, BigDecimal total) { - if(BigDecimal.ZERO.equals(total)) { + if (BigDecimal.ZERO.equals(total)) { return BigDecimal.ZERO; } return percentage.multiply(total).divide(ONE_HUNDRED, DEFAULT_SCALE, RoundingMode.HALF_UP); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java index f048e0d9..fcdabd09 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtils.java @@ -11,10 +11,12 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.util.ObjectUtils; import org.springframework.util.SerializationUtils; -public class CookieUtils { +public abstract class CookieUtils { public static final int DEFAULT_MAX_AGE = 180; public static final int REFRESH_MAX_AGE = 60 * 60 * 24 * 7; @@ -27,6 +29,7 @@ public class CookieUtils { public static final String DEVDEVDEV_DOMAIN = "devdevdev.co.kr"; public static final String ACTIVE = "active"; public static final String INACTIVE = "inactive"; + public static final String NONE = "None"; public static Cookie getRequestCookieByName(HttpServletRequest request, String name) { @@ -48,14 +51,16 @@ public static String getRequestCookieValueByName(HttpServletRequest request, Str public static void addCookieToResponse(HttpServletResponse response, String name, String value, int maxAge, boolean isHttpOnly, boolean isSecure) { - Cookie cookie = new Cookie(name, value); - cookie.setPath(DEFAULT_PATH); - cookie.setHttpOnly(isHttpOnly); - cookie.setSecure(isSecure); - cookie.setMaxAge(maxAge); - cookie.setDomain(DEVDEVDEV_DOMAIN); - - response.addCookie(cookie); + ResponseCookie accessCookie = ResponseCookie.from(name, value) + .path(DEFAULT_PATH) + .domain(DEVDEVDEV_DOMAIN) + .maxAge(maxAge) + .httpOnly(isHttpOnly) + .secure(isSecure) + .sameSite(NONE) + .build(); + + response.addHeader(HttpHeaders.SET_COOKIE, accessCookie.toString()); } // 쿠키를 삭제하려면 클라이언트에게 해당 쿠키가 더 이상 유효하지 않음을 알려야 합니다. @@ -94,7 +99,7 @@ public static void configJwtCookie(HttpServletResponse response, Token token) { ACTIVE, DEFAULT_MAX_AGE, false, true); } - public static void configMemberCookie(HttpServletResponse response, Member member) { + public static void configMemberCookie(HttpServletResponse response, Member member, boolean isNewMember) { // 닉네임 UTF-8 인코딩 필요 String nickname = URLEncoder.encode(member.getNicknameAsString(), StandardCharsets.UTF_8); @@ -104,6 +109,8 @@ public static void configMemberCookie(HttpServletResponse response, Member membe member.getEmailAsString(), DEFAULT_MAX_AGE, false, true); addCookieToResponse(response, JwtCookieConstant.DEVDEVDEV_MEMBER_IS_ADMIN, String.valueOf(member.isAdmin()), DEFAULT_MAX_AGE, false, true); + addCookieToResponse(response, JwtCookieConstant.DEVDEVDEV_MEMBER_IS_NEW, + String.valueOf(isNewMember), DEFAULT_MAX_AGE, false, true); } private static void validationCookieEmpty(Cookie[] cookies) { diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/FileUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/FileUtils.java index 1e8d44a7..e488c75a 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/FileUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/FileUtils.java @@ -8,7 +8,7 @@ import java.util.UUID; import org.springframework.web.multipart.MultipartFile; -public class FileUtils { +public abstract class FileUtils { public static final String SLASH = "/"; public static final String DASH = "-"; @@ -22,7 +22,7 @@ public static String createRandomFileNameBy(String originalFileName) { public static List createBlobInfos(String bucketName, List multipartFiles) { return multipartFiles.stream() .map(image -> Blob.newBuilder(bucketName, - FileUtils.createRandomFileNameBy(image.getOriginalFilename())) + FileUtils.createRandomFileNameBy(image.getOriginalFilename())) .setContentType(image.getContentType()) .build() ) @@ -35,7 +35,7 @@ public static void validateMediaType(MultipartFile targetMultipartFile, String[] boolean isAllowMediaType = Arrays.stream(allowedMediaTypes) .anyMatch(mediaType -> mediaType.equals(targetMultipartFile.getContentType())); - if(!isAllowMediaType) { + if (!isAllowMediaType) { String supportedMediaType = String.join(DELIMITER_COMMA, allowedMediaTypes); String errorMessage = String.format(INVALID_MEDIA_TYPE_MESSAGE, contentType, supportedMediaType); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java new file mode 100644 index 00000000..c431548d --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtils.java @@ -0,0 +1,162 @@ +package com.dreamypatisiel.devdevdev.global.utils; + +/** + * 한글 처리를 위한 유틸리티 클래스 + */ +public abstract class HangulUtils { + + // 한글 유니코드 범위 + private static final int HANGUL_START = 0xAC00; // '가' + private static final int HANGUL_END = 0xD7A3; // '힣' + + // 자모 유니코드 범위 + private static final int JAMO_START = 0x1100; // 'ㄱ' + private static final int JAMO_END = 0x11FF; // 'ㅿ' + + // 호환 자모 유니코드 범위 + private static final int COMPAT_JAMO_START = 0x3130; // 'ㄱ' + private static final int COMPAT_JAMO_END = 0x318F; // 'ㆎ' + + // 한글 분해를 위한 상수 + private static final int CHOSUNG_COUNT = 19; + private static final int JUNGSUNG_COUNT = 21; + private static final int JONGSUNG_COUNT = 28; + + // 초성 배열 + private static final char[] CHOSUNG = { + 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + }; + + // 중성 배열 + private static final char[] JUNGSUNG = { + 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', + 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ' + }; + + // 종성 배열 (첫 번째는 받침 없음) + private static final char[] JONGSUNG = { + '\0', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', + 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', + 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' + }; + + /** + * 문자열에 한글이 포함되어 있는지 확인 + */ + public static boolean hasHangul(String text) { + if (text == null || text.isEmpty()) { + return false; + } + + for (char ch : text.toCharArray()) { + if (isHangul(ch)) { + return true; + } + } + return false; + } + + /** + * 한글 문자열을 자모로 분해 + */ + public static String convertToJamo(String text) { + if (text == null || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(); + + for (char ch : text.toCharArray()) { + if (isCompleteHangul(ch)) { + // 완성된 한글 문자를 자모로 분해 + int unicode = ch - HANGUL_START; + + int chosungIndex = unicode / (JUNGSUNG_COUNT * JONGSUNG_COUNT); + int jungsungIndex = (unicode % (JUNGSUNG_COUNT * JONGSUNG_COUNT)) / JONGSUNG_COUNT; + int jongsungIndex = unicode % JONGSUNG_COUNT; + + result.append(CHOSUNG[chosungIndex]); + result.append(JUNGSUNG[jungsungIndex]); + + if (jongsungIndex > 0) { + result.append(JONGSUNG[jongsungIndex]); + } + } else { + // 한글이 아니거나 이미 자모인 경우 그대로 추가 + result.append(ch); + } + } + + return result.toString(); + } + + /** + * 한글 문자열에서 초성만 추출 + */ + public static String extractChosung(String text) { + if (text == null || text.isEmpty()) { + return text; + } + + StringBuilder result = new StringBuilder(); + + for (char ch : text.toCharArray()) { + if (isCompleteHangul(ch)) { + // 완성된 한글 문자에서 초성 추출 + int unicode = ch - HANGUL_START; + int chosungIndex = unicode / (JUNGSUNG_COUNT * JONGSUNG_COUNT); + result.append(CHOSUNG[chosungIndex]); + } else if (isChosung(ch)) { + // 이미 초성인 경우 그대로 추가 + result.append(ch); + } else if (!isHangul(ch)) { + // 한글이 아닌 문자는 그대로 추가 + result.append(ch); + } + // 중성, 종성은 무시 + } + + return result.toString(); + } + + /** + * 문자가 한글인지 확인 (완성형 한글 + 자모) + */ + private static boolean isHangul(char ch) { + return isCompleteHangul(ch) || isJamo(ch) || isCompatJamo(ch); + } + + /** + * 문자가 완성된 한글인지 확인 + */ + private static boolean isCompleteHangul(char ch) { + return ch >= HANGUL_START && ch <= HANGUL_END; + } + + /** + * 문자가 자모인지 확인 + */ + private static boolean isJamo(char ch) { + return ch >= JAMO_START && ch <= JAMO_END; + } + + /** + * 문자가 호환 자모인지 확인 + */ + private static boolean isCompatJamo(char ch) { + return ch >= COMPAT_JAMO_START && ch <= COMPAT_JAMO_END; + } + + /** + * 문자가 초성인지 확인 + */ + private static boolean isChosung(char ch) { + for (char chosung : CHOSUNG) { + if (ch == chosung) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HttpRequestUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HttpRequestUtils.java new file mode 100644 index 00000000..de67bb08 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/HttpRequestUtils.java @@ -0,0 +1,14 @@ +package com.dreamypatisiel.devdevdev.global.utils; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +public abstract class HttpRequestUtils { + public static String getHeaderValue(String headerName) { + ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletRequest request = attrs.getRequest(); + + return request.getHeader(headerName); + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/UriUtils.java b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/UriUtils.java index f0a77096..e0d90e64 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/global/utils/UriUtils.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/utils/UriUtils.java @@ -2,7 +2,7 @@ import org.springframework.web.util.UriComponentsBuilder; -public class UriUtils { +public abstract class UriUtils { public static String createUriByDomainAndEndpoint(String domain, String endpoint) { return UriComponentsBuilder diff --git a/src/main/java/com/dreamypatisiel/devdevdev/redis/sub/RedisNotificationSubscriber.java b/src/main/java/com/dreamypatisiel/devdevdev/redis/sub/RedisNotificationSubscriber.java index 8550ba3f..b982357f 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/redis/sub/RedisNotificationSubscriber.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/redis/sub/RedisNotificationSubscriber.java @@ -31,7 +31,7 @@ public void onMessage(@Nullable Message message, byte[] pattern) { try { // 채널 파싱 String channel = new String(pattern, StandardCharsets.UTF_8); - + // 구독 채널인 경우 if (channel.equals(NotificationType.SUBSCRIPTION.name())) { ObjectMapper om = new ObjectMapper(); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/exception/ApiControllerAdvice.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/exception/ApiControllerAdvice.java index a34e81ce..85147a37 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/exception/ApiControllerAdvice.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/exception/ApiControllerAdvice.java @@ -5,13 +5,7 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.SdkClientException; -import com.dreamypatisiel.devdevdev.exception.ImageFileException; -import com.dreamypatisiel.devdevdev.exception.InternalServerException; -import com.dreamypatisiel.devdevdev.exception.MemberException; -import com.dreamypatisiel.devdevdev.exception.NotFoundException; -import com.dreamypatisiel.devdevdev.exception.PickOptionImageNameException; -import com.dreamypatisiel.devdevdev.exception.TokenInvalidException; -import com.dreamypatisiel.devdevdev.exception.TokenNotFoundException; +import com.dreamypatisiel.devdevdev.exception.*; import com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; @@ -100,6 +94,12 @@ public ResponseEntity> memberException(MemberException e) HttpStatus.NOT_FOUND); } + @ExceptionHandler(NicknameException.class) + public ResponseEntity> nicknameException(NicknameException e) { + return new ResponseEntity<>(BasicResponse.fail(e.getMessage(), HttpStatus.BAD_REQUEST.value()), + HttpStatus.BAD_REQUEST); + } + @ExceptionHandler(NotFoundException.class) public ResponseEntity> notFoundException(NotFoundException e) { return new ResponseEntity<>(BasicResponse.fail(e.getMessage(), HttpStatus.NOT_FOUND.value()), 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 f90a8c94..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 @@ -1,12 +1,14 @@ package com.dreamypatisiel.devdevdev.web.controller.member; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; +import com.dreamypatisiel.devdevdev.domain.service.member.MemberNicknameDictionaryService; import com.dreamypatisiel.devdevdev.domain.service.member.MemberService; import com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.request.comment.MyWrittenCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.member.ChangeNicknameRequest; import com.dreamypatisiel.devdevdev.web.dto.request.member.RecordMemberExitSurveyAnswerRequest; import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; import com.dreamypatisiel.devdevdev.web.dto.response.comment.MyWrittenCommentResponse; @@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -42,6 +45,7 @@ public class MypageController { private final MemberService memberService; + private final MemberNicknameDictionaryService memberNicknameDictionaryService; @Operation(summary = "북마크 목록 조회") @GetMapping("/mypage/bookmarks") @@ -51,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)); } @@ -133,4 +137,29 @@ public ResponseEntity>> get return ResponseEntity.ok(BasicResponse.success(mySubscribedCompanies)); } + + @Operation(summary = "랜덤 닉네임 생성", description = "랜덤 닉네임을 생성합니다.") + @GetMapping("/mypage/nickname/random") + public ResponseEntity> getRandomNickname() { + String response = memberNicknameDictionaryService.createRandomNickname(); + return ResponseEntity.ok(BasicResponse.success(response)); + } + + @Operation(summary = "닉네임 변경", description = "유저의 닉네임을 변경합니다.") + @PatchMapping("/mypage/nickname") + public ResponseEntity> changeNickname( + @RequestBody @Valid ChangeNicknameRequest request + ) { + Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String response = memberService.changeNickname(request.getNickname(), authentication); + return ResponseEntity.ok(BasicResponse.success(response)); + } + + @Operation(summary = "닉네임 변경 가능 여부 조회", description = "닉네임 변경 가능 여부를 true/false로 반환합니다.") + @GetMapping("/mypage/nickname/changeable") + public ResponseEntity> canChangeNickname() { + Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + boolean result = memberService.canChangeNickname(authentication); + return ResponseEntity.ok(BasicResponse.success(result)); + } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java index e1a1ebb9..a01b3134 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentController.java @@ -1,10 +1,14 @@ package com.dreamypatisiel.devdevdev.web.controller.pick; +import static com.dreamypatisiel.devdevdev.web.WebConstant.HEADER_ANONYMOUS_MEMBER_ID; + import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; import com.dreamypatisiel.devdevdev.domain.service.pick.PickCommentService; import com.dreamypatisiel.devdevdev.domain.service.pick.PickServiceStrategy; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import com.dreamypatisiel.devdevdev.global.utils.HttpRequestUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; @@ -42,22 +46,26 @@ public class PickCommentController { private final PickServiceStrategy pickServiceStrategy; - @Operation(summary = "픽픽픽 댓글 작성", description = "회원은 픽픽픽 댓글을 작성할 수 있습니다.") + @Operation(summary = "픽픽픽 댓글 작성", description = "픽픽픽 댓글을 작성할 수 있습니다.") @PostMapping("/picks/{pickId}/comments") public ResponseEntity> registerPickComment( @PathVariable Long pickId, @RequestBody @Validated RegisterPickCommentRequest registerPickCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + + PickCommentDto registerCommentDto = PickCommentDto.createRegisterCommentDto(registerPickCommentRequest, + anonymousMemberId); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); - PickCommentResponse pickCommentResponse = pickCommentService.registerPickComment(pickId, - registerPickCommentRequest, authentication); + PickCommentResponse pickCommentResponse = pickCommentService.registerPickComment( + pickId, registerCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } - @Operation(summary = "픽픽픽 답글 작성", description = "회원은 픽픽픽 댓글에 답글을 작성할 수 있습니다.") + @Operation(summary = "픽픽픽 답글 작성", description = "픽픽픽 댓글에 답글을 작성할 수 있습니다.") @PostMapping("/picks/{pickId}/comments/{pickOriginParentCommentId}/{pickParentCommentId}") public ResponseEntity> registerPickRepliedComment( @PathVariable Long pickId, @@ -66,16 +74,20 @@ public ResponseEntity> registerPickRepliedCom @RequestBody @Validated RegisterPickRepliedCommentRequest registerPickRepliedCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(registerPickRepliedCommentRequest, + anonymousMemberId); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); PickCommentResponse pickCommentResponse = pickCommentService.registerPickRepliedComment( - pickParentCommentId, pickOriginParentCommentId, pickId, registerPickRepliedCommentRequest, + pickParentCommentId, pickOriginParentCommentId, pickId, repliedCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } - @Operation(summary = "픽픽픽 댓글/답글 수정", description = "회원은 자신이 작성한 픽픽픽 댓글/답글을 수정할 수 있습니다.") + @Operation(summary = "픽픽픽 댓글/답글 수정", description = "회원/익명회원 본인이 작성한 픽픽픽 댓글/답글만 수정할 수 있습니다.") @PatchMapping("/picks/{pickId}/comments/{pickCommentId}") public ResponseEntity> modifyPickComment( @PathVariable Long pickId, @@ -83,15 +95,19 @@ public ResponseEntity> modifyPickComment( @RequestBody @Validated ModifyPickCommentRequest modifyPickCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(modifyPickCommentRequest, + anonymousMemberId); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); PickCommentResponse pickCommentResponse = pickCommentService.modifyPickComment(pickCommentId, pickId, - modifyPickCommentRequest, authentication); + modifyCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } - @Operation(summary = "픽픽픽 댓글/답글 조회", description = "회원은 픽픽픽 댓글/답글을 조회할 수 있습니다.") + @Operation(summary = "픽픽픽 댓글/답글 조회", description = "픽픽픽 댓글/답글을 조회할 수 있습니다.") @GetMapping("/picks/{pickId}/comments") public ResponseEntity>> getPickComments( @PageableDefault(size = 5, sort = "id", direction = Direction.DESC) Pageable pageable, @@ -101,25 +117,26 @@ public ResponseEntity>> getPickC @RequestParam(required = false) EnumSet pickOptionTypes) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); - SliceCustom pickCommentsResponse = pickCommentService.findPickComments(pageable, - pickId, pickCommentId, pickCommentSort, pickOptionTypes, authentication); + SliceCustom pickCommentsResponse = pickCommentService.findPickComments( + pageable, pickId, pickCommentId, pickCommentSort, pickOptionTypes, anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentsResponse)); } - @Operation(summary = "픽픽픽 댓글/답글 삭제", description = "회원은 자신이 작성한 픽픽픽 댓글/답글을 삭제할 수 있습니다.(어드민은 모든 댓글 삭제 가능)") + @Operation(summary = "픽픽픽 댓글/답글 삭제", description = "회원/익명회원 본인이 작성한 픽픽픽 댓글/답글을 삭제할 수 있습니다.(어드민은 모든 댓글 삭제 가능)") @DeleteMapping("/picks/{pickId}/comments/{pickCommentId}") - public ResponseEntity> deletePickComment( - @PathVariable Long pickId, - @PathVariable Long pickCommentId) { + public ResponseEntity> deletePickComment(@PathVariable Long pickId, + @PathVariable Long pickCommentId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); PickCommentResponse pickCommentResponse = pickCommentService.deletePickComment(pickCommentId, pickId, - authentication); + anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentResponse)); } @@ -142,14 +159,14 @@ public ResponseEntity> recommendPick @Operation(summary = "픽픽픽 베스트 댓글 조회", description = "픽픽픽 베스트 댓글을 조회할 수 있습니다.") @GetMapping("/picks/{pickId}/comments/best") public ResponseEntity> getPickBestComments( - @RequestParam(defaultValue = "3") int size, - @PathVariable Long pickId) { + @RequestParam(defaultValue = "3") int size, @PathVariable Long pickId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); PickCommentService pickCommentService = pickServiceStrategy.pickCommentService(); List pickCommentsResponse = pickCommentService.findPickBestComments(size, pickId, - authentication); + anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(pickCommentsResponse)); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickController.java index 1ce1ab1e..ecef1fd7 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickController.java @@ -8,6 +8,7 @@ import com.dreamypatisiel.devdevdev.domain.service.pick.PickServiceStrategy; import com.dreamypatisiel.devdevdev.domain.service.pick.dto.VotePickOptionDto; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import com.dreamypatisiel.devdevdev.global.utils.HttpRequestUtils; import com.dreamypatisiel.devdevdev.openai.data.request.EmbeddingRequest; import com.dreamypatisiel.devdevdev.openai.data.response.Embedding; import com.dreamypatisiel.devdevdev.openai.data.response.OpenAIResponse; @@ -63,10 +64,10 @@ public class PickController { public ResponseEntity>> getPicksMain( @PageableDefault(sort = "id", direction = Direction.DESC) Pageable pageable, @RequestParam(required = false) Long pickId, - @RequestParam(required = false) PickSort pickSort, - @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId) { + @RequestParam(required = false) PickSort pickSort) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); PickService pickService = pickServiceStrategy.getPickService(); Slice response = pickService.findPicksMain(pageable, pickId, pickSort, anonymousMemberId, @@ -158,10 +159,10 @@ public ResponseEntity> getPickDetail(@PathVari @Operation(summary = "픽픽픽 선택지 투표", description = "픽픽픽 상세 페이지에서 픽픽픽 선택지에 투표합니다.") @PostMapping("/picks/vote") public ResponseEntity> votePickOption( - @RequestBody @Validated VotePickOptionRequest votePickOptionRequest, - @RequestHeader(value = HEADER_ANONYMOUS_MEMBER_ID, required = false) String anonymousMemberId) { + @RequestBody @Validated VotePickOptionRequest votePickOptionRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); PickService pickService = pickServiceStrategy.getPickService(); diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordController.java index bac7cebb..c7f3463d 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordController.java @@ -1,35 +1,36 @@ package com.dreamypatisiel.devdevdev.web.controller.techArticle; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticKeywordService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword.TechKeywordService; import com.dreamypatisiel.devdevdev.web.dto.response.BasicResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import java.io.IOException; -import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @Tag(name = "검색어 자동완성 API", description = "검색어 자동완성, 검색어 추가 API") @Slf4j +@Profile({"test", "dev", "prod"}) // local 에서는 검색어 자동완성 불가 @RestController @RequestMapping("/devdevdev/api/v1/keywords") @RequiredArgsConstructor public class KeywordController { - private final ElasticKeywordService elasticKeywordService; + private final TechKeywordService techKeywordService; @Operation(summary = "기술블로그 검색어 자동완성") @GetMapping("/auto-complete") - public ResponseEntity> autocompleteKeyword(@RequestParam String prefix) - throws IOException { - - List response = elasticKeywordService.autocompleteKeyword(prefix); - + public ResponseEntity> autocompleteKeyword( + @RequestParam String prefix + ) { + List response = techKeywordService.autocompleteKeyword(prefix); return ResponseEntity.ok(BasicResponse.success(response)); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentController.java b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentController.java index dce201c3..382547a8 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentController.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/TechArticleCommentController.java @@ -1,9 +1,13 @@ package com.dreamypatisiel.devdevdev.web.controller.techArticle; +import static com.dreamypatisiel.devdevdev.web.WebConstant.HEADER_ANONYMOUS_MEMBER_ID; + import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; import com.dreamypatisiel.devdevdev.domain.service.techArticle.TechArticleServiceStrategy; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.TechCommentService; import com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils; +import com.dreamypatisiel.devdevdev.global.utils.HttpRequestUtils; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; @@ -38,22 +42,26 @@ public class TechArticleCommentController { private final TechArticleServiceStrategy techArticleServiceStrategy; - @Operation(summary = "기술블로그 댓글 작성") + @Operation(summary = "기술블로그 댓글 작성", description = "기술블로그 댓글을 작성할 수 있습니다.") @PostMapping("/articles/{techArticleId}/comments") public ResponseEntity> registerMainTechComment( @PathVariable Long techArticleId, @RequestBody @Validated RegisterTechCommentRequest registerTechCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, + anonymousMemberId); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); TechCommentResponse response = techCommentService.registerMainTechComment(techArticleId, - registerTechCommentRequest, authentication); + registerCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } - @Operation(summary = "기술블로그 답글 작성") + @Operation(summary = "기술블로그 답글 작성", description = "기술블로그 답글을 작성할 수 있습니다.") @PostMapping("/articles/{techArticleId}/comments/{originParentTechCommentId}/{parentTechCommentId}") public ResponseEntity> registerRepliedTechComment( @PathVariable Long techArticleId, @@ -62,16 +70,18 @@ public ResponseEntity> registerRepliedTechCom @RequestBody @Validated RegisterTechCommentRequest registerRepliedTechCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechCommentRequest, + anonymousMemberId); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); TechCommentResponse response = techCommentService.registerRepliedTechComment(techArticleId, - originParentTechCommentId, - parentTechCommentId, registerRepliedTechCommentRequest, authentication); + originParentTechCommentId, parentTechCommentId, registerRepliedCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } - @Operation(summary = "기술블로그 댓글/답글 수정") + @Operation(summary = "기술블로그 댓글/답글 수정", description = "기술블로그 댓글/답글을 수정할 수 있습니다.") @PatchMapping("/articles/{techArticleId}/comments/{techCommentId}") public ResponseEntity> modifyTechComment( @PathVariable Long techArticleId, @@ -79,10 +89,13 @@ public ResponseEntity> modifyTechComment( @RequestBody @Validated ModifyTechCommentRequest modifyTechCommentRequest) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); + TechCommentDto modifyTechCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, + anonymousMemberId); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); TechCommentResponse response = techCommentService.modifyTechComment(techArticleId, techCommentId, - modifyTechCommentRequest, authentication); + modifyTechCommentDto, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } @@ -94,28 +107,29 @@ public ResponseEntity> deleteTechComment( @PathVariable Long techCommentId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); TechCommentResponse response = techCommentService.deleteTechComment(techArticleId, techCommentId, - authentication); + anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } - @Operation(summary = "기술블로그 댓글/답글 조회") + @Operation(summary = "기술블로그 댓글/답글 조회", description = "기술블로그 댓글/답글을 조회할 수 있습니다.") @GetMapping("/articles/{techArticleId}/comments") public ResponseEntity>> getTechComments( @PageableDefault(size = 5) Pageable pageable, @PathVariable Long techArticleId, @RequestParam(required = false) TechCommentSort techCommentSort, - @RequestParam(required = false) Long techCommentId - ) { + @RequestParam(required = false) Long techCommentId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); SliceCustom response = techCommentService.getTechComments(techArticleId, techCommentId, - techCommentSort, pageable, authentication); + techCommentSort, pageable, anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(response)); } @@ -143,10 +157,11 @@ public ResponseEntity> getTechBestComments( @PathVariable Long techArticleId) { Authentication authentication = AuthenticationMemberUtils.getAuthentication(); + String anonymousMemberId = HttpRequestUtils.getHeaderValue(HEADER_ANONYMOUS_MEMBER_ID); TechCommentService techCommentService = techArticleServiceStrategy.getTechCommentService(); List techCommentsResponse = techCommentService.findTechBestComments(size, techArticleId, - authentication); + anonymousMemberId, authentication); return ResponseEntity.ok(BasicResponse.success(techCommentsResponse)); } 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..d554af14 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) Double 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/request/member/ChangeNicknameRequest.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/request/member/ChangeNicknameRequest.java new file mode 100644 index 00000000..b6a8ba75 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/request/member/ChangeNicknameRequest.java @@ -0,0 +1,20 @@ +package com.dreamypatisiel.devdevdev.web.dto.request.member; + +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ChangeNicknameRequest { + @NotBlank(message = "닉네임은 필수입니다.") + private String nickname; + + @Builder + public ChangeNicknameRequest(String nickname) { + this.nickname = nickname; + } +} diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickCommentsResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickCommentsResponse.java index 7929399f..7ed2b106 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickCommentsResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickCommentsResponse.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.web.dto.response.pick; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.PickComment; import com.dreamypatisiel.devdevdev.domain.entity.PickVote; @@ -11,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonFormat.Shape; import java.time.LocalDateTime; import java.util.List; +import javax.annotation.Nullable; import lombok.Builder; import lombok.Data; import org.springframework.util.ObjectUtils; @@ -23,6 +25,7 @@ public class PickCommentsResponse { private LocalDateTime createdAt; private Long memberId; + private Long anonymousMemberId; private String author; private Boolean isCommentOfPickAuthor; private Boolean isCommentAuthor; @@ -38,7 +41,7 @@ public class PickCommentsResponse { private List replies; @Builder - public PickCommentsResponse(Long pickCommentId, LocalDateTime createdAt, Long memberId, String author, + public PickCommentsResponse(Long pickCommentId, LocalDateTime createdAt, Long memberId, Long anonymousMemberId, String author, Boolean isCommentOfPickAuthor, Boolean isCommentAuthor, String maskedEmail, PickOptionType votedPickOption, String votedPickOptionTitle, String contents, Long replyTotalCount, Long recommendTotalCount, Boolean isModified, Boolean isDeleted, @@ -46,6 +49,7 @@ public PickCommentsResponse(Long pickCommentId, LocalDateTime createdAt, Long me this.pickCommentId = pickCommentId; this.createdAt = createdAt; this.memberId = memberId; + this.anonymousMemberId = anonymousMemberId; this.author = author; this.isCommentOfPickAuthor = isCommentOfPickAuthor; this.isCommentAuthor = isCommentAuthor; @@ -61,19 +65,57 @@ public PickCommentsResponse(Long pickCommentId, LocalDateTime createdAt, Long me this.replies = replies; } - public static PickCommentsResponse of(Member member, PickComment originParentPickComment, - List replies) { + public static PickCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + PickComment originParentPickComment, List replies) { Member createdBy = originParentPickComment.getCreatedBy(); + AnonymousMember createdAnonymousBy = originParentPickComment.getCreatedAnonymousBy(); PickVote pickVote = originParentPickComment.getPickVote(); - PickCommentsResponseBuilder responseBuilder = PickCommentsResponse.builder() + PickCommentsResponseBuilder responseBuilder = createPickCommentsResponseBuilder( + member, anonymousMember, originParentPickComment, replies, createdBy, createdAnonymousBy); + + // 회원이 픽픽픽 투표를 안했거나, 투표 비공개일 경우 + if (ObjectUtils.isEmpty(pickVote) || originParentPickComment.isVotePrivate()) { + return responseBuilder.build(); + } + + return responseBuilder + .votedPickOption(pickVote.getPickOption().getPickOptionType()) + .votedPickOptionTitle(pickVote.getPickOption().getTitle().getTitle()) + .build(); + } + + private static PickCommentsResponseBuilder createPickCommentsResponseBuilder(Member member, AnonymousMember anonymousMember, + PickComment originParentPickComment, + List replies, + Member createdBy, + AnonymousMember createdAnonymousBy) { + // 익명회원이 작성한 댓글인 경우 + if (createdBy == null) { + return PickCommentsResponse.builder() + .pickCommentId(originParentPickComment.getId()) + .createdAt(originParentPickComment.getCreatedAt()) + .anonymousMemberId(createdAnonymousBy.getId()) + .author(createdAnonymousBy.getNickname()) + .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(null, originParentPickComment.getPick())) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, originParentPickComment)) + .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, originParentPickComment)) + .contents(CommentResponseUtil.getCommentByPickCommentStatus(originParentPickComment)) + .replyTotalCount((long) replies.size()) + .recommendTotalCount(originParentPickComment.getRecommendTotalCount().getCount()) + .isModified(originParentPickComment.isModified()) + .isDeleted(originParentPickComment.isDeleted()) + .replies(replies); + } + + return PickCommentsResponse.builder() .pickCommentId(originParentPickComment.getId()) .createdAt(originParentPickComment.getCreatedAt()) .memberId(createdBy.getId()) .author(createdBy.getNickname().getNickname()) .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(createdBy, originParentPickComment.getPick())) - .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, originParentPickComment)) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, originParentPickComment)) .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, originParentPickComment)) .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(createdBy.getEmail().getEmail())) .contents(CommentResponseUtil.getCommentByPickCommentStatus(originParentPickComment)) @@ -82,15 +124,5 @@ public static PickCommentsResponse of(Member member, PickComment originParentPic .isModified(originParentPickComment.isModified()) .isDeleted(originParentPickComment.isDeleted()) .replies(replies); - - // 회원이 픽픽픽 투표를 안했거나, 투표 비공개일 경우 - if (ObjectUtils.isEmpty(pickVote) || originParentPickComment.isVotePrivate()) { - return responseBuilder.build(); - } - - return responseBuilder - .votedPickOption(pickVote.getPickOption().getPickOptionType()) - .votedPickOptionTitle(pickVote.getPickOption().getTitle().getTitle()) - .build(); } } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickRepliedCommentsResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickRepliedCommentsResponse.java index d47eee47..96c05599 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickRepliedCommentsResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/pick/PickRepliedCommentsResponse.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.web.dto.response.pick; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.PickComment; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; @@ -16,7 +17,9 @@ public class PickRepliedCommentsResponse { private Long pickCommentId; private Long memberId; + private Long anonymousMemberId; private Long pickParentCommentMemberId; // 부모 댓글의 작성자 회원 아이디 + private Long pickParentCommentAnonymousMemberId; // 부모 댓글의 작성자 익명회원 아이디 private Long pickParentCommentId; private Long pickOriginParentCommentId; @@ -35,17 +38,20 @@ public class PickRepliedCommentsResponse { private Boolean isDeleted; @Builder - public PickRepliedCommentsResponse(Long pickCommentId, Long memberId, Long pickParentCommentMemberId, + public PickRepliedCommentsResponse(Long pickCommentId, Long memberId, Long anonymousMemberId, Long pickParentCommentMemberId, Long pickParentCommentId, Long pickOriginParentCommentId, - LocalDateTime createdAt, Boolean isCommentOfPickAuthor, Boolean isCommentAuthor, + Long pickParentCommentAnonymousMemberId, LocalDateTime createdAt, + Boolean isCommentOfPickAuthor, Boolean isCommentAuthor, Boolean isRecommended, String pickParentCommentAuthor, String author, String maskedEmail, String contents, Long recommendTotalCount, Boolean isModified, Boolean isDeleted) { this.pickCommentId = pickCommentId; this.memberId = memberId; + this.anonymousMemberId = anonymousMemberId; this.pickParentCommentMemberId = pickParentCommentMemberId; this.pickParentCommentId = pickParentCommentId; this.pickOriginParentCommentId = pickOriginParentCommentId; + this.pickParentCommentAnonymousMemberId = pickParentCommentAnonymousMemberId; this.createdAt = createdAt; this.isCommentOfPickAuthor = isCommentOfPickAuthor; this.isCommentAuthor = isCommentAuthor; @@ -59,25 +65,133 @@ public PickRepliedCommentsResponse(Long pickCommentId, Long memberId, Long pickP this.isDeleted = isDeleted; } - // member 가 null 인 경우 익명회원 응답 - public static PickRepliedCommentsResponse of(@Nullable Member member, PickComment repliedPickComment) { + public static PickRepliedCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + PickComment repliedPickComment) { - Member createdBy = repliedPickComment.getCreatedBy(); + // 부모 댓글 PickComment parentPickComment = repliedPickComment.getParent(); + // 부모 댓글/답글 익명회원이 작성한 경우 + if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedPickComment, parentPickComment); + } + + // 부모 댓글은 익명회원이 작성하고 답글은 회원이 작성한 경우 + if (parentPickComment.isCreatedAnonymousMember() && repliedPickComment.isCreatedMember()) { + return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedPickComment, parentPickComment); + } + + // 부모 댓글은 회원이 작성하고 답글은 익명회원이 작성한 경우 + if (parentPickComment.isCreatedMember() && repliedPickComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedPickComment, parentPickComment); + } + + // 부모 댓글/답글 회원이 작성한 경우 + return createResponseForMemberReplyToMember(member, anonymousMember, repliedPickComment, parentPickComment); + } + + private static PickRepliedCommentsResponse createResponseForMemberReplyToMember(Member member, + AnonymousMember anonymousMember, + PickComment repliedPickComment, + PickComment parentPickComment) { + Member parentCreatedBy = parentPickComment.getCreatedBy(); + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + + return PickRepliedCommentsResponse.builder() + .pickCommentId(repliedPickComment.getId()) + .memberId(repliedCreatedBy.getId()) + .pickParentCommentMemberId(parentCreatedBy.getId()) + .author(repliedCreatedBy.getNickname().getNickname()) + .pickParentCommentAuthor(parentCreatedBy.getNicknameAsString()) + .pickParentCommentId(parentPickComment.getId()) + .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) + .createdAt(repliedPickComment.getCreatedAt()) + .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(repliedCreatedBy, repliedPickComment.getPick())) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, repliedPickComment)) + .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, repliedPickComment)) + .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(repliedCreatedBy.getEmail().getEmail())) + .contents(CommentResponseUtil.getCommentByPickCommentStatus(repliedPickComment)) + .recommendTotalCount(repliedPickComment.getRecommendTotalCount().getCount()) + .isModified(repliedPickComment.isModified()) + .isDeleted(repliedPickComment.isDeleted()) + .build(); + } + + private static PickRepliedCommentsResponse createResponseForMemberReplyToAnonymous(Member member, + AnonymousMember anonymousMember, + PickComment repliedPickComment, + PickComment parentPickComment) { + + AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); + Member repliedCreatedBy = repliedPickComment.getCreatedBy(); + + return PickRepliedCommentsResponse.builder() + .pickCommentId(repliedPickComment.getId()) + .memberId(repliedCreatedBy.getId()) + .pickParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) + .author(repliedCreatedBy.getNickname().getNickname()) + .pickParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) + .pickParentCommentId(parentPickComment.getId()) + .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) + .createdAt(repliedPickComment.getCreatedAt()) + .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(repliedCreatedBy, repliedPickComment.getPick())) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, repliedPickComment)) + .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, repliedPickComment)) + .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(repliedCreatedBy.getEmail().getEmail())) + .contents(CommentResponseUtil.getCommentByPickCommentStatus(repliedPickComment)) + .recommendTotalCount(repliedPickComment.getRecommendTotalCount().getCount()) + .isModified(repliedPickComment.isModified()) + .isDeleted(repliedPickComment.isDeleted()) + .build(); + } + + private static PickRepliedCommentsResponse createResponseForAnonymousReplyToMember(Member member, + AnonymousMember anonymousMember, + PickComment repliedPickComment, + PickComment parentPickComment) { + + Member parentCreatedBy = parentPickComment.getCreatedBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); + + return PickRepliedCommentsResponse.builder() + .pickCommentId(repliedPickComment.getId()) + .anonymousMemberId(repliedCreatedAnonymousBy.getId()) + .pickParentCommentAnonymousMemberId(parentCreatedBy.getId()) + .author(repliedCreatedAnonymousBy.getNickname()) + .pickParentCommentAuthor(parentCreatedBy.getNicknameAsString()) + .pickParentCommentId(parentPickComment.getId()) + .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) + .createdAt(repliedPickComment.getCreatedAt()) + .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(null, repliedPickComment.getPick())) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, repliedPickComment)) + .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, repliedPickComment)) + .contents(CommentResponseUtil.getCommentByPickCommentStatus(repliedPickComment)) + .recommendTotalCount(repliedPickComment.getRecommendTotalCount().getCount()) + .isModified(repliedPickComment.isModified()) + .isDeleted(repliedPickComment.isDeleted()) + .build(); + } + + private static PickRepliedCommentsResponse createResponseForAnonymousReplyToAnonymous(Member member, + AnonymousMember anonymousMember, + PickComment repliedPickComment, + PickComment parentPickComment) { + + AnonymousMember parentCreatedAnonymousBy = parentPickComment.getCreatedAnonymousBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedPickComment.getCreatedAnonymousBy(); + return PickRepliedCommentsResponse.builder() .pickCommentId(repliedPickComment.getId()) - .memberId(createdBy.getId()) - .pickParentCommentMemberId(parentPickComment.getCreatedBy().getId()) - .author(createdBy.getNickname().getNickname()) - .pickParentCommentAuthor(parentPickComment.getCreatedBy().getNicknameAsString()) + .anonymousMemberId(repliedCreatedAnonymousBy.getId()) + .pickParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) + .author(repliedCreatedAnonymousBy.getNickname()) + .pickParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) .pickParentCommentId(parentPickComment.getId()) .pickOriginParentCommentId(repliedPickComment.getOriginParent().getId()) .createdAt(repliedPickComment.getCreatedAt()) - .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(createdBy, repliedPickComment.getPick())) - .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, repliedPickComment)) + .isCommentOfPickAuthor(CommentResponseUtil.isPickAuthor(null, repliedPickComment.getPick())) + .isCommentAuthor(CommentResponseUtil.isPickCommentAuthor(member, anonymousMember, repliedPickComment)) .isRecommended(CommentResponseUtil.isPickCommentRecommended(member, repliedPickComment)) - .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(createdBy.getEmail().getEmail())) .contents(CommentResponseUtil.getCommentByPickCommentStatus(repliedPickComment)) .recommendTotalCount(repliedPickComment.getRecommendTotalCount().getCount()) .isModified(repliedPickComment.isModified()) 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..104f9bcb 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; + public final Double 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, Double 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, Double 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, Double 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 Double getValidScore(Double score) { + return Objects.isNull(score) || Double.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/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechCommentsResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechCommentsResponse.java index b2d90017..ddab7215 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechCommentsResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechCommentsResponse.java @@ -1,22 +1,23 @@ package com.dreamypatisiel.devdevdev.web.dto.response.techArticle; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.web.dto.util.CommentResponseUtil; import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Builder; -import lombok.Data; - -import javax.annotation.Nullable; import java.time.LocalDateTime; import java.util.List; +import javax.annotation.Nullable; +import lombok.Builder; +import lombok.Data; @Data public class TechCommentsResponse { private Long techCommentId; private Long memberId; + private Long anonymousMemberId; private String author; private String maskedEmail; private String contents; @@ -32,12 +33,13 @@ public class TechCommentsResponse { private LocalDateTime createdAt; @Builder - public TechCommentsResponse(Long techCommentId, Long memberId, String author, String maskedEmail, String contents, - Long replyTotalCount, Long recommendTotalCount, Boolean isDeleted, Boolean isCommentAuthor, - Boolean isModified, Boolean isRecommended, List replies, - LocalDateTime createdAt) { + public TechCommentsResponse(Long techCommentId, Long memberId, Long anonymousMemberId, String author, String maskedEmail, + String contents, Long replyTotalCount, Long recommendTotalCount, Boolean isDeleted, + Boolean isCommentAuthor, Boolean isModified, Boolean isRecommended, + List replies, LocalDateTime createdAt) { this.techCommentId = techCommentId; this.memberId = memberId; + this.anonymousMemberId = anonymousMemberId; this.author = author; this.maskedEmail = maskedEmail; this.contents = contents; @@ -51,11 +53,48 @@ public TechCommentsResponse(Long techCommentId, Long memberId, String author, St this.createdAt = createdAt; } - public static TechCommentsResponse of(@Nullable Member member, - TechComment originParentTechComment, - List replies) { + public static TechCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + TechComment originParentTechComment, List replies) { + Member createdBy = originParentTechComment.getCreatedBy(); + AnonymousMember createdAnonymousBy = originParentTechComment.getCreatedAnonymousBy(); + + // 회원이 작성한 댓글 응답 + if (originParentTechComment.isCreatedMember()) { + return createTechCommentsResponseByCreatedMember(member, anonymousMember, originParentTechComment, replies, + createdBy); + } + + // 익명회원이 작성한 댓글 응답 + return createTechCommentsResponseByCreatedAnonymousMember(member, anonymousMember, originParentTechComment, replies, + createdAnonymousBy); + } + + private static TechCommentsResponse createTechCommentsResponseByCreatedAnonymousMember(Member member, + AnonymousMember anonymousMember, + TechComment originParentTechComment, + List replies, + AnonymousMember createdAnonymousBy) { + return TechCommentsResponse.builder() + .techCommentId(originParentTechComment.getId()) + .anonymousMemberId(createdAnonymousBy.getId()) + .author(createdAnonymousBy.getNickname()) + .contents(CommentResponseUtil.getCommentByTechCommentStatus(originParentTechComment)) + .replyTotalCount(originParentTechComment.getReplyTotalCount().getCount()) + .recommendTotalCount(originParentTechComment.getRecommendTotalCount().getCount()) + .isDeleted(originParentTechComment.isDeleted()) + .isModified(originParentTechComment.isModified()) + .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, originParentTechComment)) + .createdAt(originParentTechComment.getCreatedAt()) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, originParentTechComment)) + .replies(replies) + .build(); + } + private static TechCommentsResponse createTechCommentsResponseByCreatedMember(Member member, AnonymousMember anonymousMember, + TechComment originParentTechComment, + List replies, + Member createdBy) { return TechCommentsResponse.builder() .techCommentId(originParentTechComment.getId()) .memberId(createdBy.getId()) @@ -68,7 +107,7 @@ public static TechCommentsResponse of(@Nullable Member member, .isModified(originParentTechComment.isModified()) .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, originParentTechComment)) .createdAt(originParentTechComment.getCreatedAt()) - .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, originParentTechComment)) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, originParentTechComment)) .replies(replies) .build(); } diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechRepliedCommentsResponse.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechRepliedCommentsResponse.java index c84eecd1..4cb59073 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechRepliedCommentsResponse.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/response/techArticle/TechRepliedCommentsResponse.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.web.dto.response.techArticle; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; @@ -16,7 +17,9 @@ public class TechRepliedCommentsResponse { private Long techCommentId; private Long memberId; + private Long anonymousMemberId; private Long techParentCommentMemberId; + private Long techParentCommentAnonymousMemberId; // 부모 댓글의 작성자 익명회원 아이디 private Long techParentCommentId; private Long techOriginParentCommentId; @@ -35,15 +38,16 @@ public class TechRepliedCommentsResponse { private LocalDateTime createdAt; @Builder - public TechRepliedCommentsResponse(Long techCommentId, Long memberId, Long techParentCommentMemberId, - Long techParentCommentId, Long techOriginParentCommentId, - Boolean isCommentAuthor, - Boolean isRecommended, String techParentCommentAuthor, String author, - String maskedEmail, String contents, Long recommendTotalCount, Boolean isDeleted, - Boolean isModified, LocalDateTime createdAt) { + public TechRepliedCommentsResponse(Long techCommentId, Long memberId, Long anonymousMemberId, Long techParentCommentMemberId, + Long techParentCommentAnonymousMemberId, Long techParentCommentId, + Long techOriginParentCommentId, Boolean isCommentAuthor, Boolean isRecommended, + String techParentCommentAuthor, String author, String maskedEmail, String contents, + Long recommendTotalCount, Boolean isDeleted, Boolean isModified, LocalDateTime createdAt) { this.techCommentId = techCommentId; this.memberId = memberId; + this.anonymousMemberId = anonymousMemberId; this.techParentCommentMemberId = techParentCommentMemberId; + this.techParentCommentAnonymousMemberId = techParentCommentAnonymousMemberId; this.techParentCommentId = techParentCommentId; this.techOriginParentCommentId = techOriginParentCommentId; this.isCommentAuthor = isCommentAuthor; @@ -58,22 +62,129 @@ public TechRepliedCommentsResponse(Long techCommentId, Long memberId, Long techP this.createdAt = createdAt; } - public static TechRepliedCommentsResponse of(@Nullable Member member, TechComment repliedTechComment) { + public static TechRepliedCommentsResponse of(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + TechComment repliedTechComment) { - Member createdBy = repliedTechComment.getCreatedBy(); - TechComment techParentComment = repliedTechComment.getParent(); + // 부모 댓글 + TechComment parentTechComment = repliedTechComment.getParent(); + + // 부모 댓글/답글 익명회원이 작성한 경우 + if (parentTechComment.isCreatedAnonymousMember() && repliedTechComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToAnonymous(member, anonymousMember, repliedTechComment, parentTechComment); + } + + // 부모 댓글은 익명회원이 작성하고 답글은 회원이 작성한 경우 + if (parentTechComment.isCreatedAnonymousMember() && repliedTechComment.isCreatedMember()) { + return createResponseForMemberReplyToAnonymous(member, anonymousMember, repliedTechComment, parentTechComment); + } + + // 부모 댓글은 회원이 작성하고 답글은 익명회원이 작성한 경우 + if (parentTechComment.isCreatedMember() && repliedTechComment.isCreatedAnonymousMember()) { + return createResponseForAnonymousReplyToMember(member, anonymousMember, repliedTechComment, parentTechComment); + } + + // 부모 댓글/답글 회원이 작성한 경우 + return createResponseForMemberReplyToMember(member, anonymousMember, repliedTechComment, parentTechComment); + } + + private static TechRepliedCommentsResponse createResponseForAnonymousReplyToAnonymous(Member member, + AnonymousMember anonymousMember, + TechComment repliedTechComment, + TechComment parentTechComment) { + + AnonymousMember parentCreatedAnonymousBy = parentTechComment.getCreatedAnonymousBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedTechComment.getCreatedAnonymousBy(); + + return TechRepliedCommentsResponse.builder() + .techCommentId(repliedTechComment.getId()) + .anonymousMemberId(repliedCreatedAnonymousBy.getId()) + .author(repliedCreatedAnonymousBy.getNickname()) + .techParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) + .techParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) + .techParentCommentId(repliedTechComment.getParent().getId()) + .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) + .createdAt(repliedTechComment.getCreatedAt()) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, repliedTechComment)) + .contents(CommentResponseUtil.getCommentByTechCommentStatus(repliedTechComment)) + .recommendTotalCount(repliedTechComment.getRecommendTotalCount().getCount()) + .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, repliedTechComment)) + .isDeleted(repliedTechComment.isDeleted()) + .isModified(repliedTechComment.isModified()) + .build(); + } + + private static TechRepliedCommentsResponse createResponseForMemberReplyToAnonymous(Member member, + AnonymousMember anonymousMember, + TechComment repliedTechComment, + TechComment parentTechComment) { + + AnonymousMember parentCreatedAnonymousBy = parentTechComment.getCreatedAnonymousBy(); + Member repliedCreatedBy = repliedTechComment.getCreatedBy(); + + return TechRepliedCommentsResponse.builder() + .techCommentId(repliedTechComment.getId()) + .memberId(repliedCreatedBy.getId()) + .author(repliedCreatedBy.getNickname().getNickname()) + .techParentCommentAnonymousMemberId(parentCreatedAnonymousBy.getId()) + .techParentCommentAuthor(parentCreatedAnonymousBy.getNickname()) + .techParentCommentId(repliedTechComment.getParent().getId()) + .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) + .createdAt(repliedTechComment.getCreatedAt()) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, repliedTechComment)) + .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(repliedCreatedBy.getEmail().getEmail())) + .contents(CommentResponseUtil.getCommentByTechCommentStatus(repliedTechComment)) + .recommendTotalCount(repliedTechComment.getRecommendTotalCount().getCount()) + .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, repliedTechComment)) + .isDeleted(repliedTechComment.isDeleted()) + .isModified(repliedTechComment.isModified()) + .build(); + } + + private static TechRepliedCommentsResponse createResponseForMemberReplyToMember(Member member, + AnonymousMember anonymousMember, + TechComment repliedTechComment, + TechComment parentTechComment) { + + Member parentCreatedBy = parentTechComment.getCreatedBy(); + Member repliedCreatedBy = repliedTechComment.getCreatedBy(); + + return TechRepliedCommentsResponse.builder() + .techCommentId(repliedTechComment.getId()) + .memberId(repliedCreatedBy.getId()) + .author(repliedCreatedBy.getNickname().getNickname()) + .techParentCommentMemberId(parentCreatedBy.getId()) + .techParentCommentAuthor(parentCreatedBy.getNicknameAsString()) + .techParentCommentId(repliedTechComment.getParent().getId()) + .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) + .createdAt(repliedTechComment.getCreatedAt()) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, repliedTechComment)) + .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(repliedCreatedBy.getEmail().getEmail())) + .contents(CommentResponseUtil.getCommentByTechCommentStatus(repliedTechComment)) + .recommendTotalCount(repliedTechComment.getRecommendTotalCount().getCount()) + .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, repliedTechComment)) + .isDeleted(repliedTechComment.isDeleted()) + .isModified(repliedTechComment.isModified()) + .build(); + } + + private static TechRepliedCommentsResponse createResponseForAnonymousReplyToMember(Member member, + AnonymousMember anonymousMember, + TechComment repliedTechComment, + TechComment parentTechComment) { + + Member parentCreatedBy = parentTechComment.getCreatedBy(); + AnonymousMember repliedCreatedAnonymousBy = repliedTechComment.getCreatedAnonymousBy(); return TechRepliedCommentsResponse.builder() .techCommentId(repliedTechComment.getId()) - .memberId(createdBy.getId()) - .author(createdBy.getNickname().getNickname()) - .techParentCommentMemberId(techParentComment.getCreatedBy().getId()) - .techParentCommentAuthor(techParentComment.getCreatedBy().getNicknameAsString()) + .anonymousMemberId(repliedCreatedAnonymousBy.getId()) + .author(repliedCreatedAnonymousBy.getNickname()) + .techParentCommentMemberId(parentCreatedBy.getId()) + .techParentCommentAuthor(parentCreatedBy.getNicknameAsString()) .techParentCommentId(repliedTechComment.getParent().getId()) .techOriginParentCommentId(repliedTechComment.getOriginParent().getId()) .createdAt(repliedTechComment.getCreatedAt()) - .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, repliedTechComment)) - .maskedEmail(CommonResponseUtil.sliceAndMaskEmail(createdBy.getEmail().getEmail())) + .isCommentAuthor(CommentResponseUtil.isTechCommentAuthor(member, anonymousMember, repliedTechComment)) .contents(CommentResponseUtil.getCommentByTechCommentStatus(repliedTechComment)) .recommendTotalCount(repliedTechComment.getRecommendTotalCount().getCount()) .isRecommended(CommentResponseUtil.isTechCommentRecommendedByMember(member, repliedTechComment)) diff --git a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/util/CommentResponseUtil.java b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/util/CommentResponseUtil.java index e8c66ec1..a651afa0 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/web/dto/util/CommentResponseUtil.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/web/dto/util/CommentResponseUtil.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.web.dto.util; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.Pick; import com.dreamypatisiel.devdevdev.domain.entity.PickComment; @@ -10,29 +11,87 @@ import javax.annotation.Nullable; public class CommentResponseUtil { - + + public static final String DELETE_COMMENT_MESSAGE = "댓글 작성자에 의해 삭제된 댓글입니다."; + public static final String DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE = "커뮤니티 정책을 위반하여 삭제된 댓글입니다."; + public static final String CONTACT_ADMIN_MESSAGE = "오류가 발생 했습니다. 관리자에게 문의 하세요."; + public static String getCommentByPickCommentStatus(PickComment pickComment) { - if (pickComment.isDeleted()) { - // 댓글 작성자에 의해 삭제된 경우 - if (pickComment.getDeletedBy().isEqualsId(pickComment.getCreatedBy().getId())) { - return "댓글 작성자에 의해 삭제된 댓글입니다."; + + if (!pickComment.isDeleted()) { + return pickComment.getContents().getCommentContents(); + } + + // 익명회원이 작성한 댓글인 경우 + if (pickComment.isCreatedAnonymousMember()) { + // 자기자신이 삭제한 경우 + if (pickComment.isDeletedByAnonymousMember()) { + return DELETE_COMMENT_MESSAGE; + } + + // 어드민이 삭제한 경우 + if (pickComment.getDeletedBy().isAdmin()) { + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + } + + return CONTACT_ADMIN_MESSAGE; + } + + // 회원이 작성한 댓글인 경우 + if (pickComment.isCreatedMember()) { + // 자기 자신인 경우 + if (pickComment.isDeletedMemberByMySelf()) { + return DELETE_COMMENT_MESSAGE; } - return "커뮤니티 정책을 위반하여 삭제된 댓글입니다."; + + // 어드민이 삭제한 경우 + if (pickComment.getDeletedBy().isAdmin()) { + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + } + + return CONTACT_ADMIN_MESSAGE; } - return pickComment.getContents().getCommentContents(); + return CONTACT_ADMIN_MESSAGE; } public static String getCommentByTechCommentStatus(TechComment techComment) { - if (techComment.isDeleted()) { - // 댓글 작성자에 의해 삭제된 경우 - if (techComment.getDeletedBy().isEqualsId(techComment.getCreatedBy().getId())) { - return "댓글 작성자에 의해 삭제된 댓글입니다."; + // 기술블로그 댓글이 삭제되지 않은 경우 + if (!techComment.isDeleted()) { + return techComment.getContents().getCommentContents(); + } + + // 익명회원이 작성한 기술블로그 댓글인 경우 + if (techComment.isCreatedAnonymousMember()) { + // 자기자신이 삭제한 경우 + if (techComment.isDeletedByAnonymousMember()) { + return DELETE_COMMENT_MESSAGE; } - return "커뮤니티 정책을 위반하여 삭제된 댓글입니다."; + + // 어드민이 삭제한 경우 + if (techComment.getDeletedBy().isAdmin()) { + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + } + + return CONTACT_ADMIN_MESSAGE; } - return techComment.getContents().getCommentContents(); + // 회원이 작성한 기술블로그 댓글인 경우 + if (techComment.isCreatedMember()) { + // 자기 자신이 삭제한 경우 + if (techComment.isDeletedByMember()) { + return DELETE_COMMENT_MESSAGE; + } + + // 어드민이 삭제한 경우 + if (techComment.getDeletedBy().isAdmin()) { + return DELETE_INVALID_COMMUNITY_POLICY_COMMENT_MESSAGE; + } + + return CONTACT_ADMIN_MESSAGE; + } + + return CONTACT_ADMIN_MESSAGE; } public static boolean isDeletedByAdmin(PickComment pickComment) { @@ -49,12 +108,21 @@ public static boolean isPickAuthor(@Nullable Member member, Pick pick) { return pick.getMember().isEqualsId(member.getId()); } - public static boolean isPickCommentAuthor(@Nullable Member member, PickComment pickComment) { - // member 가 null 인 경우 익명회원이 조회한 것 - if (member == null) { - return false; + public static boolean isPickCommentAuthor(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + PickComment pickComment) { + + // 회원이 조회하고 픽픽픽 댓글을 회원이 작성한 경우 + if (member != null && pickComment.isCreatedMember()) { + // 픽픽픽 댓글을 회원이 작성한 경우 + return pickComment.getCreatedBy().isEqualsId(member.getId()); + } + + // 익명회원이 조회하고 픽픽픽 댓글을 익명회원이 작성한 경우 + if (anonymousMember != null && pickComment.isCreatedAnonymousMember()) { + return pickComment.getCreatedAnonymousBy().isEqualAnonymousMemberId(anonymousMember.getId()); } - return pickComment.getCreatedBy().isEqualsId(member.getId()); + + return false; } public static boolean isPickCommentRecommended(@Nullable Member member, PickComment pickComment) { @@ -68,12 +136,19 @@ public static boolean isPickCommentRecommended(@Nullable Member member, PickComm .anyMatch(pickCommentRecommend -> pickCommentRecommend.getMember().isEqualsId(member.getId())); } - public static boolean isTechCommentAuthor(Member member, TechComment techComment) { - // member 가 null 인 경우 익명회원이 조회한 것 - if (member == null) { - return false; + public static boolean isTechCommentAuthor(@Nullable Member member, @Nullable AnonymousMember anonymousMember, + TechComment techComment) { + // 회원이 조회하고 기술블로그 댓글을 회원이 작성한 경우 + if (member != null && techComment.isCreatedMember()) { + return techComment.getCreatedBy().isEqualsId(member.getId()); + } + + // 익명회원이 조회하고 기술블로그 댓글을 익명회원이 작성한 경우 + if (anonymousMember != null && techComment.isCreatedAnonymousMember()) { + return techComment.getCreatedAnonymousBy().isEqualAnonymousMemberId(anonymousMember.getId()); } - return techComment.getCreatedBy().isEqualsId(member.getId()); + + return false; } public static boolean isTechCommentRecommendedByMember(@Nullable Member member, TechComment techComment) { diff --git a/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor b/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor new file mode 100644 index 00000000..74b9d6ff --- /dev/null +++ b/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor @@ -0,0 +1 @@ +com.dreamypatisiel.devdevdev.global.config.CustomMySQLFunctionContributor diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 8c612903..10d39c46 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,3 +1,8 @@ +nickname: + change: + interval: + minutes: 1 + bucket: plan: local 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/entity/MemberTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java new file mode 100644 index 00000000..76124f35 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/entity/MemberTest.java @@ -0,0 +1,57 @@ +package com.dreamypatisiel.devdevdev.domain.entity; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class MemberTest { + + @ParameterizedTest + @CsvSource({ + ", true", // 변경 이력 없음(null) + "60, false", // 24시간 이내 + "1439, false", // 24시간 이내 + "1440, true", // 24시간 경과(경계) + "1550, true", // 24시간 초과 + }) + @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") + void canChangeNickname(Long minutesAgo, boolean expected) { + // given + LocalDateTime now = LocalDateTime.now(); + Member member = new Member(); + if (minutesAgo != null) { + member.changeNickname("닉네임", now.minusMinutes(minutesAgo)); + } + int restrictionMinutes = 1440; // 24시간 + // when + boolean result = member.canChangeNickname(restrictionMinutes, now); + // then + assertThat(result).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + ", true", // 변경 이력 없음(null) + "0, false", // 24시간 이내 + "1, true", // 24시간 이내 + "60, true", // 24시간 경과(경계) + "1440, true", // 24시간 초과 + }) + @DisplayName("닉네임 변경 가능 여부 파라미터 테스트") + void canChangeNicknameWhenDev(Long minutesAgo, boolean expected) { + // given + LocalDateTime now = LocalDateTime.now(); + Member member = new Member(); + if (minutesAgo != null) { + member.changeNickname("닉네임", now.minusMinutes(minutesAgo)); + } + int restrictionMinutes = 1; // 1분 + // when + boolean result = member.canChangeNickname(restrictionMinutes, now); + // then + assertThat(result).isEqualTo(expected); + } +} 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/blame/MemberPickBlameServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/blame/MemberPickBlameServiceTest.java index 20c4dee6..b2b700cb 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/blame/MemberPickBlameServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/blame/MemberPickBlameServiceTest.java @@ -390,7 +390,7 @@ void blamePickCommentIsDeleted() { // 삭제 상태의 픽픽픽 댓글 생성 PickComment pickComment = createPickComment(pick, member, "픽픽픽 댓글"); - pickComment.changeDeletedAt(LocalDateTime.now(), member); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(pickComment); BlamePickDto blamePickDto = new BlamePickDto(pick.getId(), pickComment.getId(), 0L, null); 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 80716cf4..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 @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -15,7 +16,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; -import com.dreamypatisiel.devdevdev.domain.exception.CompanyExceptionMessage; +import com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage; import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; @@ -31,10 +32,11 @@ 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; import com.dreamypatisiel.devdevdev.global.common.MemberProvider; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; @@ -50,15 +52,19 @@ 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.ArrayList; import java.util.List; import java.util.Optional; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.auditing.AuditingHandler; import org.springframework.data.auditing.DateTimeProvider; import org.springframework.data.domain.PageRequest; @@ -68,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 = "꿈빛파티시엘"; @@ -116,6 +124,8 @@ class MemberServiceTest extends ElasticsearchSupportTest { PickCommentRepository pickCommentRepository; @Autowired SubscriptionRepository subscriptionRepository; + @MockBean + TimeProvider timeProvider; @Test @DisplayName("회원이 회원탈퇴 설문조사를 완료하지 않으면 탈퇴가 불가능하다.") @@ -415,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(); @@ -455,6 +472,8 @@ void getBookmarkedTechArticlesNotFoundMemberException() { @DisplayName("회원탈퇴 서베이 이력을 기록한다.") void recordMemberExitSurveyAnswer() { // given + when(timeProvider.getLocalDateTimeNow()).thenReturn(LocalDateTime.of(2024, 1, 1, 0, 0, 0, 0)); + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); Member member = Member.createMemberBy(socialMemberDto); memberRepository.save(member); @@ -1177,6 +1196,108 @@ void findMySubscribedCompaniesNotFoundMemberException() { .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } + @Test + @DisplayName("회원은 닉네임을 변경할 수 있다.") + void changeNickname() { + // given + String oldNickname = "이전 닉네임"; + String newNickname = "변경된 닉네임"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, 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 + String changedNickname = memberService.changeNickname(newNickname, authentication); + + // then + assertThat(member.getNickname().getNickname()).isEqualTo(newNickname); + assertThat(changedNickname).isEqualTo(newNickname); + } + + @DisplayName("회원이 1440분(24시간) 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + @ParameterizedTest + @CsvSource({ + "0, true", + "60, true", // 1시간 + "1439, true", // 23.9시간 + "1440, false", // 24시간, 변경 허용 + "1500, false" // 25시간, 변경 허용 + }) + void changeNicknameThrowsExceptionWhenChangedWithin24Hours(long minutesAgo, boolean shouldThrowException) { + // given + LocalDateTime fixedNow = LocalDateTime.of(2024, 1, 1, 12, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(fixedNow); + + String oldNickname = "이전 닉네임"; + String newNickname = "새 닉네임"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + member.changeNickname(oldNickname, fixedNow.minusMinutes(minutesAgo)); + 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 + if (shouldThrowException) { + assertThatThrownBy(() -> memberService.changeNickname(newNickname, authentication)) + .isInstanceOf(NicknameException.class) + .hasMessageContaining(NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE); + } else { + assertThatCode(() -> memberService.changeNickname(newNickname, authentication)) + .doesNotThrowAnyException(); + assertThat(member.getNickname().getNickname()).isEqualTo(newNickname); + } + } + + @DisplayName("회원의 닉네임 변경 가능 여부를 반환한다.") + @ParameterizedTest + @CsvSource({ + "0, false", + "60, false", // 1시간 + "1439, false", // 23.9시간 + "1440, true", // 24시간 + "1500, true" // 25시간 + }) + void canChangeNickname(long minutesAgo, boolean expected) { + // given + LocalDateTime fixedNow = LocalDateTime.of(2024, 1, 1, 12, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(fixedNow); + + String oldNickname = "이전 닉네임"; + String newNickname = "새 닉네임"; + + SocialMemberDto socialMemberDto = createSocialDto(userId, name, oldNickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + member.changeNickname(newNickname, fixedNow.minusMinutes(minutesAgo)); + 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 + boolean result = memberService.canChangeNickname(authentication); + + // then + assertThat(result).isEqualTo(expected); + } + private static Company createCompany(String companyName, String officialUrl, String careerUrl, String imageUrl, String description, String industry) { return Company.builder() @@ -1203,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(); } @@ -1321,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/pick/GuestPickCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceTest.java index db3c8d4a..6aa216a5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceTest.java @@ -1,5 +1,12 @@ package com.dreamypatisiel.devdevdev.domain.service.pick; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPick; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickCommentRecommend; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOption; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickVote; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createReplidPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createSocialDto; import static com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -158,7 +165,7 @@ void findPickCommentsByPickCommentSort(PickCommentSort pickCommentSort) { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -172,7 +179,7 @@ void findPickCommentsByPickCommentSort(PickCommentSort pickCommentSort) { // when Pageable pageable = PageRequest.of(0, 5); SliceCustom response = guestPickCommentService.findPickComments(pageable, - pick.getId(), Long.MAX_VALUE, pickCommentSort, null, authentication); + pick.getId(), Long.MAX_VALUE, pickCommentSort, null, null, authentication); // then // 최상위 댓글 검증 @@ -459,7 +466,7 @@ void findPickCommentsByPickCommentSortAndFirstPickOption(PickCommentSort pickCom Pageable pageable = PageRequest.of(0, 5); SliceCustom response = guestPickCommentService.findPickComments(pageable, pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.firstPickOption), - authentication); + null, authentication); // then // 최상위 댓글 검증 @@ -683,7 +690,7 @@ void findPickCommentsByPickCommentSortAndSecondPickOption(PickCommentSort pickCo Pageable pageable = PageRequest.of(0, 5); SliceCustom response = guestPickCommentService.findPickComments(pageable, pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), - authentication); + null, authentication); // then // 최상위 댓글 검증 @@ -746,7 +753,7 @@ void findPickCommentsNotAnonymousMember(PickCommentSort pickCommentSort) { // when // then assertThatThrownBy(() -> guestPickCommentService.findPickComments(pageable, - 1L, Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), authentication)) + 1L, Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } @@ -766,7 +773,7 @@ void findPickBestCommentsNotAnonymousMember() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy(() -> guestPickCommentService.findPickBestComments(3, 1L, authentication)) + assertThatThrownBy(() -> guestPickCommentService.findPickBestComments(3, 1L, null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } @@ -837,7 +844,7 @@ void findPickBestComments() { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -854,8 +861,7 @@ void findPickBestComments() { Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - List response = guestPickCommentService.findPickBestComments(3, pick.getId(), - authentication); + List response = guestPickCommentService.findPickBestComments(3, pick.getId(), null, authentication); // then // 최상위 댓글 검증 @@ -997,121 +1003,4 @@ void findPickBestComments() { pickReply3.getParent().getCreatedBy().getNicknameAsString()) ); } - - private PickVote createPickVote(Member member, PickOption pickOption, Pick pick) { - PickVote pickVote = PickVote.builder() - .member(member) - .build(); - - pickVote.changePickOption(pickOption); - pickVote.changePick(pick); - - return pickVote; - } - - private Pick createPick(Title title, ContentStatus contentStatus, Count viewTotalCount, Count voteTotalCount, - Count commentTotalCount, Count popularScore, Member member) { - return Pick.builder() - .title(title) - .contentStatus(contentStatus) - .viewTotalCount(viewTotalCount) - .voteTotalCount(voteTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .member(member) - .build(); - } - - private PickComment createPickComment(CommentContents contents, Boolean isPublic, Count recommendTotalCount, - Member member, Pick pick) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .isPublic(isPublic) - .createdBy(member) - .recommendTotalCount(recommendTotalCount) - .pick(pick) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickCommentRecommend createPickCommentRecommend(PickComment pickComment, Member member, - Boolean recommendedStatus) { - return PickCommentRecommend.builder() - .pickComment(pickComment) - .member(member) - .recommendedStatus(recommendedStatus) - .build(); - } - - private Pick createPick(Title title, ContentStatus contentStatus, Count commentTotalCount, Member member) { - return Pick.builder() - .title(title) - .contentStatus(contentStatus) - .commentTotalCount(commentTotalCount) - .member(member) - .build(); - } - - private PickComment createPickComment(CommentContents contents, Boolean isPublic, Count replyTotalCount, - Count recommendTotalCount, Member member, Pick pick, PickVote pickVote) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .isPublic(isPublic) - .createdBy(member) - .replyTotalCount(replyTotalCount) - .recommendTotalCount(recommendTotalCount) - .pick(pick) - .pickVote(pickVote) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickComment createReplidPickComment(CommentContents contents, Member member, Pick pick, - PickComment originParent, PickComment parent) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .createdBy(member) - .pick(pick) - .originParent(originParent) - .isPublic(false) - .parent(parent) - .recommendTotalCount(new Count(0)) - .replyTotalCount(new Count(0)) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickOption createPickOption(Title title, Count voteTotalCount, Pick pick, PickOptionType pickOptionType) { - PickOption pickOption = PickOption.builder() - .title(title) - .voteTotalCount(voteTotalCount) - .pickOptionType(pickOptionType) - .build(); - - pickOption.changePick(pick); - - return pickOption; - } - - 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(); - } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java new file mode 100644 index 00000000..29bf9648 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/GuestPickCommentServiceV2Test.java @@ -0,0 +1,2095 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick; + +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.PickExceptionMessage.INVALID_NOT_FOUND_PICK_VOTE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickCommentService.DELETE; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickCommentService.MODIFY; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickCommentService.REGISTER; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPick; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickCommentRecommend; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOption; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOptionImage; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickVote; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createReplidPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createSocialDto; +import static com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazonaws.services.s3.AmazonS3; +import com.dreamypatisiel.devdevdev.aws.s3.AwsS3Uploader; +import com.dreamypatisiel.devdevdev.aws.s3.properties.AwsS3Properties; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.Member; +import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.PickComment; +import com.dreamypatisiel.devdevdev.domain.entity.PickCommentRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.PickOption; +import com.dreamypatisiel.devdevdev.domain.entity.PickOptionImage; +import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.PickOptionContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; +import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; +import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; +import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; +import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.domain.policy.PickPopularScorePolicy; +import com.dreamypatisiel.devdevdev.domain.repository.member.AnonymousMemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRecommendRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentSort; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickOptionImageRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickOptionRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; +import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +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 com.dreamypatisiel.devdevdev.web.dto.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickRepliedCommentsResponse; +import com.dreamypatisiel.devdevdev.web.dto.util.CommentResponseUtil; +import com.dreamypatisiel.devdevdev.web.dto.util.CommonResponseUtil; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.time.LocalDateTime; +import java.util.EnumSet; +import java.util.List; +import org.assertj.core.groups.Tuple; +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.junit.jupiter.params.provider.EnumSource.Mode; +import org.junit.jupiter.params.provider.ValueSource; +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.security.core.Authentication; +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 +@Transactional +class GuestPickCommentServiceV2Test { + + @Autowired + GuestPickCommentServiceV2 guestPickCommentServiceV2; + @Autowired + PickRepository pickRepository; + @Autowired + PickOptionRepository pickOptionRepository; + @Autowired + PickVoteRepository pickVoteRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + PickOptionImageRepository pickOptionImageRepository; + @Autowired + PickPopularScorePolicy pickPopularScorePolicy; + @Autowired + PickCommentRepository pickCommentRepository; + @Autowired + PickCommentRecommendRepository pickCommentRecommendRepository; + @Autowired + AnonymousMemberRepository anonymousMemberRepository; + + @PersistenceContext + EntityManager em; + @Autowired + AwsS3Uploader awsS3Uploader; + @Autowired + AwsS3Properties awsS3Properties; + @Autowired + AmazonS3 amazonS3Client; + + String userId = "dreamy5patisiel"; + String name = "꿈빛파티시엘"; + String nickname = "행복한 꿈빛파티시엘"; + String email = "dreamy5patisiel@kakao.com"; + String password = "password"; + String socialType = SocialType.KAKAO.name(); + String role = Role.ROLE_USER.name(); + String author = "운영자"; + + @Test + @DisplayName("익명회원이 승인상태의 픽픽픽에 선택지 투표 공개 댓글을 작성한다.") + void registerPickCommentWithPickMainVote() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), author); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1 타이틀"), + new PickOptionContents("픽픽픽 옵션1 컨텐츠"), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2 타이틀"), + new PickOptionContents("픽픽픽 옵션2 컨텐츠"), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 이미지 생성 + PickOptionImage firstPickOptionImage = createPickOptionImage("firstPickOptionImage", firstPickOption); + PickOptionImage secondPickOptionImage = createPickOptionImage("secondPickOptionImage", firstPickOption); + pickOptionImageRepository.saveAll(List.of(firstPickOptionImage, secondPickOptionImage)); + + // 픽픽픽 투표 생성 + PickVote pickVote = createPickVote(anonymousMember, firstPickOption, pick); + pickVoteRepository.save(pickVote); + + em.flush(); + em.clear(); + + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // when + PickCommentResponse pickCommentResponse = guestPickCommentServiceV2.registerPickComment(pick.getId(), pickCommentDto, + authentication); + + // then + assertThat(pickCommentResponse.getPickCommentId()).isNotNull(); + + PickComment findPickComment = pickCommentRepository.findById(pickCommentResponse.getPickCommentId()).get(); + assertAll( + () -> assertThat(findPickComment.getContents().getCommentContents()).isEqualTo("안녕하세웅"), + () -> assertThat(findPickComment.getIsPublic()).isEqualTo(true), + () -> assertThat(findPickComment.getDeletedAt()).isNull(), + () -> assertThat(findPickComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getPick().getId()).isEqualTo(pick.getId()), + () -> assertThat(findPickComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findPickComment.getPickVote().getId()).isEqualTo(pickVote.getId()) + ); + } + + @Test + @DisplayName("익명회원이 승인상태의 픽픽픽에 선택지 투표 비공개 댓글을 작성한다.") + void registerPickCommentWithOutPickMainVote() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), author); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1 타이틀"), + new PickOptionContents("픽픽픽 옵션1 컨텐츠"), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2 타이틀"), + new PickOptionContents("픽픽픽 옵션2 컨텐츠"), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 이미지 생성 + PickOptionImage firstPickOptionImage = createPickOptionImage("firstPickOptionImage", firstPickOption); + PickOptionImage secondPickOptionImage = createPickOptionImage("secondPickOptionImage", firstPickOption); + pickOptionImageRepository.saveAll(List.of(firstPickOptionImage, secondPickOptionImage)); + + // 픽픽픽 투표 생성 + PickVote pickVote = createPickVote(anonymousMember, firstPickOption, pick); + pickVoteRepository.save(pickVote); + + em.flush(); + em.clear(); + + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", false, "anonymousMemberId"); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // when + PickCommentResponse pickCommentResponse = guestPickCommentServiceV2.registerPickComment(pick.getId(), pickCommentDto, + authentication); + + // then + assertThat(pickCommentResponse.getPickCommentId()).isNotNull(); + + PickComment findPickComment = pickCommentRepository.findById(pickCommentResponse.getPickCommentId()).get(); + assertAll( + () -> assertThat(findPickComment.getContents().getCommentContents()).isEqualTo("안녕하세웅"), + () -> assertThat(findPickComment.getIsPublic()).isEqualTo(false), + () -> assertThat(findPickComment.getDeletedAt()).isNull(), + () -> assertThat(findPickComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getPick().getId()).isEqualTo(pick.getId()), + () -> assertThat(findPickComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findPickComment.getPickVote()).isNull() + ); + } + + @Test + @DisplayName("픽픽픽 익명회원 댓글을 작성할 때 익명회원이 아니면 예외가 발생한다.") + void registerPickCommentIllegalStateException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + 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 + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + + // then + assertThatThrownBy(() -> guestPickCommentServiceV2.registerPickComment(0L, pickCommentDto, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 작성할 때 픽픽픽이 존재하지 않으면 예외가 발생한다.") + void registerPickCommentPickMainNotFoundException() { + // given + // 익명회원 생성 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // when + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + + // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickComment(1L, pickCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_MESSAGE); + } + + @ParameterizedTest + @EnumSource(value = ContentStatus.class, mode = Mode.EXCLUDE, names = {"APPROVAL"}) + @DisplayName("익명회원이 픽픽픽 댓글을 작성할 때 픽픽픽이 승인상태가 아니면 예외가 발생한다.") + void registerPickCommentNotApproval(ContentStatus contentStatus) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), contentStatus, new Count(0L), new Count(0L), new Count(0L), + new Count(0L), author); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1 타이틀"), + new PickOptionContents("픽픽픽 옵션1 컨텐츠"), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2 타이틀"), + new PickOptionContents("픽픽픽 옵션2 컨텐츠"), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + em.flush(); + em.clear(); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickComment(pick.getId(), pickCommentDto, authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, REGISTER); + } + + @Test + @DisplayName("익명회원이 승인상태의 픽픽픽에 선택지 투표 공개 댓글을 작성할 때 픽픽픽 선택지 투표 이력이 없으면 예외가 발생한다.") + void registerPickCommentNotFoundPickMainVote() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), author); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(pick, new Title("픽픽픽 옵션1 타이틀"), + new PickOptionContents("픽픽픽 옵션1 컨텐츠"), PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(pick, new Title("픽픽픽 옵션2 타이틀"), + new PickOptionContents("픽픽픽 옵션2 컨텐츠"), PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + em.flush(); + em.clear(); + + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickComment(pick.getId(), pickCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("익명회원이 승인상태의 픽픽픽의 삭제상태가 아닌 댓글에 답글을 작성한다.") + void registerPickRepliedComment(Boolean isPublic) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("댓글1"), isPublic, author, pick); + pickCommentRepository.save(pickComment); + + // 픽픽픽 답글 생성 + PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글1"), anonymousMember, pick, + pickComment, pickComment); + pickCommentRepository.save(replidPickComment); + + em.flush(); + em.clear(); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when + PickCommentResponse response = guestPickCommentServiceV2.registerPickRepliedComment( + replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication); + + em.flush(); + em.clear(); + + // then + assertThat(response.getPickCommentId()).isNotNull(); + + PickComment findPickComment = pickCommentRepository.findById(response.getPickCommentId()).get(); + assertAll( + () -> assertThat(findPickComment.getContents().getCommentContents()).isEqualTo("댓글1의 답글1의 답글"), + () -> assertThat(findPickComment.getIsPublic()).isFalse(), + () -> assertThat(findPickComment.getDeletedAt()).isNull(), + () -> assertThat(findPickComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findPickComment.getPick().getId()).isEqualTo(pick.getId()), + () -> assertThat(findPickComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findPickComment.getParent().getId()).isEqualTo(replidPickComment.getId()), + () -> assertThat(findPickComment.getOriginParent().getId()).isEqualTo(pickComment.getId()), + () -> assertThat(findPickComment.getOriginParent().getReplyTotalCount().getCount()).isEqualTo(1L) + ); + } + + @Test + @DisplayName("회원이 익명회원 전용 픽픽픽 답글을 작성할 때 예외가 발생한다.") + void registerPickRepliedCommentMemberException() { + // given + // 회원 생성 + 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(); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment(0L, 0L, 0L, repliedCommentDto, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 답글을 작성할 때, 답글 대상의 댓글이 존재하지 않으면 예외가 발생한다.") + void registerPickRepliedCommentNotFoundExceptionParent() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, member); + pickRepository.save(pick); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment(0L, 0L, 0L, repliedCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @EnumSource(value = ContentStatus.class, mode = Mode.EXCLUDE, names = {"APPROVAL"}) + @DisplayName("익명회원이 픽픽픽 답글을 작성할 때, 픽픽픽이 승인상태가 아니면 예외가 발생한다.") + void registerPickRepliedCommentPickIsNotApproval(ContentStatus contentStatus) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), contentStatus, member); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); + pickCommentRepository.save(pickComment); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment( + pickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE, REGISTER); + } + + @Test + @DisplayName("익명회원이 픽픽픽 답글을 작성할 때 답글 대상의 댓글이 삭제 상태 이면 예외가 발생한다." + + "(최초 댓글이 삭제상태이고 해당 댓글에 답글을 작성하는 경우)") + void registerPickRepliedCommentDeleted() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), member); + pickRepository.save(pick); + + // 삭제상태의 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); + pickCommentRepository.save(pickComment); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment( + pickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); + } + + @Test + @DisplayName("익명회원이 픽픽픽 답글을 작성할 때 답글 대상의 댓글이 삭제 상태 이면 예외가 발생한다." + + "(최초 댓글의 답글이 삭제상태이고 그 답글에 답글을 작성하는 경우)") + void registerPickRepliedCommentRepliedDeleted() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(0L), new Count(0L), + new Count(0L), new Count(0L), member); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); + pickCommentRepository.save(pickComment); + + // 삭제상태의 픽픽픽 댓글의 답글 생성 + PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, + pickComment, pickComment); + replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); + pickCommentRepository.save(replidPickComment); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment( + replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); + } + + @Test + @DisplayName("익명회원이 픽픽픽 답글을 작성할 때 답글 대상의 댓글이 존재하지 않으면 예외가 발생한다." + + "(최초 댓글의 답글이 존재하지 않고 그 답글에 답글을 작성하는 경우)") + void registerPickRepliedCommentRepliedNotFoundException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, member); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); + pickCommentRepository.save(pickComment); + + // 삭제상태의 픽픽픽 댓글의 답글(삭제 상태) + PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, + pickComment, pickComment); + pickCommentRepository.save(replidPickComment); + replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); + + RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy( + () -> guestPickCommentServiceV2.registerPickRepliedComment( + 0L, pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("승인 상태이고 익명회원 본인이 작성한 삭제되지 않은 픽픽픽 댓글을 수정한다.") + void modifyPickComment(boolean isPublic) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), isPublic, anonymousMember, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, anonymousMember.getAnonymousMemberId()); + + // when + PickCommentResponse response = guestPickCommentServiceV2.modifyPickComment(pickComment.getId(), + pick.getId(), modifyCommentDto, authentication); + + // then + PickComment findPickComment = pickCommentRepository.findById(pickComment.getId()).get(); + assertAll( + () -> assertThat(response.getPickCommentId()).isEqualTo(pickComment.getId()), + () -> assertThat(findPickComment.getContents().getCommentContents()).isEqualTo(request.getContents()), + () -> assertThat(findPickComment.getContentsLastModifiedAt()).isNotNull() + ); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 수정할 때 익명회원 전용 메소드를 호출하지 않으면 예외가 발생한다.") + void modifyPickCommentMemberException() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, "anonymousMemberId"); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.modifyPickComment(0L, 0L, modifyCommentDto, + authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 수정할 때 댓글이 존재하지 않으면 예외가 발생한다.") + void modifyPickCommentNotFoundPickComment() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 승인 상태 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.modifyPickComment(0L, pick.getId(), modifyCommentDto, + authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 수정할 때 본인이 작성한 댓글이 존재하지 않으면 예외가 발생한다.") + void modifyPickCommentNotFoundPickCommentOtherMember() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 승인 상태 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성(다른 사람이 작성) + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, author, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, + authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 수정할 때 댓글이 삭제 상태이면 예외가 발생한다.") + void modifyPickCommentNotFoundPickCommentIsDeletedAt() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 승인 상태 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 삭제 상태의 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, anonymousMember, pick); + pickComment.changeDeletedAtByAnonymousMember(LocalDateTime.now(), anonymousMember); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, + authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @EnumSource(value = ContentStatus.class, mode = Mode.EXCLUDE, names = {"APPROVAL"}) + @DisplayName("익명회원이 승인 상태가 아닌 픽픽픽 댓글을 수정할 때 예외가 발생한다.") + void modifyPickCommentNotApproval(ContentStatus contentStatus) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 승인 상태가 아닌 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), contentStatus, author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, anonymousMember, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, + authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); + } + + @ParameterizedTest + @EnumSource(PickCommentSort.class) + @DisplayName("익명회원이 픽픽픽 모든 댓글/답글을 알맞게 정렬하여 커서 방식으로 조회한다.") + void findPickCommentsByPickCommentSort(PickCommentSort pickCommentSort) { + // given + // 회원 생성 + SocialMemberDto socialMemberDto1 = createSocialDto("user1", name, "nickname1", password, "user1@gmail.com", + socialType, Role.ROLE_ADMIN.name()); + SocialMemberDto socialMemberDto2 = createSocialDto("user2", name, "nickname2", password, "user2@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto3 = createSocialDto("user3", name, "nickname3", password, "user3@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto4 = createSocialDto("user4", name, "nickname4", password, "user4@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto5 = createSocialDto("user5", name, "nickname5", password, "user5@gmail.com", + socialType, role); + Member member1 = Member.createMemberBy(socialMemberDto1); + Member member2 = Member.createMemberBy(socialMemberDto2); + Member member3 = Member.createMemberBy(socialMemberDto3); + Member member4 = Member.createMemberBy(socialMemberDto4); + Member member5 = Member.createMemberBy(socialMemberDto5); + memberRepository.saveAll(List.of(member1, member2, member3, member4, member5)); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(6), member1); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(new Title("픽픽픽 옵션1"), new Count(0), pick, + PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(new Title("픽픽픽 옵션2"), new Count(0), pick, + PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 투표 생성 + PickVote member1PickVote = createPickVote(member1, firstPickOption, pick); + PickVote member2PickVote = createPickVote(member2, firstPickOption, pick); + PickVote member3PickVote = createPickVote(member3, secondPickOption, pick); + PickVote member4PickVote = createPickVote(member4, secondPickOption, pick); + pickVoteRepository.saveAll(List.of(member1PickVote, member2PickVote, member3PickVote, member4PickVote)); + + // 픽픽픽 최초 댓글 생성 + PickComment originParentPickComment1 = createPickComment(new CommentContents("댓글1"), true, new Count(2), + new Count(2), member1, pick, member1PickVote); + originParentPickComment1.modifyCommentContents(new CommentContents("댓글1 수정"), LocalDateTime.now()); + PickComment originParentPickComment2 = createPickComment(new CommentContents("댓글2"), true, new Count(1), + new Count(1), member2, pick, member2PickVote); + PickComment originParentPickComment3 = createPickComment(new CommentContents("댓글3"), true, new Count(0), + new Count(0), member3, pick, member3PickVote); + PickComment originParentPickComment4 = createPickComment(new CommentContents("댓글4"), false, new Count(0), + new Count(0), member4, pick, member4PickVote); + PickComment originParentPickComment5 = createPickComment(new CommentContents("익명회원이 작성한 댓글5"), false, new Count(0), + new Count(0), anonymousMember, pick, null); + PickComment originParentPickComment6 = createPickComment(new CommentContents("익명회원이 작성한 댓글6"), false, new Count(0), + new Count(0), anonymousMember, pick, null); + pickCommentRepository.saveAll( + List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, + originParentPickComment3, originParentPickComment2, originParentPickComment1)); + + // 픽픽픽 답글 생성 + PickComment pickReply1 = createReplidPickComment(new CommentContents("댓글1 답글1"), member1, pick, + originParentPickComment1, originParentPickComment1); + PickComment pickReply2 = createReplidPickComment(new CommentContents("익명회원이 작성한 답글1 답글1"), anonymousMember, pick, + originParentPickComment1, pickReply1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); // 삭제 상태로 변경 + PickComment pickReply3 = createReplidPickComment(new CommentContents("익명회원이 작성한 댓글2 답글1"), anonymousMember, pick, + originParentPickComment2, originParentPickComment2); + pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); + + em.flush(); + em.clear(); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // when + Pageable pageable = PageRequest.of(0, 5); + SliceCustom response = guestPickCommentServiceV2.findPickComments(pageable, + pick.getId(), Long.MAX_VALUE, pickCommentSort, null, anonymousMember.getAnonymousMemberId(), authentication); + + // then + // 최상위 댓글 검증 + assertThat(response).hasSize(5) + .extracting( + "pickCommentId", + "memberId", + "anonymousMemberId", + "author", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "maskedEmail", + "votedPickOption", + "votedPickOptionTitle", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isDeleted", + "isModified") + .containsExactly( + Tuple.tuple(originParentPickComment1.getId(), + originParentPickComment1.getCreatedBy().getId(), + null, + originParentPickComment1.getCreatedBy().getNickname().getNickname(), + true, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment1.getCreatedBy().getEmail().getEmail()), + originParentPickComment1.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment1.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment1.getContents().getCommentContents(), + originParentPickComment1.getReplyTotalCount().getCount(), + originParentPickComment1.getRecommendTotalCount().getCount(), + false, + true), + + Tuple.tuple(originParentPickComment2.getId(), + originParentPickComment2.getCreatedBy().getId(), + null, + originParentPickComment2.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment2.getCreatedBy().getEmail().getEmail()), + originParentPickComment2.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment2.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment2.getContents().getCommentContents(), + originParentPickComment2.getReplyTotalCount().getCount(), + originParentPickComment2.getRecommendTotalCount().getCount(), + false, + false), + + Tuple.tuple(originParentPickComment3.getId(), + originParentPickComment3.getCreatedBy().getId(), + null, + originParentPickComment3.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment3.getCreatedBy().getEmail().getEmail()), + originParentPickComment3.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment3.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment3.getContents().getCommentContents(), + originParentPickComment3.getReplyTotalCount().getCount(), + originParentPickComment3.getRecommendTotalCount().getCount(), + false, + false), + + Tuple.tuple(originParentPickComment4.getId(), + originParentPickComment4.getCreatedBy().getId(), + null, + originParentPickComment4.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment4.getCreatedBy().getEmail().getEmail()), + null, + null, + originParentPickComment4.getContents().getCommentContents(), + originParentPickComment4.getReplyTotalCount().getCount(), + originParentPickComment4.getRecommendTotalCount().getCount(), + false, + false), + + Tuple.tuple(originParentPickComment5.getId(), + null, + originParentPickComment5.getCreatedAnonymousBy().getId(), + originParentPickComment5.getCreatedAnonymousBy().getNickname(), + false, + true, + false, + null, + null, + null, + originParentPickComment5.getContents().getCommentContents(), + originParentPickComment5.getReplyTotalCount().getCount(), + originParentPickComment5.getRecommendTotalCount().getCount(), + false, + false) + ); + + // 첫 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse1 = response.getContent().get(0); + List replies1 = pickCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(2) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply1.getId(), + pickReply1.getCreatedBy().getId(), + null, + pickReply1.getParent().getId(), + pickReply1.getOriginParent().getId(), + true, + false, + false, + pickReply1.getCreatedBy().getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(pickReply1.getCreatedBy().getEmail().getEmail()), + pickReply1.getContents().getCommentContents(), + pickReply1.getRecommendTotalCount().getCount(), + false, + false, + pickReply1.getParent().getCreatedBy().getId(), + null, + pickReply1.getParent().getCreatedBy().getNickname().getNickname()), + + Tuple.tuple(pickReply2.getId(), + null, + pickReply2.getCreatedAnonymousBy().getId(), + pickReply2.getParent().getId(), + pickReply2.getOriginParent().getId(), + false, + true, + false, + pickReply2.getCreatedAnonymousBy().getNickname(), + null, + CommentResponseUtil.getCommentByPickCommentStatus(pickReply2), + pickReply2.getRecommendTotalCount().getCount(), + true, + false, + null, + pickReply2.getParent().getCreatedBy().getId(), + pickReply2.getParent().getCreatedBy().getNickname().getNickname()) + ); + + // 두 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse2 = response.getContent().get(1); + List replies2 = pickCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(1) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply3.getId(), + null, + pickReply3.getCreatedAnonymousBy().getId(), + pickReply3.getParent().getId(), + pickReply3.getOriginParent().getId(), + false, + true, + false, + pickReply3.getCreatedAnonymousBy().getNickname(), + null, + pickReply3.getContents().getCommentContents(), + pickReply3.getRecommendTotalCount().getCount(), + false, + false, + null, + pickReply3.getParent().getCreatedBy().getId(), + pickReply3.getParent().getCreatedBy().getNickname().getNickname()) + ); + + // 세 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse3 = response.getContent().get(2); + List replies3 = pickCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + // 네 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse4 = response.getContent().get(3); + List replies4 = pickCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + // 다섯 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse5 = response.getContent().get(4); + List replies5 = pickCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + } + + @ParameterizedTest + @EnumSource(PickCommentSort.class) + @DisplayName("익명회원이 픽픽픽 모든 첫 번째 픽픽픽 옵션에 투표한 댓글/답글을 알맞게 정렬하여 커서 방식으로 조회한다.") + void findPickCommentsByPickCommentSortAndFirstPickOption(PickCommentSort pickCommentSort) { + // given + // 회원 생성 + SocialMemberDto socialMemberDto1 = createSocialDto("user1", name, "nickname1", password, "user1@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto2 = createSocialDto("user2", name, "nickname2", password, "user2@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto3 = createSocialDto("user3", name, "nickname3", password, "user3@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto4 = createSocialDto("user4", name, "nickname4", password, "user4@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto5 = createSocialDto("user5", name, "nickname5", password, "user5@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto6 = createSocialDto("user6", name, "nickname6", password, "user6@gmail.com", + socialType, role); + Member member1 = Member.createMemberBy(socialMemberDto1); + Member member2 = Member.createMemberBy(socialMemberDto2); + Member member3 = Member.createMemberBy(socialMemberDto3); + Member member4 = Member.createMemberBy(socialMemberDto4); + Member member5 = Member.createMemberBy(socialMemberDto5); + Member member6 = Member.createMemberBy(socialMemberDto6); + memberRepository.saveAll(List.of(member1, member2, member3, member4, member5, member6)); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(9), member1); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(new Title("픽픽픽 옵션1"), new Count(0), pick, + PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(new Title("픽픽픽 옵션2"), new Count(0), pick, + PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 투표 생성 + PickVote member1PickVote = createPickVote(member1, firstPickOption, pick); + PickVote member2PickVote = createPickVote(member2, firstPickOption, pick); + PickVote member3PickVote = createPickVote(member3, secondPickOption, pick); + PickVote member4PickVote = createPickVote(member4, secondPickOption, pick); + pickVoteRepository.saveAll(List.of(member1PickVote, member2PickVote, member3PickVote, member4PickVote)); + + // 픽픽픽 최초 댓글 생성 + PickComment originParentPickComment1 = createPickComment(new CommentContents("댓글1"), true, new Count(2), + new Count(2), anonymousMember, pick, member1PickVote); + originParentPickComment1.modifyCommentContents(new CommentContents("댓글1 수정"), LocalDateTime.now()); + PickComment originParentPickComment2 = createPickComment(new CommentContents("댓글2"), true, new Count(1), + new Count(1), member2, pick, member2PickVote); + PickComment originParentPickComment3 = createPickComment(new CommentContents("댓글3"), true, new Count(0), + new Count(0), member3, pick, member3PickVote); + PickComment originParentPickComment4 = createPickComment(new CommentContents("댓글4"), false, new Count(0), + new Count(0), member4, pick, member4PickVote); + PickComment originParentPickComment5 = createPickComment(new CommentContents("댓글5"), false, new Count(0), + new Count(0), member5, pick, null); + PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), + new Count(0), member6, pick, null); + pickCommentRepository.saveAll( + List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, + originParentPickComment3, originParentPickComment2, originParentPickComment1)); + + // 픽픽픽 답글 생성 + PickComment pickReply1 = createReplidPickComment(new CommentContents("댓글1 답글1"), anonymousMember, pick, + originParentPickComment1, originParentPickComment1); + PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, + originParentPickComment1, pickReply1); + PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, + originParentPickComment2, originParentPickComment2); + pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); + + em.flush(); + em.clear(); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // when + Pageable pageable = PageRequest.of(0, 5); + SliceCustom response = guestPickCommentServiceV2.findPickComments(pageable, + pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.firstPickOption), + anonymousMember.getAnonymousMemberId(), authentication); + + // then + // 최상위 댓글 검증 + assertThat(response).hasSize(2) + .extracting( + "pickCommentId", + "memberId", + "anonymousMemberId", + "author", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "maskedEmail", + "votedPickOption", + "votedPickOptionTitle", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isDeleted", + "isModified") + .containsExactly( + Tuple.tuple(originParentPickComment1.getId(), + null, + originParentPickComment1.getCreatedAnonymousBy().getId(), + originParentPickComment1.getCreatedAnonymousBy().getNickname(), + false, + true, + false, + null, + originParentPickComment1.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment1.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment1.getContents().getCommentContents(), + originParentPickComment1.getReplyTotalCount().getCount(), + originParentPickComment1.getRecommendTotalCount().getCount(), + false, + true), + + Tuple.tuple(originParentPickComment2.getId(), + originParentPickComment2.getCreatedBy().getId(), + null, + originParentPickComment2.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment2.getCreatedBy().getEmail().getEmail()), + originParentPickComment2.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment2.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment2.getContents().getCommentContents(), + originParentPickComment2.getReplyTotalCount().getCount(), + originParentPickComment2.getRecommendTotalCount().getCount(), + false, + false) + ); + + // 첫 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse1 = response.getContent().get(0); + List replies1 = pickCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(2) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply1.getId(), + null, + pickReply1.getCreatedAnonymousBy().getId(), + pickReply1.getParent().getId(), + pickReply1.getOriginParent().getId(), + false, + true, + false, + pickReply1.getCreatedAnonymousBy().getNickname(), + null, + pickReply1.getContents().getCommentContents(), + pickReply1.getRecommendTotalCount().getCount(), + false, + false, + null, + pickReply1.getParent().getCreatedAnonymousBy().getId(), + pickReply1.getParent().getCreatedAnonymousBy().getNickname()), + + Tuple.tuple(pickReply2.getId(), + pickReply2.getCreatedBy().getId(), + null, + pickReply2.getParent().getId(), + pickReply2.getOriginParent().getId(), + false, + false, + false, + pickReply2.getCreatedBy().getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(pickReply2.getCreatedBy().getEmail().getEmail()), + pickReply2.getContents().getCommentContents(), + pickReply2.getRecommendTotalCount().getCount(), + false, + false, + null, + pickReply2.getParent().getCreatedAnonymousBy().getId(), + pickReply2.getParent().getCreatedAnonymousBy().getNickname()) + ); + + // 두 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse2 = response.getContent().get(1); + List replies2 = pickCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(1) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply3.getId(), + pickReply3.getCreatedBy().getId(), + null, + pickReply3.getParent().getId(), + pickReply3.getOriginParent().getId(), + false, + false, + false, + pickReply3.getCreatedBy().getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(pickReply3.getCreatedBy().getEmail().getEmail()), + pickReply3.getContents().getCommentContents(), + pickReply3.getRecommendTotalCount().getCount(), + false, + false, + pickReply3.getParent().getCreatedBy().getId(), + null, + pickReply3.getParent().getCreatedBy().getNickname().getNickname()) + ); + } + + @ParameterizedTest + @EnumSource(PickCommentSort.class) + @DisplayName("익명회원이 픽픽픽 모든 두 번째 픽픽픽 옵션에 투표한 댓글/답글을 알맞게 정렬하여 커서 방식으로 조회한다.") + void findPickCommentsByPickCommentSortAndSecondPickOption(PickCommentSort pickCommentSort) { + // given + // 회원 생성 + SocialMemberDto socialMemberDto1 = createSocialDto("user1", name, "nickname1", password, "user1@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto2 = createSocialDto("user2", name, "nickname2", password, "user2@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto3 = createSocialDto("user3", name, "nickname3", password, "user3@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto4 = createSocialDto("user4", name, "nickname4", password, "user4@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto5 = createSocialDto("user5", name, "nickname5", password, "user5@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto6 = createSocialDto("user6", name, "nickname6", password, "user6@gmail.com", + socialType, role); + Member member1 = Member.createMemberBy(socialMemberDto1); + Member member2 = Member.createMemberBy(socialMemberDto2); + Member member3 = Member.createMemberBy(socialMemberDto3); + Member member4 = Member.createMemberBy(socialMemberDto4); + Member member5 = Member.createMemberBy(socialMemberDto5); + Member member6 = Member.createMemberBy(socialMemberDto6); + memberRepository.saveAll(List.of(member1, member2, member3, member4, member5, member6)); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(6), member1); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(new Title("픽픽픽 옵션1"), new Count(0), pick, + PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(new Title("픽픽픽 옵션2"), new Count(0), pick, + PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 투표 생성 + PickVote member1PickVote = createPickVote(member1, firstPickOption, pick); + PickVote member2PickVote = createPickVote(member2, firstPickOption, pick); + PickVote member3PickVote = createPickVote(member3, secondPickOption, pick); + PickVote member4PickVote = createPickVote(member4, secondPickOption, pick); + pickVoteRepository.saveAll(List.of(member1PickVote, member2PickVote, member3PickVote, member4PickVote)); + + // 픽픽픽 최초 댓글 생성 + PickComment originParentPickComment1 = createPickComment(new CommentContents("댓글1"), true, new Count(2), + new Count(2), anonymousMember, pick, member1PickVote); + PickComment originParentPickComment2 = createPickComment(new CommentContents("댓글2"), true, new Count(1), + new Count(1), member2, pick, member2PickVote); + PickComment originParentPickComment3 = createPickComment(new CommentContents("댓글3"), true, new Count(0), + new Count(0), member3, pick, member3PickVote); + PickComment originParentPickComment4 = createPickComment(new CommentContents("댓글4"), false, new Count(0), + new Count(0), member4, pick, member4PickVote); + PickComment originParentPickComment5 = createPickComment(new CommentContents("댓글5"), false, new Count(0), + new Count(0), member5, pick, null); + PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), + new Count(0), member6, pick, null); + pickCommentRepository.saveAll( + List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, + originParentPickComment3, originParentPickComment2, originParentPickComment1)); + + // 픽픽픽 답글 생성 + PickComment pickReply1 = createReplidPickComment(new CommentContents("댓글1 답글1"), member1, pick, + originParentPickComment1, originParentPickComment1); + PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, + originParentPickComment1, pickReply1); + PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, + originParentPickComment2, originParentPickComment2); + pickCommentRepository.saveAll(List.of(pickReply3, pickReply2, pickReply1)); + + em.flush(); + em.clear(); + + // when + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + Pageable pageable = PageRequest.of(0, 5); + SliceCustom response = guestPickCommentServiceV2.findPickComments(pageable, + pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), + anonymousMember.getAnonymousMemberId(), authentication); + + // then + // 최상위 댓글 검증 + assertThat(response).hasSize(1) + .extracting( + "pickCommentId", + "memberId", + "anonymousMemberId", + "author", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "maskedEmail", + "votedPickOption", + "votedPickOptionTitle", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isDeleted", + "isModified") + .containsExactly( + Tuple.tuple(originParentPickComment3.getId(), + originParentPickComment3.getCreatedBy().getId(), + null, + originParentPickComment3.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment3.getCreatedBy().getEmail().getEmail()), + originParentPickComment3.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment3.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment3.getContents().getCommentContents(), + originParentPickComment3.getReplyTotalCount().getCount(), + originParentPickComment3.getRecommendTotalCount().getCount(), + false, + false) + ); + + // 첫 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse1 = response.getContent().get(0); + List replies1 = pickCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(0); + } + + @ParameterizedTest + @EnumSource(PickCommentSort.class) + @DisplayName("익명회원이 아닌 경우 익명회원 전용 픽픽픽 댓글/답글 조회 메소드를 호출하면 예외가 발생한다.") + void findPickCommentsNotAnonymousMember(PickCommentSort pickCommentSort) { + // given + 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(); + + Pageable pageable = PageRequest.of(0, 5); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.findPickComments(pageable, + 1L, Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), null, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명 회원이 아닌 경우 익명회원 전용 픽픽픽 베스트 댓글 조회 메소드를 호출하면 예외가 발생한다.") + void findPickBestCommentsNotAnonymousMember() { + // given + 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(() -> guestPickCommentServiceV2.findPickBestComments(3, 1L, "anonymousMemberId", authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명 회원이 offset에 정책에 맞게 픽픽픽 베스트 댓글을 조회한다.(추천수가 1개 이상인 댓글 부터 최대 3개가 조회된다.)") + void findPickBestComments() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto1 = createSocialDto("user1", name, "nickname1", password, "user1@gmail.com", + socialType, Role.ROLE_ADMIN.name()); + SocialMemberDto socialMemberDto2 = createSocialDto("user2", name, "nickname2", password, "user2@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto3 = createSocialDto("user3", name, "nickname3", password, "user3@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto4 = createSocialDto("user4", name, "nickname4", password, "user4@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto5 = createSocialDto("user5", name, "nickname5", password, "user5@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto6 = createSocialDto("user6", name, "nickname6", password, "user6@gmail.com", + socialType, role); + Member member1 = Member.createMemberBy(socialMemberDto1); + Member member2 = Member.createMemberBy(socialMemberDto2); + Member member3 = Member.createMemberBy(socialMemberDto3); + Member member4 = Member.createMemberBy(socialMemberDto4); + Member member5 = Member.createMemberBy(socialMemberDto5); + Member member6 = Member.createMemberBy(socialMemberDto6); + memberRepository.saveAll(List.of(member1, member2, member3, member4, member5, member6)); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, new Count(6), member1); + pickRepository.save(pick); + + // 픽픽픽 옵션 생성 + PickOption firstPickOption = createPickOption(new Title("픽픽픽 옵션1"), new Count(0), pick, + PickOptionType.firstPickOption); + PickOption secondPickOption = createPickOption(new Title("픽픽픽 옵션2"), new Count(0), pick, + PickOptionType.secondPickOption); + pickOptionRepository.saveAll(List.of(firstPickOption, secondPickOption)); + + // 픽픽픽 투표 생성 + PickVote member1PickVote = createPickVote(member1, firstPickOption, pick); + PickVote member2PickVote = createPickVote(member2, firstPickOption, pick); + PickVote member3PickVote = createPickVote(member3, secondPickOption, pick); + PickVote member4PickVote = createPickVote(member4, secondPickOption, pick); + pickVoteRepository.saveAll(List.of(member1PickVote, member2PickVote, member3PickVote, member4PickVote)); + + // 픽픽픽 최초 댓글 생성 + PickComment originParentPickComment1 = createPickComment(new CommentContents("댓글1"), true, new Count(2), + new Count(3), anonymousMember, pick, member1PickVote); + originParentPickComment1.modifyCommentContents(new CommentContents("수정된 댓글1"), LocalDateTime.now()); + PickComment originParentPickComment2 = createPickComment(new CommentContents("댓글2"), true, new Count(1), + new Count(2), member2, pick, member2PickVote); + PickComment originParentPickComment3 = createPickComment(new CommentContents("댓글3"), true, new Count(0), + new Count(0), member3, pick, member3PickVote); + PickComment originParentPickComment4 = createPickComment(new CommentContents("댓글4"), false, new Count(0), + new Count(0), member4, pick, member4PickVote); + PickComment originParentPickComment5 = createPickComment(new CommentContents("댓글5"), false, new Count(0), + new Count(0), member5, pick, null); + PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), + new Count(0), member6, pick, null); + pickCommentRepository.saveAll( + List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, + originParentPickComment3, originParentPickComment2, originParentPickComment1)); + + // 픽픽픽 답글 생성 + PickComment pickReply1 = createReplidPickComment(new CommentContents("댓글1 답글1"), anonymousMember, pick, + originParentPickComment1, originParentPickComment1); + PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, + originParentPickComment1, pickReply1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); + PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, + originParentPickComment2, originParentPickComment2); + pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); + + // 추천 생성 + PickCommentRecommend pickCommentRecommend = createPickCommentRecommend(originParentPickComment1, member1, true); + pickCommentRecommendRepository.save(pickCommentRecommend); + + em.flush(); + em.clear(); + + // when + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + List response = guestPickCommentServiceV2.findPickBestComments(3, pick.getId(), + anonymousMember.getAnonymousMemberId(), authentication); + + // then + // 최상위 댓글 검증 + assertThat(response).hasSize(2) + .extracting( + "pickCommentId", + "memberId", + "anonymousMemberId", + "author", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "maskedEmail", + "votedPickOption", + "votedPickOptionTitle", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isDeleted", + "isModified") + .containsExactly( + Tuple.tuple(originParentPickComment1.getId(), + null, + originParentPickComment1.getCreatedAnonymousBy().getId(), + originParentPickComment1.getCreatedAnonymousBy().getNickname(), + false, + true, + false, + null, + originParentPickComment1.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment1.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment1.getContents().getCommentContents(), + originParentPickComment1.getReplyTotalCount().getCount(), + originParentPickComment1.getRecommendTotalCount().getCount(), + false, + true), + + Tuple.tuple(originParentPickComment2.getId(), + originParentPickComment2.getCreatedBy().getId(), + null, + originParentPickComment2.getCreatedBy().getNickname().getNickname(), + false, + false, + false, + CommonResponseUtil.sliceAndMaskEmail( + originParentPickComment2.getCreatedBy().getEmail().getEmail()), + originParentPickComment2.getPickVote().getPickOption().getPickOptionType(), + originParentPickComment2.getPickVote().getPickOption().getTitle().getTitle(), + originParentPickComment2.getContents().getCommentContents(), + originParentPickComment2.getReplyTotalCount().getCount(), + originParentPickComment2.getRecommendTotalCount().getCount(), + false, + false) + ); + + // 첫 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse1 = response.get(0); + List replies1 = pickCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(2) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply1.getId(), + null, + pickReply1.getCreatedAnonymousBy().getId(), + pickReply1.getParent().getId(), + pickReply1.getOriginParent().getId(), + false, + true, + false, + pickReply1.getCreatedAnonymousBy().getNickname(), + null, + pickReply1.getContents().getCommentContents(), + pickReply1.getRecommendTotalCount().getCount(), + false, + false, + null, + pickReply1.getParent().getCreatedAnonymousBy().getId(), + pickReply1.getParent().getCreatedAnonymousBy().getNickname()), + + Tuple.tuple(pickReply2.getId(), + pickReply2.getCreatedBy().getId(), + null, + pickReply2.getParent().getId(), + pickReply2.getOriginParent().getId(), + false, + false, + false, + pickReply2.getCreatedBy().getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(pickReply2.getCreatedBy().getEmail().getEmail()), + CommentResponseUtil.getCommentByPickCommentStatus(pickReply2), + pickReply2.getRecommendTotalCount().getCount(), + true, + false, + null, + pickReply1.getParent().getCreatedAnonymousBy().getId(), + pickReply1.getParent().getCreatedAnonymousBy().getNickname()) + ); + + // 두 번째 최상위 댓글의 답글 검증 + PickCommentsResponse pickCommentsResponse2 = response.get(1); + List replies2 = pickCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(1) + .extracting("pickCommentId", + "memberId", + "anonymousMemberId", + "pickParentCommentId", + "pickOriginParentCommentId", + "isCommentOfPickAuthor", + "isCommentAuthor", + "isRecommended", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isDeleted", + "isModified", + "pickParentCommentMemberId", + "pickParentCommentAnonymousMemberId", + "pickParentCommentAuthor") + .containsExactly( + Tuple.tuple(pickReply3.getId(), + pickReply3.getCreatedBy().getId(), + null, + pickReply3.getParent().getId(), + pickReply3.getOriginParent().getId(), + false, + false, + false, + pickReply3.getCreatedBy().getNickname().getNickname(), + CommonResponseUtil.sliceAndMaskEmail(pickReply3.getCreatedBy().getEmail().getEmail()), + pickReply3.getContents().getCommentContents(), + pickReply3.getRecommendTotalCount().getCount(), + false, + false, + pickReply3.getParent().getCreatedBy().getId(), + null, + pickReply3.getParent().getCreatedBy().getNicknameAsString()) + ); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("승인 상태의 픽픽픽에 포함되어 있는 삭제 상태가 아닌 댓글을 익명회원 본인이 삭제한다.") + void deletePickComment(boolean isPublic) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), isPublic, anonymousMember, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + // when + PickCommentResponse response = guestPickCommentServiceV2.deletePickComment(pickComment.getId(), + pick.getId(), anonymousMember.getAnonymousMemberId(), authentication); + + // then + PickComment findPickComment = pickCommentRepository.findById(pickComment.getId()).get(); + assertAll( + () -> assertThat(response.getPickCommentId()).isEqualTo(pickComment.getId()), + () -> assertThat(findPickComment.getDeletedAt()).isNotNull(), + () -> assertThat(findPickComment.getDeletedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()) + ); + } + + @Test + @DisplayName("익명회원 전용 댓글을 삭제할 때 익명회원이 아니면 예외가 발생한다.") + void deletePickComment() { + // given + // 회원 생성 + 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(); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + 0L, 0L, "anonymousMemberId", authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원이 픽픽픽 댓글을 삭제할 때 픽픽픽 댓글이 존재하지 않으면 예외가 발생한다.") + void deletePickCommentNotFoundPickComment() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + 0L, pick.getId(), anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("익명회원이 픽픽픽 댓글을 삭제할 때 본인이 작성한 픽픽픽 댓글이 아니면 예외가 발생한다.") + void deletePickCommentNotFoundPickCommentByMember() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 다른 회원이 직성한 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, author, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + pickComment.getId(), pick.getId(), anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("익명회원이 픽픽픽 댓글을 삭제할 때 픽픽픽이 존재하지 않으면 예외가 발생한다.") + void deletePickCommentNotFoundPick(boolean isPublic) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 다른 회원이 직성한 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), isPublic, anonymousMember, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + pickComment.getId(), 0L, anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } + + @ParameterizedTest + @EnumSource(value = ContentStatus.class, mode = Mode.EXCLUDE, names = {"APPROVAL"}) + @DisplayName("익명회원이 픽픽픽 댓글을 삭제할 때 승인상태의 픽픽픽이 존재하지 않으면 예외가 발생한다.") + void deletePickCommentNotFoundApprovalPick(ContentStatus contentStatus) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), contentStatus, author); + pickRepository.save(pick); + + // 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, anonymousMember, pick); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + pickComment.getId(), pick.getId(), anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, DELETE); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("익명회원이 픽픽픽 댓글을 삭제할 때 삭제 상태인 픽픽픽 댓글을 삭제하려고 하면 예외가 발생한다.") + void deletePickCommentNotFoundPickCommentByDeletedAtIsNull(boolean isPublic) { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 픽픽픽 작성자 생성 + SocialMemberDto authorSocialMemberDto = createSocialDto("authorId", "author", + nickname, password, "authorDreamy5patisiel@kakao.com", socialType, role); + Member author = Member.createMemberBy(authorSocialMemberDto); + memberRepository.save(author); + + // 픽픽픽 생성 + Pick pick = createPick(new Title("픽픽픽 타이틀"), ContentStatus.APPROVAL, author); + pickRepository.save(pick); + + // 삭제 상태의 픽픽픽 댓글 생성 + PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), isPublic, anonymousMember, pick); + pickComment.changeDeletedAtByAnonymousMember(LocalDateTime.now(), anonymousMember); + pickCommentRepository.save(pickComment); + + em.flush(); + em.clear(); + + // when // then + assertThatThrownBy(() -> guestPickCommentServiceV2.deletePickComment( + pickComment.getId(), pick.getId(), anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); + } +} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentServiceTest.java index 48900318..b746199d 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/MemberPickCommentServiceTest.java @@ -12,6 +12,14 @@ import static com.dreamypatisiel.devdevdev.domain.service.pick.MemberPickCommentService.MODIFY; import static com.dreamypatisiel.devdevdev.domain.service.pick.MemberPickCommentService.RECOMMEND; import static com.dreamypatisiel.devdevdev.domain.service.pick.MemberPickCommentService.REGISTER; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPick; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickCommentRecommend; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOption; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickOptionImage; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createPickVote; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createReplidPickComment; +import static com.dreamypatisiel.devdevdev.domain.service.pick.PickTestUtils.createSocialDto; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -43,18 +51,14 @@ import com.dreamypatisiel.devdevdev.domain.repository.pick.PickOptionRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickVoteRepository; +import com.dreamypatisiel.devdevdev.domain.service.pick.dto.PickCommentDto; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NotFoundException; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.web.dto.SliceCustom; import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickOptionRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickOptionRequest; import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRepliedCommentRequest; -import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRequest; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentRecommendResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentResponse; import com.dreamypatisiel.devdevdev.web.dto.response.pick.PickCommentsResponse; @@ -66,7 +70,6 @@ import java.time.LocalDateTime; import java.util.EnumSet; import java.util.List; -import java.util.Map; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -78,8 +81,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -171,11 +172,10 @@ void registerPickCommentWithPickMainVote() { em.flush(); em.clear(); - RegisterPickCommentRequest request = new RegisterPickCommentRequest("안녕하세웅", true); + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); // when - PickCommentResponse pickCommentResponse = memberPickCommentService.registerPickComment(pick.getId(), - request, + PickCommentResponse pickCommentResponse = memberPickCommentService.registerPickComment(pick.getId(), pickCommentDto, authentication); // then @@ -239,11 +239,11 @@ void registerPickCommentWithOutPickMainVote() { em.flush(); em.clear(); - RegisterPickCommentRequest registerPickCommentDto = new RegisterPickCommentRequest("안녕하세웅", false); + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", false, "anonymousMemberId"); // when - PickCommentResponse pickCommentResponse = memberPickCommentService.registerPickComment(pick.getId(), - registerPickCommentDto, authentication); + PickCommentResponse pickCommentResponse = memberPickCommentService.registerPickComment(pick.getId(), pickCommentDto, + authentication); // then assertThat(pickCommentResponse.getPickCommentId()).isNotNull(); @@ -276,9 +276,10 @@ void registerPickCommentMemberException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when - RegisterPickCommentRequest request = new RegisterPickCommentRequest("안녕하세웅", true); + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + // then - assertThatThrownBy(() -> memberPickCommentService.registerPickComment(0L, request, authentication)) + assertThatThrownBy(() -> memberPickCommentService.registerPickComment(0L, pickCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -302,10 +303,11 @@ void registerPickCommentPickMainNotFoundException() { em.clear(); // when - RegisterPickCommentRequest registerPickCommentDto = new RegisterPickCommentRequest("안녕하세웅", true); + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); + // then assertThatThrownBy( - () -> memberPickCommentService.registerPickComment(1L, registerPickCommentDto, authentication)) + () -> memberPickCommentService.registerPickComment(1L, pickCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_MESSAGE); } @@ -347,11 +349,11 @@ void registerPickCommentNotApproval(ContentStatus contentStatus) { em.flush(); em.clear(); - RegisterPickCommentRequest request = new RegisterPickCommentRequest("안녕하세웅", true); + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); // when // then assertThatThrownBy( - () -> memberPickCommentService.registerPickComment(pick.getId(), request, authentication)) + () -> memberPickCommentService.registerPickComment(pick.getId(), pickCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, REGISTER); } @@ -392,11 +394,10 @@ void registerPickCommentNotFoundPickMainVote() { em.flush(); em.clear(); - RegisterPickCommentRequest request = new RegisterPickCommentRequest("안녕하세웅", true); - + PickCommentDto pickCommentDto = new PickCommentDto("안녕하세웅", true, "anonymousMemberId"); // when // then assertThatThrownBy( - () -> memberPickCommentService.registerPickComment(pick.getId(), request, authentication)) + () -> memberPickCommentService.registerPickComment(pick.getId(), pickCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_VOTE_MESSAGE); } @@ -441,10 +442,11 @@ void registerPickRepliedComment(Boolean isPublic) { em.clear(); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when PickCommentResponse response = memberPickCommentService.registerPickRepliedComment( - replidPickComment.getId(), pickComment.getId(), pick.getId(), request, authentication); + replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication); em.flush(); em.clear(); @@ -482,10 +484,11 @@ void registerPickRepliedCommentMemberException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( - () -> memberPickCommentService.registerPickRepliedComment(0L, 0L, 0L, request, authentication)) + () -> memberPickCommentService.registerPickRepliedComment(0L, 0L, 0L, repliedCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -510,10 +513,11 @@ void registerPickRepliedCommentNotFoundExceptionParent() { pickRepository.save(pick); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( - () -> memberPickCommentService.registerPickRepliedComment(0L, 0L, 0L, request, authentication)) + () -> memberPickCommentService.registerPickRepliedComment(0L, 0L, 0L, repliedCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); } @@ -543,11 +547,12 @@ void registerPickRepliedCommentPickIsNotApproval(ContentStatus contentStatus) { pickCommentRepository.save(pickComment); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( () -> memberPickCommentService.registerPickRepliedComment( - pickComment.getId(), pickComment.getId(), pick.getId(), request, authentication)) + pickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_REPLY_MESSAGE, REGISTER); } @@ -575,15 +580,16 @@ void registerPickRepliedCommentDeleted() { // 삭제상태의 픽픽픽 댓글 생성 PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); - pickComment.changeDeletedAt(LocalDateTime.now(), member); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(pickComment); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( () -> memberPickCommentService.registerPickRepliedComment( - pickComment.getId(), pickComment.getId(), pick.getId(), request, authentication)) + pickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); } @@ -616,15 +622,16 @@ void registerPickRepliedCommentRepliedDeleted() { // 삭제상태의 픽픽픽 댓글의 답글 생성 PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, pickComment, pickComment); - replidPickComment.changeDeletedAt(LocalDateTime.now(), member); + replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(replidPickComment); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( () -> memberPickCommentService.registerPickRepliedComment( - replidPickComment.getId(), pickComment.getId(), pick.getId(), request, authentication)) + replidPickComment.getId(), pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_PICK_COMMENT_MESSAGE, REGISTER); } @@ -653,16 +660,19 @@ void registerPickRepliedCommentRepliedNotFoundException() { PickComment pickComment = createPickComment(new CommentContents("댓글1"), false, member, pick); pickCommentRepository.save(pickComment); - // 삭제상태의 픽픽픽 댓글의 답글 + // 삭제상태의 픽픽픽 댓글의 답글(삭제 상태) PickComment replidPickComment = createReplidPickComment(new CommentContents("댓글1의 답글"), member, pick, pickComment, pickComment); + pickCommentRepository.save(replidPickComment); + replidPickComment.changeDeletedAtByMember(LocalDateTime.now(), member); RegisterPickRepliedCommentRequest request = new RegisterPickRepliedCommentRequest("댓글1의 답글1의 답글"); + PickCommentDto repliedCommentDto = PickCommentDto.createRepliedCommentDto(request, "anonymousMemberId"); // when // then assertThatThrownBy( () -> memberPickCommentService.registerPickRepliedComment( - 0L, pickComment.getId(), pick.getId(), request, authentication)) + 0L, pickComment.getId(), pick.getId(), repliedCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); } @@ -701,10 +711,11 @@ void modifyPickComment(boolean isPublic) { em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when PickCommentResponse response = memberPickCommentService.modifyPickComment(pickComment.getId(), - pick.getId(), request, authentication); + pick.getId(), modifyCommentDto, authentication); // then PickComment findPickComment = pickCommentRepository.findById(pickComment.getId()).get(); @@ -733,9 +744,10 @@ void modifyPickCommentMemberException() { em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when // then - assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(0L, 0L, request, + assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(0L, 0L, modifyCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); @@ -770,9 +782,10 @@ void modifyPickCommentNotFoundPickComment() { em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when // then - assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(0L, pick.getId(), request, + assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(0L, pick.getId(), modifyCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); @@ -811,9 +824,10 @@ void modifyPickCommentNotFoundPickCommentOtherMember() { em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when // then - assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), request, + assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); @@ -846,16 +860,17 @@ void modifyPickCommentNotFoundPickCommentIsDeletedAt() { // 삭제 상태의 픽픽픽 댓글 생성 PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), false, member, pick); - pickComment.changeDeletedAt(LocalDateTime.now(), member); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(pickComment); em.flush(); em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when // then - assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), request, + assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); @@ -895,9 +910,10 @@ void modifyPickCommentNotApproval(ContentStatus contentStatus) { em.clear(); ModifyPickCommentRequest request = new ModifyPickCommentRequest("주무세웅"); + PickCommentDto modifyCommentDto = PickCommentDto.createModifyCommentDto(request, null); // when // then - assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), request, + assertThatThrownBy(() -> memberPickCommentService.modifyPickComment(pickComment.getId(), pick.getId(), modifyCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, MODIFY); @@ -938,7 +954,7 @@ void deletePickComment(boolean isPublic) { // when PickCommentResponse response = memberPickCommentService.deletePickComment(pickComment.getId(), - pick.getId(), authentication); + pick.getId(), null, authentication); // then PickComment findPickComment = pickCommentRepository.findById(pickComment.getId()).get(); @@ -985,7 +1001,7 @@ void deletePickCommentAdmin(ContentStatus contentStatus) { // when PickCommentResponse response = memberPickCommentService.deletePickComment(pickComment.getId(), - pick.getId(), authentication); + pick.getId(), null, authentication); // then PickComment findPickComment = pickCommentRepository.findById(pickComment.getId()).get(); @@ -1014,7 +1030,7 @@ void deletePickComment() { em.clear(); // when // then - assertThatThrownBy(() -> memberPickCommentService.deletePickComment(0L, 0L, authentication)) + assertThatThrownBy(() -> memberPickCommentService.deletePickComment(0L, 0L, null, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -1049,7 +1065,7 @@ void deletePickCommentNotFoundPickComment() { // when // then assertThatThrownBy( - () -> memberPickCommentService.deletePickComment(0L, pick.getId(), authentication)) + () -> memberPickCommentService.deletePickComment(0L, pick.getId(), null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); } @@ -1088,7 +1104,7 @@ void deletePickCommentNotFoundPickCommentByMember() { em.clear(); // when // then - assertThatThrownBy(() -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), + assertThatThrownBy(() -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); @@ -1128,7 +1144,7 @@ void deletePickCommentNotFoundPick(boolean isPublic) { em.clear(); // when // then - assertThatThrownBy(() -> memberPickCommentService.deletePickComment(pickComment.getId(), 0L, authentication)) + assertThatThrownBy(() -> memberPickCommentService.deletePickComment(pickComment.getId(), 0L, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); } @@ -1168,7 +1184,7 @@ void deletePickCommentNotFoundApprovalPick(ContentStatus contentStatus) { // when // then assertThatThrownBy( - () -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), authentication)) + () -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), null, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_NOT_APPROVAL_STATUS_PICK_COMMENT_MESSAGE, DELETE); } @@ -1201,7 +1217,7 @@ void deletePickCommentNotFoundPickCommentByDeletedAtIsNull(boolean isPublic) { // 삭제 상태의 픽픽픽 댓글 생성 PickComment pickComment = createPickComment(new CommentContents("안녕하세웅"), isPublic, member, pick); - pickComment.changeDeletedAt(LocalDateTime.now(), member); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(pickComment); em.flush(); @@ -1209,7 +1225,7 @@ void deletePickCommentNotFoundPickCommentByDeletedAtIsNull(boolean isPublic) { // when // then assertThatThrownBy( - () -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), authentication)) + () -> memberPickCommentService.deletePickComment(pickComment.getId(), pick.getId(), null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_PICK_COMMENT_MESSAGE); } @@ -1287,7 +1303,7 @@ void findPickCommentsByPickCommentSort(PickCommentSort pickCommentSort) { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -1302,7 +1318,7 @@ void findPickCommentsByPickCommentSort(PickCommentSort pickCommentSort) { // when Pageable pageable = PageRequest.of(0, 5); SliceCustom response = memberPickCommentService.findPickComments(pageable, - pick.getId(), Long.MAX_VALUE, pickCommentSort, null, authentication); + pick.getId(), Long.MAX_VALUE, pickCommentSort, null, null, authentication); // then // 최상위 댓글 검증 @@ -1595,8 +1611,7 @@ void findPickCommentsByPickCommentSortAndFirstPickOption(PickCommentSort pickCom // when Pageable pageable = PageRequest.of(0, 5); SliceCustom response = memberPickCommentService.findPickComments(pageable, - pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.firstPickOption), - authentication); + pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.firstPickOption), null, authentication); // then // 최상위 댓글 검증 @@ -1827,7 +1842,7 @@ void findPickCommentsByPickCommentSortAndSecondPickOption(PickCommentSort pickCo Pageable pageable = PageRequest.of(0, 5); SliceCustom response = memberPickCommentService.findPickComments(pageable, pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.secondPickOption), - authentication); + null, authentication); // then // 최상위 댓글 검증 @@ -1960,7 +1975,7 @@ void findPickCommentsByPickCommentSortAndAllPickOption(PickCommentSort pickComme SliceCustom response = memberPickCommentService.findPickComments(pageable, pick.getId(), Long.MAX_VALUE, pickCommentSort, EnumSet.of(PickOptionType.firstPickOption, PickOptionType.secondPickOption), - authentication); + null, authentication); // then // 최상위 댓글 검증 @@ -2349,7 +2364,7 @@ void recommendPickCommentIsDeleted() { // 픽픽픽 댓글 생성 PickComment pickComment = createPickComment(new CommentContents("픽픽픽 댓글"), true, member, pick); - pickComment.changeDeletedAt(LocalDateTime.now(), member); + pickComment.changeDeletedAtByMember(LocalDateTime.now(), member); pickCommentRepository.save(pickComment); // when // then @@ -2431,7 +2446,7 @@ void findPickBestComments() { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("답글1 답글1"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("댓글2 답글1"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -2445,7 +2460,7 @@ void findPickBestComments() { // when List response = memberPickCommentService.findPickBestComments(3, pick.getId(), - authentication); + null, authentication); // then // 최상위 댓글 검증 @@ -2587,328 +2602,4 @@ void findPickBestComments() { pickReply3.getParent().getCreatedBy().getNicknameAsString()) ); } - - private Pick createPick(Title title, ContentStatus contentStatus, Count viewTotalCount, Count voteTotalCount, - Count commentTotalCount, Count popularScore, Member member) { - return Pick.builder() - .title(title) - .contentStatus(contentStatus) - .viewTotalCount(viewTotalCount) - .voteTotalCount(voteTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(popularScore) - .member(member) - .build(); - } - - private PickComment createPickComment(CommentContents contents, Boolean isPublic, Count recommendTotalCount, - Member member, Pick pick) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .isPublic(isPublic) - .createdBy(member) - .recommendTotalCount(recommendTotalCount) - .pick(pick) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickCommentRecommend createPickCommentRecommend(PickComment pickComment, Member member, - Boolean recommendedStatus) { - PickCommentRecommend pickCommentRecommend = PickCommentRecommend.builder() - .member(member) - .recommendedStatus(recommendedStatus) - .build(); - - pickCommentRecommend.changePickComment(pickComment); - - return pickCommentRecommend; - } - - private Pick createPick(Title title, ContentStatus contentStatus, Count commentTotalCount, Member member) { - return Pick.builder() - .title(title) - .contentStatus(contentStatus) - .commentTotalCount(commentTotalCount) - .member(member) - .build(); - } - - private PickComment createPickComment(CommentContents contents, Boolean isPublic, Count replyTotalCount, - Count recommendTotalCount, Member member, Pick pick, PickVote pickVote) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .isPublic(isPublic) - .createdBy(member) - .replyTotalCount(replyTotalCount) - .recommendTotalCount(recommendTotalCount) - .pick(pick) - .pickVote(pickVote) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickComment createReplidPickComment(CommentContents contents, Member member, Pick pick, - PickComment originParent, PickComment parent) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .createdBy(member) - .pick(pick) - .originParent(originParent) - .isPublic(false) - .parent(parent) - .recommendTotalCount(new Count(0)) - .replyTotalCount(new Count(0)) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private PickComment createPickComment(CommentContents contents, Boolean isPublic, Member member, Pick pick) { - PickComment pickComment = PickComment.builder() - .contents(contents) - .isPublic(isPublic) - .createdBy(member) - .replyTotalCount(new Count(0)) - .pick(pick) - .build(); - - pickComment.changePick(pick); - - return pickComment; - } - - private Pick createPick(Title title, ContentStatus contentStatus, Member member) { - return Pick.builder() - .title(title) - .contentStatus(contentStatus) - .member(member) - .build(); - } - - private PickOption createPickOption(Title title, Count voteTotalCount, Pick pick, PickOptionType pickOptionType) { - PickOption pickOption = PickOption.builder() - .title(title) - .voteTotalCount(voteTotalCount) - .pickOptionType(pickOptionType) - .build(); - - pickOption.changePick(pick); - - return pickOption; - } - - private Pick createPick(Title title, Count viewTotalCount, Count commentTotalCount, Count voteTotalCount, - Count poplarScore, Member member, ContentStatus contentStatus) { - return Pick.builder() - .title(title) - .viewTotalCount(viewTotalCount) - .voteTotalCount(voteTotalCount) - .commentTotalCount(commentTotalCount) - .popularScore(poplarScore) - .member(member) - .contentStatus(contentStatus) - .build(); - } - - private ModifyPickRequest createModifyPickRequest(String pickTitle, - Map modifyPickOptionRequests) { - return ModifyPickRequest.builder() - .pickTitle(pickTitle) - .pickOptions(modifyPickOptionRequests) - .build(); - } - - private PickOptionImage createPickOptionImage(String name, String imageUrl, String imageKey) { - return PickOptionImage.builder() - .name(name) - .imageUrl(imageUrl) - .imageKey(imageKey) - .build(); - } - - private PickOptionImage createPickOptionImage(String name) { - return PickOptionImage.builder() - .name(name) - .imageUrl("imageUrl") - .imageKey("imageKey") - .build(); - } - - private PickOptionImage createPickOptionImage(String name, String imageUrl, PickOption pickOption) { - PickOptionImage pickOptionImage = PickOptionImage.builder() - .name(name) - .imageUrl(imageUrl) - .imageKey("imageKey") - .build(); - - pickOptionImage.changePickOption(pickOption); - - return pickOptionImage; - } - - private PickOptionImage createPickOptionImage(String name, PickOption pickOption) { - PickOptionImage pickOptionImage = PickOptionImage.builder() - .name(name) - .imageUrl("imageUrl") - .imageKey("imageKey") - .build(); - - pickOptionImage.changePickOption(pickOption); - - return pickOptionImage; - } - - private RegisterPickRequest createPickRegisterRequest(String pickTitle, - Map pickOptions) { - return RegisterPickRequest.builder() - .pickTitle(pickTitle) - .pickOptions(pickOptions) - .build(); - } - - private RegisterPickOptionRequest createPickOptionRequest(String pickOptionTitle, String pickOptionContent, - List pickOptionImageIds) { - return RegisterPickOptionRequest.builder() - .pickOptionTitle(pickOptionTitle) - .pickOptionContent(pickOptionContent) - .pickOptionImageIds(pickOptionImageIds) - .build(); - } - - private MockMultipartFile createMockMultipartFile(String name, String originalFilename) { - return new MockMultipartFile( - name, - originalFilename, - MediaType.IMAGE_PNG_VALUE, - name.getBytes() - ); - } - - 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 Pick createPick(Title title, Member member) { - return Pick.builder() - .title(title) - .member(member) - .build(); - } - - private Pick createPick(Title title, Count pickVoteTotalCount, Count pickViewTotalCount, - Count pickcommentTotalCount, Count pickPopularScore, String thumbnailUrl, - String author, ContentStatus contentStatus - ) { - - return Pick.builder() - .title(title) - .voteTotalCount(pickVoteTotalCount) - .viewTotalCount(pickViewTotalCount) - .commentTotalCount(pickcommentTotalCount) - .popularScore(pickPopularScore) - .thumbnailUrl(thumbnailUrl) - .author(author) - .contentStatus(contentStatus) - .build(); - } - - private Pick createPick(Title title, Count pickVoteTotalCount, Count pickViewTotalCount, - Count pickcommentTotalCount, String thumbnailUrl, String author, - ContentStatus contentStatus, - List pickVotes - ) { - - Pick pick = Pick.builder() - .title(title) - .voteTotalCount(pickVoteTotalCount) - .viewTotalCount(pickViewTotalCount) - .commentTotalCount(pickcommentTotalCount) - .thumbnailUrl(thumbnailUrl) - .author(author) - .contentStatus(contentStatus) - .build(); - - pick.changePickVote(pickVotes); - - return pick; - } - - private PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, - Count voteTotalCount, PickOptionType pickOptionType) { - PickOption pickOption = PickOption.builder() - .title(title) - .contents(pickOptionContents) - .voteTotalCount(voteTotalCount) - .pickOptionType(pickOptionType) - .build(); - - pickOption.changePick(pick); - - return pickOption; - } - - private PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, - PickOptionType pickOptionType) { - PickOption pickOption = PickOption.builder() - .title(title) - .pickOptionType(pickOptionType) - .contents(pickOptionContents) - .pick(pick) - .build(); - - pickOption.changePick(pick); - - return pickOption; - } - - private PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, - Count pickOptionVoteCount) { - PickOption pickOption = PickOption.builder() - .title(title) - .contents(pickOptionContents) - .voteTotalCount(pickOptionVoteCount) - .build(); - - pickOption.changePick(pick); - - return pickOption; - } - - private PickOption createPickOption(Title title, PickOptionContents pickOptionContents, - PickOptionType pickOptionType) { - return PickOption.builder() - .title(title) - .contents(pickOptionContents) - .pickOptionType(pickOptionType) - .build(); - } - - private PickVote createPickVote(Member member, PickOption pickOption, Pick pick) { - PickVote pickVote = PickVote.builder() - .member(member) - .build(); - - pickVote.changePickOption(pickOption); - pickVote.changePick(pick); - - return pickVote; - } } \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java new file mode 100644 index 00000000..39e8a8bb --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/pick/PickTestUtils.java @@ -0,0 +1,428 @@ +package com.dreamypatisiel.devdevdev.domain.service.pick; + +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +import com.dreamypatisiel.devdevdev.domain.entity.Member; +import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.PickComment; +import com.dreamypatisiel.devdevdev.domain.entity.PickCommentRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.PickOption; +import com.dreamypatisiel.devdevdev.domain.entity.PickOptionImage; +import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.PickOptionContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; +import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; +import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; +import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; +import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickOptionRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.ModifyPickRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickOptionRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.pick.RegisterPickRequest; +import java.util.List; +import java.util.Map; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +public abstract class PickTestUtils { + + public static Pick createPick(Title title, Count pickVoteCount, Count commentTotalCount, Member member, + ContentStatus contentStatus, List embeddings) { + return Pick.builder() + .title(title) + .voteTotalCount(pickVoteCount) + .commentTotalCount(commentTotalCount) + .member(member) + .contentStatus(contentStatus) + .embeddings(embeddings) + .build(); + } + + public static Pick createPick(Title title, ContentStatus contentStatus, Count viewTotalCount, Count voteTotalCount, + Count commentTotalCount, Count popularScore, Member member) { + return Pick.builder() + .title(title) + .contentStatus(contentStatus) + .viewTotalCount(viewTotalCount) + .voteTotalCount(voteTotalCount) + .commentTotalCount(commentTotalCount) + .popularScore(popularScore) + .member(member) + .build(); + } + + public static PickComment createPickComment(CommentContents contents, Boolean isPublic, Count recommendTotalCount, + Member member, Pick pick) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdBy(member) + .recommendTotalCount(recommendTotalCount) + .pick(pick) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickCommentRecommend createPickCommentRecommend(PickComment pickComment, Member member, + Boolean recommendedStatus) { + PickCommentRecommend pickCommentRecommend = PickCommentRecommend.builder() + .member(member) + .recommendedStatus(recommendedStatus) + .build(); + + pickCommentRecommend.changePickComment(pickComment); + + return pickCommentRecommend; + } + + public static Pick createPick(Title title, ContentStatus contentStatus, Count commentTotalCount, Member member) { + return Pick.builder() + .title(title) + .contentStatus(contentStatus) + .commentTotalCount(commentTotalCount) + .member(member) + .build(); + } + + public static PickComment createPickComment(CommentContents contents, Boolean isPublic, Count replyTotalCount, + Count recommendTotalCount, Member member, Pick pick, PickVote pickVote) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdBy(member) + .replyTotalCount(replyTotalCount) + .recommendTotalCount(recommendTotalCount) + .pick(pick) + .pickVote(pickVote) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createPickComment(CommentContents contents, Boolean isPublic, Count replyTotalCount, + Count recommendTotalCount, AnonymousMember anonymousMember, Pick pick, + PickVote pickVote) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdAnonymousBy(anonymousMember) + .replyTotalCount(replyTotalCount) + .recommendTotalCount(recommendTotalCount) + .pick(pick) + .pickVote(pickVote) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createReplidPickComment(CommentContents contents, Member member, Pick pick, + PickComment originParent, PickComment parent) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .createdBy(member) + .pick(pick) + .originParent(originParent) + .isPublic(false) + .parent(parent) + .recommendTotalCount(new Count(0)) + .replyTotalCount(new Count(0)) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createReplidPickComment(CommentContents contents, AnonymousMember anonymousMember, Pick pick, + PickComment originParent, PickComment parent) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .createdAnonymousBy(anonymousMember) + .pick(pick) + .originParent(originParent) + .isPublic(false) + .parent(parent) + .recommendTotalCount(new Count(0)) + .replyTotalCount(new Count(0)) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createPickComment(CommentContents contents, Boolean isPublic, Member member, Pick pick) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdBy(member) + .replyTotalCount(new Count(0)) + .pick(pick) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static PickComment createPickComment(CommentContents contents, Boolean isPublic, AnonymousMember anonymousMember, + Pick pick) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdAnonymousBy(anonymousMember) + .replyTotalCount(new Count(0)) + .pick(pick) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + + public static Pick createPick(Title title, ContentStatus contentStatus, Member member) { + return Pick.builder() + .title(title) + .contentStatus(contentStatus) + .member(member) + .build(); + } + + public static PickOption createPickOption(Title title, Count voteTotalCount, Pick pick, PickOptionType pickOptionType) { + PickOption pickOption = PickOption.builder() + .title(title) + .voteTotalCount(voteTotalCount) + .pickOptionType(pickOptionType) + .build(); + + pickOption.changePick(pick); + + return pickOption; + } + + public static Pick createPick(Title title, Count viewTotalCount, Count commentTotalCount, Count voteTotalCount, + Count poplarScore, Member member, ContentStatus contentStatus) { + return Pick.builder() + .title(title) + .viewTotalCount(viewTotalCount) + .voteTotalCount(voteTotalCount) + .commentTotalCount(commentTotalCount) + .popularScore(poplarScore) + .member(member) + .contentStatus(contentStatus) + .build(); + } + + public static ModifyPickRequest createModifyPickRequest(String pickTitle, + Map modifyPickOptionRequests) { + return ModifyPickRequest.builder() + .pickTitle(pickTitle) + .pickOptions(modifyPickOptionRequests) + .build(); + } + + public static PickOptionImage createPickOptionImage(String name, String imageUrl, String imageKey) { + return PickOptionImage.builder() + .name(name) + .imageUrl(imageUrl) + .imageKey(imageKey) + .build(); + } + + public static PickOptionImage createPickOptionImage(String name) { + return PickOptionImage.builder() + .name(name) + .imageUrl("imageUrl") + .imageKey("imageKey") + .build(); + } + + public static PickOptionImage createPickOptionImage(String name, String imageUrl, PickOption pickOption) { + PickOptionImage pickOptionImage = PickOptionImage.builder() + .name(name) + .imageUrl(imageUrl) + .imageKey("imageKey") + .build(); + + pickOptionImage.changePickOption(pickOption); + + return pickOptionImage; + } + + public static PickOptionImage createPickOptionImage(String name, PickOption pickOption) { + PickOptionImage pickOptionImage = PickOptionImage.builder() + .name(name) + .imageUrl("imageUrl") + .imageKey("imageKey") + .build(); + + pickOptionImage.changePickOption(pickOption); + + return pickOptionImage; + } + + public static RegisterPickRequest createPickRegisterRequest(String pickTitle, + Map pickOptions) { + return RegisterPickRequest.builder() + .pickTitle(pickTitle) + .pickOptions(pickOptions) + .build(); + } + + public static RegisterPickOptionRequest createPickOptionRequest(String pickOptionTitle, String pickOptionContent, + List pickOptionImageIds) { + return RegisterPickOptionRequest.builder() + .pickOptionTitle(pickOptionTitle) + .pickOptionContent(pickOptionContent) + .pickOptionImageIds(pickOptionImageIds) + .build(); + } + + public static MockMultipartFile createMockMultipartFile(String name, String originalFilename) { + return new MockMultipartFile( + name, + originalFilename, + MediaType.IMAGE_PNG_VALUE, + name.getBytes() + ); + } + + public static 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(); + } + + public static Pick createPick(Title title, Member member) { + return Pick.builder() + .title(title) + .member(member) + .build(); + } + + public static Pick createPick(Title title, Count pickVoteTotalCount, Count pickViewTotalCount, + Count pickcommentTotalCount, Count pickPopularScore, String thumbnailUrl, + String author, ContentStatus contentStatus + ) { + + return Pick.builder() + .title(title) + .voteTotalCount(pickVoteTotalCount) + .viewTotalCount(pickViewTotalCount) + .commentTotalCount(pickcommentTotalCount) + .popularScore(pickPopularScore) + .thumbnailUrl(thumbnailUrl) + .author(author) + .contentStatus(contentStatus) + .build(); + } + + public static Pick createPick(Title title, Count pickVoteTotalCount, Count pickViewTotalCount, + Count pickcommentTotalCount, String thumbnailUrl, String author, + ContentStatus contentStatus, + List pickVotes + ) { + + Pick pick = Pick.builder() + .title(title) + .voteTotalCount(pickVoteTotalCount) + .viewTotalCount(pickViewTotalCount) + .commentTotalCount(pickcommentTotalCount) + .thumbnailUrl(thumbnailUrl) + .author(author) + .contentStatus(contentStatus) + .build(); + + pick.changePickVote(pickVotes); + + return pick; + } + + public static PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, + Count voteTotalCount, PickOptionType pickOptionType) { + PickOption pickOption = PickOption.builder() + .title(title) + .contents(pickOptionContents) + .voteTotalCount(voteTotalCount) + .pickOptionType(pickOptionType) + .build(); + + pickOption.changePick(pick); + + return pickOption; + } + + public static PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, + PickOptionType pickOptionType) { + PickOption pickOption = PickOption.builder() + .title(title) + .pickOptionType(pickOptionType) + .contents(pickOptionContents) + .pick(pick) + .build(); + + pickOption.changePick(pick); + + return pickOption; + } + + public static PickOption createPickOption(Pick pick, Title title, PickOptionContents pickOptionContents, + Count pickOptionVoteCount) { + PickOption pickOption = PickOption.builder() + .title(title) + .contents(pickOptionContents) + .voteTotalCount(pickOptionVoteCount) + .build(); + + pickOption.changePick(pick); + + return pickOption; + } + + public static PickOption createPickOption(Title title, PickOptionContents pickOptionContents, + PickOptionType pickOptionType) { + return PickOption.builder() + .title(title) + .contents(pickOptionContents) + .pickOptionType(pickOptionType) + .build(); + } + + public static PickVote createPickVote(Member member, PickOption pickOption, Pick pick) { + PickVote pickVote = PickVote.builder() + .member(member) + .build(); + + pickVote.changePickOption(pickOption); + pickVote.changePick(pick); + + return pickVote; + } + + public static PickVote createPickVote(AnonymousMember anonymousMember, PickOption pickOption, Pick pick) { + PickVote pickVote = PickVote.builder() + .anonymousMember(anonymousMember) + .build(); + + pickVote.changePickOption(pickOption); + pickVote.changePick(pick); + + return pickVote; + } +} 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 eade4b29..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 @@ -1,45 +1,53 @@ package com.dreamypatisiel.devdevdev.domain.service.techArticle; +import static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createCompany; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createMainTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createRepliedTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createSocialDto; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createTechCommentRecommend; +import static com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils.INVALID_METHODS_CALL_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.Company; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.domain.entity.TechCommentRecommend; import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; -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 static com.dreamypatisiel.devdevdev.domain.exception.GuestExceptionMessage.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.GuestTechCommentService; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; 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 static com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE; import com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; 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 static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.PageRequest; @@ -101,17 +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, registerTechCommentRequest, authentication)) + techArticleId, registerCommentDto, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -130,22 +137,21 @@ 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(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when // then assertThatThrownBy(() -> guestTechCommentService.registerRepliedTechComment( - techArticleId, parentTechCommentId, parentTechCommentId, registerRepliedTechComment, authentication)) + techArticleId, parentTechCommentId, parentTechCommentId, registerCommentDto, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -164,13 +170,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); // when // then @@ -195,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(); @@ -246,7 +248,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), // when SliceCommentCustom response = guestTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.OLDEST, pageable, authentication); + null, TechCommentSort.OLDEST, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -489,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(); @@ -540,7 +540,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), // when SliceCommentCustom response = guestTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.LATEST, pageable, authentication); + null, TechCommentSort.LATEST, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -658,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(); @@ -709,7 +707,7 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), // when SliceCommentCustom response = guestTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.MOST_COMMENTED, pageable, authentication); + null, TechCommentSort.MOST_COMMENTED, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -952,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(); @@ -983,7 +979,7 @@ void getTechCommentsSortByMostRecommended() { // when SliceCommentCustom response = guestTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.MOST_LIKED, pageable, authentication); + null, TechCommentSort.MOST_LIKED, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -1100,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(); @@ -1132,7 +1127,7 @@ void getTechCommentsByCursor() { // when SliceCommentCustom response = guestTechCommentService.getTechComments(techArticleId, - originParentTechComment6.getId(), null, pageable, authentication); + originParentTechComment6.getId(), null, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(5L); // 삭제된 댓글은 카운트하지 않는다 @@ -1249,7 +1244,7 @@ void findTechBestCommentsNotAnonymousMember() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy(() -> guestTechCommentService.findTechBestComments(3, 1L, authentication)) + assertThatThrownBy(() -> guestTechCommentService.findTechBestComments(3, 1L, null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_METHODS_CALL_MESSAGE); } @@ -1276,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); // 댓글 생성 @@ -1306,7 +1300,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), Authentication authentication = mock(Authentication.class); when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); - List response = guestTechCommentService.findTechBestComments(3, techArticle.getId(), + List response = guestTechCommentService.findTechBestComments(3, techArticle.getId(), null, authentication); // then @@ -1400,70 +1394,19 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ); } - private TechCommentRecommend createTechCommentRecommend(Boolean recommendedStatus, TechComment techComment, - Member member) { - TechCommentRecommend techCommentRecommend = TechCommentRecommend.builder() - .recommendedStatus(recommendedStatus) - .techComment(techComment) - .member(member) - .build(); - - techCommentRecommend.changeTechComment(techComment); - - return techCommentRecommend; - } - - private static TechComment createMainTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .build(); - } - - private static TechComment createRepliedTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - TechComment originParent, TechComment parent, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .originParent(originParent) - .parent(parent) - .build(); - } - - 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 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)) + 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 6b2ed657..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 @@ -5,6 +5,11 @@ import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createCompany; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createMainTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createRepliedTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createSocialDto; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createTechCommentRecommend; import static com.dreamypatisiel.devdevdev.global.common.MemberProvider.INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -18,7 +23,6 @@ import com.dreamypatisiel.devdevdev.domain.entity.TechComment; import com.dreamypatisiel.devdevdev.domain.entity.TechCommentRecommend; import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; -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; @@ -30,6 +34,7 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; import com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment.MemberTechCommentService; import com.dreamypatisiel.devdevdev.exception.MemberException; import com.dreamypatisiel.devdevdev.exception.NotFoundException; @@ -46,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; @@ -123,17 +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, registerTechCommentRequest, authentication); + techArticleId, registerCommentDto, authentication); em.flush(); // then @@ -149,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) ); @@ -163,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); @@ -180,10 +184,11 @@ void registerTechCommentNotFoundTechArticleException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when // then assertThatThrownBy( - () -> memberTechCommentService.registerMainTechComment(id, registerTechCommentRequest, authentication)) + () -> memberTechCommentService.registerMainTechComment(techArticleId, registerCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @@ -200,10 +205,11 @@ void registerTechCommentNotFoundMemberException() { Long id = 1L; RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when // then assertThatThrownBy( - () -> memberTechCommentService.registerMainTechComment(id, registerTechCommentRequest, authentication)) + () -> memberTechCommentService.registerMainTechComment(id, registerCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -226,24 +232,23 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); LocalDateTime modifiedDateTime = LocalDateTime.of(2024, 10, 6, 0, 0, 0); when(timeProvider.getLocalDateTimeNow()).thenReturn(modifiedDateTime); // when TechCommentResponse techCommentResponse = memberTechCommentService.modifyTechComment( - techArticleId, techCommentId, modifyTechCommentRequest, authentication); + techArticleId, techCommentId, modifyCommentDto, authentication); em.flush(); // then @@ -277,10 +282,11 @@ void modifyTechCommentNotFoundMemberException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); // when // then assertThatThrownBy( - () -> memberTechCommentService.modifyTechComment(0L, 0L, modifyTechCommentRequest, + () -> memberTechCommentService.modifyTechComment(0L, 0L, modifyCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); @@ -304,17 +310,16 @@ 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(); ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); // when // then assertThatThrownBy( - () -> memberTechCommentService.modifyTechComment(techArticleId, 0L, modifyTechCommentRequest, + () -> memberTechCommentService.modifyTechComment(techArticleId, 0L, modifyCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); @@ -339,13 +344,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -354,11 +357,11 @@ void modifyTechCommentAlreadyDeletedException() { em.flush(); ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정"); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); // when // then - assertThatThrownBy( - () -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyTechCommentRequest, - authentication)) + assertThatThrownBy(() -> memberTechCommentService.modifyTechComment(techArticleId, techCommentId, modifyCommentDto, + authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -381,13 +384,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -397,7 +398,7 @@ void deleteTechComment() { em.flush(); // when - memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication); + memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication); // then TechComment findTechComment = techCommentRepository.findById(techCommentId).get(); @@ -428,13 +429,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -444,7 +443,7 @@ void deleteTechCommentAlreadyDeletedException() { // when // then assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication)) + () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -468,15 +467,13 @@ 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(); // when // then assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(techArticleId, 0L, authentication)) + () -> memberTechCommentService.deleteTechComment(techArticleId, 0L, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -504,13 +501,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -520,7 +515,7 @@ void deleteTechCommentAdmin() { em.flush(); // when - memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication); + memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication); // then TechComment findTechComment = techCommentRepository.findById(techCommentId).get(); @@ -555,19 +550,17 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), author, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), author, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); // when // then assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication)) + () -> memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -586,8 +579,7 @@ void deleteTechCommentNotFoundMemberException() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // when // then - assertThatThrownBy( - () -> memberTechCommentService.deleteTechComment(0L, 0L, authentication)) + assertThatThrownBy(() -> memberTechCommentService.deleteTechComment(0L, 0L, null, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -610,22 +602,21 @@ 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(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when TechCommentResponse techCommentResponse = memberTechCommentService.registerRepliedTechComment( - techArticleId, parentTechCommentId, parentTechCommentId, registerRepliedTechComment, authentication); + techArticleId, parentTechCommentId, parentTechCommentId, registerRepliedCommentDto, authentication); em.flush(); // then @@ -670,27 +661,26 @@ 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(); - TechComment originParentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(originParentTechComment); Long originParentTechCommentId = originParentTechComment.getId(); - TechComment parentTechComment = TechComment.createRepliedTechComment(new CommentContents("답글입니다."), member, + TechComment parentTechComment = TechComment.createRepliedTechCommentByMember(new CommentContents("답글입니다."), member, techArticle, originParentTechComment, originParentTechComment); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when TechCommentResponse techCommentResponse = memberTechCommentService.registerRepliedTechComment( - techArticleId, originParentTechCommentId, parentTechCommentId, registerRepliedTechComment, + techArticleId, originParentTechCommentId, parentTechCommentId, registerRepliedCommentDto, authentication); em.flush(); @@ -713,7 +703,7 @@ void registerRepliedTechCommentToRepliedTechComment() { 1L), // 기술블로그 댓글 수 증가 확인 () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( - 3L) + 2L) ); } @@ -735,22 +725,21 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId() + 1; RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when // then assertThatThrownBy( () -> memberTechCommentService.registerRepliedTechComment(techArticleId, techCommentId, techCommentId, - registerRepliedTechComment, authentication)) + registerRepliedCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); } @@ -773,13 +762,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -790,11 +777,12 @@ void registerRepliedTechCommentDeletedTechCommentException() { em.clear(); RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when // then assertThatThrownBy( () -> memberTechCommentService.registerRepliedTechComment(techArticleId, techCommentId, techCommentId, - registerRepliedTechComment, authentication)) + registerRepliedCommentDto, authentication)) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE); } @@ -816,11 +804,12 @@ void registerRepliedTechCommentNotFoundMemberException() { em.clear(); RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); // when // then assertThatThrownBy( () -> memberTechCommentService.registerRepliedTechComment(0L, 0L, 0L, - registerRepliedTechComment, authentication)) + registerRepliedCommentDto, authentication)) .isInstanceOf(MemberException.class) .hasMessage(INVALID_MEMBER_NOT_FOUND_MESSAGE); } @@ -843,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(); @@ -894,7 +881,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), // when SliceCommentCustom response = memberTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.OLDEST, pageable, authentication); + null, TechCommentSort.OLDEST, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -1140,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(); @@ -1191,7 +1176,7 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), // when SliceCommentCustom response = memberTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.LATEST, pageable, authentication); + null, TechCommentSort.LATEST, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -1312,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(); @@ -1363,7 +1346,7 @@ techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), // when SliceCommentCustom response = memberTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.MOST_COMMENTED, pageable, authentication); + null, TechCommentSort.MOST_COMMENTED, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -1609,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(); @@ -1640,7 +1621,7 @@ void getTechCommentsSortByMostRecommended() { // when SliceCommentCustom response = memberTechCommentService.getTechComments(techArticleId, - null, TechCommentSort.MOST_LIKED, pageable, authentication); + null, TechCommentSort.MOST_LIKED, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); @@ -1760,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(); @@ -1793,7 +1772,7 @@ void getTechCommentsByCursor() { // when SliceCommentCustom response = memberTechCommentService.getTechComments(techArticleId, - originParentTechComment6.getId(), null, pageable, authentication); + originParentTechComment6.getId(), null, pageable, null, authentication); // then assertThat(response.getTotalOriginParentComments()).isEqualTo(5L); // 삭제된 댓글은 카운트하지 않는다 @@ -1913,13 +1892,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); // when @@ -1951,13 +1928,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); TechCommentRecommend techCommentRecommend = TechCommentRecommend.create(techComment, member); @@ -1992,13 +1967,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId() + 1; @@ -2027,13 +2000,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -2084,7 +2055,7 @@ void findTechBestCommentsNotAnonymousMember() { when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); // when // then - assertThatThrownBy(() -> memberTechCommentService.findTechBestComments(3, 0L, authentication)) + assertThatThrownBy(() -> memberTechCommentService.findTechBestComments(3, 0L, null, authentication)) .isInstanceOf(IllegalStateException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -2118,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, @@ -2145,7 +2115,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), techCommentRepository.save(repliedTechComment); // when - List response = memberTechCommentService.findTechBestComments(3, techArticle.getId(), + List response = memberTechCommentService.findTechBestComments(3, techArticle.getId(), null, authentication); // then @@ -2268,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, @@ -2296,7 +2265,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), // when List response = memberTechCommentService.findTechBestComments(3, techArticle.getId(), - authentication); + null, authentication); // then assertThat(response).hasSize(1) @@ -2365,70 +2334,19 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), ); } - private TechCommentRecommend createTechCommentRecommend(Boolean recommendedStatus, TechComment techComment, - Member member) { - TechCommentRecommend techCommentRecommend = TechCommentRecommend.builder() - .recommendedStatus(recommendedStatus) - .techComment(techComment) - .member(member) - .build(); - - techCommentRecommend.changeTechComment(techComment); - - return techCommentRecommend; - } - - private static TechComment createMainTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .build(); - } - - private static TechComment createRepliedTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - TechComment originParent, TechComment parent, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .originParent(originParent) - .parent(parent) - .build(); - } - - 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 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)) + 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 new file mode 100644 index 00000000..7245680d --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -0,0 +1,208 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.TechKeyword; +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 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; +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; +import org.springframework.test.context.transaction.BeforeTransaction; +import org.springframework.transaction.annotation.Transactional; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@Testcontainers +class TechKeywordServiceTest { + + @Autowired + EntityManager em; + + @Autowired + TechKeywordService techKeywordService; + + @Autowired + TechKeywordRepository techKeywordRepository; + + @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 + public void initIndexes() throws SQLException { + if (!indexesCreated) { + // 인덱스 생성 + createFulltextIndexesWithJDBC(); + indexesCreated = true; + + // 데이터 추가 + TechKeyword keyword1 = createTechKeyword("자바"); + TechKeyword keyword2 = createTechKeyword("자바스크립트"); + TechKeyword keyword3 = createTechKeyword("스프링"); + TechKeyword keyword4 = createTechKeyword("스프링부트"); + TechKeyword keyword5 = createTechKeyword("꿈빛"); + TechKeyword keyword6 = createTechKeyword("꿈빛 나라"); + TechKeyword keyword7 = createTechKeyword("행복한 꿈빛 파티시엘"); + List techKeywords = List.of(keyword1, keyword2, keyword3, keyword4, keyword5, keyword6, keyword7); + techKeywordRepository.saveAll(techKeywords); + } + } + + /** + * 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__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(); + } + } + } + + @Test + @DisplayName("검색어와 prefix가 일치하는 키워드를 조회한다.") + void autocompleteKeyword() { + // given + String prefix = "자바"; + + // when + List keywords = techKeywordService.autocompleteKeyword(prefix); + + // then + assertThat(keywords) + .hasSize(2) + .contains("자바", "자바스크립트"); + } + + @ParameterizedTest + @ValueSource(strings = {"ㅈ", "자", "잡", "ㅈㅏ", "ㅈㅏㅂ", "ㅈㅏㅂㅏ"}) + @DisplayName("한글 검색어의 경우 자음, 모음을 분리하여 검색할 수 있다.") + void autocompleteKoreanKeywordBySeparatingConsonantsAndVowels(String prefix) { + // given // when + List keywords = techKeywordService.autocompleteKeyword(prefix); + + // then + assertThat(keywords) + .hasSize(2) + .contains("자바", "자바스크립트"); + } + + @Test + @DisplayName("한글 검색어의 경우 초성검색을 할 수 있다.") + void autocompleteKoreanKeywordByChosung() { + // given + String prefix = "ㅅㅍㄹ"; + + // when + List keywords = techKeywordService.autocompleteKeyword(prefix); + + // then + assertThat(keywords) + .hasSize(2) + .contains("스프링", "스프링부트"); + } + + @Test + @DisplayName("일치하는 키워드가 없을 경우 빈 리스트를 반환한다.") + void autocompleteKeywordNotFound() { + // given + String prefix = "엘라스틱서치"; + + // when + List keywords = techKeywordService.autocompleteKeyword(prefix); + + // then + assertThat(keywords).isEmpty(); + } + + @ParameterizedTest + @ValueSource(ints = {19, 20, 21, 22}) + @DisplayName("검색 결과는 최대 20개로 제한된다.") + void autocompleteKeywordLimitTo20Results(int n) { + // given + List techKeywords = new ArrayList<>(); + for (int i = 0; i < n; i++) { + techKeywords.add(createTechKeyword("키워드" + i)); + } + techKeywordRepository.saveAll(techKeywords); + + // when + List result = techKeywordService.autocompleteKeyword("키워드"); + + // then + assertThat(result).hasSizeLessThanOrEqualTo(20); + } + + @Test + @DisplayName("검색 결과가 관련도 순으로 정렬된다.") + void autocompleteKeywordSortedByRelevance() { + // given // when + List result = techKeywordService.autocompleteKeyword("꿈빛"); + + // then + assertThat(result).isNotEmpty(); + // 더 정확히 매치되는 "꿈빛"이 상위에 나와야 한다 + assertThat(result.get(0)).isEqualTo("꿈빛"); + } + + private TechKeyword createTechKeyword(String keyword) { + return TechKeyword.builder() + .keyword(keyword) + .jamoKey(HangulUtils.convertToJamo(keyword)) + .chosungKey(HangulUtils.extractChosung(keyword)) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechTestUtils.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechTestUtils.java new file mode 100644 index 00000000..09789824 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechTestUtils.java @@ -0,0 +1,112 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle; + +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; +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.TechComment; +import com.dreamypatisiel.devdevdev.domain.entity.TechCommentRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CompanyName; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Count; +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.global.security.oauth2.model.SocialMemberDto; + +public class TechTestUtils { + + public static TechCommentRecommend createTechCommentRecommend(Boolean recommendedStatus, TechComment techComment, + Member member) { + TechCommentRecommend techCommentRecommend = TechCommentRecommend.builder() + .recommendedStatus(recommendedStatus) + .techComment(techComment) + .member(member) + .build(); + + techCommentRecommend.changeTechComment(techComment); + + return techCommentRecommend; + } + + public static TechComment createMainTechComment(CommentContents contents, Member createdBy, TechArticle techArticle, + Count blameTotalCount, Count recommendTotalCount, Count replyTotalCount) { + return TechComment.builder() + .contents(contents) + .createdBy(createdBy) + .techArticle(techArticle) + .blameTotalCount(blameTotalCount) + .recommendTotalCount(recommendTotalCount) + .replyTotalCount(replyTotalCount) + .build(); + } + + public static TechComment createMainTechComment(CommentContents contents, AnonymousMember createdAnonymousBy, + TechArticle techArticle, Count blameTotalCount, Count recommendTotalCount, + Count replyTotalCount) { + return TechComment.builder() + .contents(contents) + .createdAnonymousBy(createdAnonymousBy) + .techArticle(techArticle) + .blameTotalCount(blameTotalCount) + .recommendTotalCount(recommendTotalCount) + .replyTotalCount(replyTotalCount) + .build(); + } + + public static TechComment createRepliedTechComment(CommentContents contents, Member createdBy, + TechArticle techArticle, + TechComment originParent, TechComment parent, + Count blameTotalCount, Count recommendTotalCount, + Count replyTotalCount) { + return TechComment.builder() + .contents(contents) + .createdBy(createdBy) + .techArticle(techArticle) + .blameTotalCount(blameTotalCount) + .recommendTotalCount(recommendTotalCount) + .replyTotalCount(replyTotalCount) + .originParent(originParent) + .parent(parent) + .build(); + } + + public static TechComment createRepliedTechComment(CommentContents contents, AnonymousMember createdAnonymousBy, + TechArticle techArticle, TechComment originParent, TechComment parent, + Count blameTotalCount, Count recommendTotalCount, + Count replyTotalCount) { + return TechComment.builder() + .contents(contents) + .createdAnonymousBy(createdAnonymousBy) + .techArticle(techArticle) + .blameTotalCount(blameTotalCount) + .recommendTotalCount(recommendTotalCount) + .replyTotalCount(replyTotalCount) + .originParent(originParent) + .parent(parent) + .build(); + } + + public static 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(); + } + + 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(); + } +} 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 new file mode 100644 index 00000000..1620b719 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -0,0 +1,2047 @@ +package com.dreamypatisiel.devdevdev.domain.service.techArticle.techComment; + +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createCompany; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createMainTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createRepliedTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createSocialDto; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createTechCommentRecommend; +import static com.dreamypatisiel.devdevdev.global.utils.AuthenticationMemberUtils.INVALID_METHODS_CALL_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +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.Member; +import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; +import com.dreamypatisiel.devdevdev.domain.entity.TechComment; +import com.dreamypatisiel.devdevdev.domain.entity.TechCommentRecommend; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; +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.AnonymousMemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; +import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.dto.TechCommentDto; +import com.dreamypatisiel.devdevdev.exception.NotFoundException; +import com.dreamypatisiel.devdevdev.global.common.TimeProvider; +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 com.dreamypatisiel.devdevdev.web.dto.SliceCommentCustom; +import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentResponse; +import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechCommentsResponse; +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; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.security.core.Authentication; +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 +@Transactional +class GuestTechCommentServiceV2Test { + + @Autowired + GuestTechCommentServiceV2 guestTechCommentServiceV2; + @Autowired + TechArticleRepository techArticleRepository; + @Autowired + TechCommentRepository techCommentRepository; + @Autowired + CompanyRepository companyRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + TechCommentRecommendRepository techCommentRecommendRepository; + @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @MockBean + TimeProvider timeProvider; + @Autowired + EntityManager em; + + String userId = "dreamy5patisiel"; + String name = "꿈빛파티시엘"; + String nickname = "행복한 꿈빛파티시엘"; + String email = "dreamy5patisiel@kakao.com"; + String password = "password"; + String socialType = SocialType.KAKAO.name(); + String role = Role.ROLE_USER.name(); + String author = "운영자"; + + @Test + @DisplayName("익명 회원은 커서 방식으로 기술블로그 댓글/답글을 조회할 수 있다. (등록순)") + void getTechCommentsSortByOLDEST() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), anonymousMember, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment4 = createMainTechComment(new CommentContents("최상위 댓글4"), member, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), member, techArticle, + new Count(0L), new Count(0L), new Count(0L)); + + TechComment parentTechComment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1"), anonymousMember, + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2"), member, + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment3 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1"), member, + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment4 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글2"), member, + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + + TechComment techcomment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1의 답글"), member, + techArticle, originParentTechComment1, parentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment techcomment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2의 답글"), member, + techArticle, originParentTechComment1, parentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + + techCommentRepository.saveAll(List.of( + originParentTechComment1, originParentTechComment2, originParentTechComment3, + originParentTechComment4, originParentTechComment5, originParentTechComment6, + parentTechComment1, parentTechComment2, parentTechComment3, parentTechComment4, + techcomment1, techcomment2 + )); + + Pageable pageable = PageRequest.of(0, 5); + + em.flush(); + em.clear(); + + // when + SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, + null, TechCommentSort.OLDEST, pageable, anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment1.getId(), + null, + anonymousMember.getNickname(), + null, + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId() + ), + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ) + ); + + TechCommentsResponse techCommentsResponse1 = response.getContent().get(0); + List replies1 = techCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(4) + .extracting( + "techCommentId", + "memberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" + ) + .containsExactly( + Tuple.tuple(parentTechComment1.getId(), + null, + originParentTechComment1.getId(), + null, + anonymousMember.getNickname(), + originParentTechComment1.getId(), + anonymousMember.getNickname(), + null, + parentTechComment1.getContents().getCommentContents(), + parentTechComment1.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId(), + originParentTechComment1.getCreatedAnonymousBy().getId() + ), + Tuple.tuple(parentTechComment2.getId(), + member.getId(), + originParentTechComment1.getId(), + null, + anonymousMember.getNickname(), + originParentTechComment1.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment2.getContents().getCommentContents(), + parentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + originParentTechComment1.getCreatedAnonymousBy().getId() + ), + Tuple.tuple(techcomment1.getId(), + member.getId(), + parentTechComment1.getId(), + null, + anonymousMember.getNickname(), + originParentTechComment1.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + techcomment1.getContents().getCommentContents(), + techcomment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + originParentTechComment1.getCreatedAnonymousBy().getId() + ), + Tuple.tuple(techcomment2.getId(), + member.getId(), + parentTechComment2.getId(), + parentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment1.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + techcomment2.getContents().getCommentContents(), + techcomment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ) + ); + + TechCommentsResponse techCommentsResponse2 = response.getContent().get(1); + List replies2 = techCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(2) + .extracting( + "techCommentId", + "memberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" + ) + .containsExactly( + Tuple.tuple(parentTechComment3.getId(), + member.getId(), + originParentTechComment2.getId(), + originParentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment3.getContents().getCommentContents(), + parentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ), + Tuple.tuple(parentTechComment4.getId(), + member.getId(), + originParentTechComment2.getId(), + originParentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment4.getContents().getCommentContents(), + parentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ) + ); + + TechCommentsResponse techCommentsResponse3 = response.getContent().get(2); + List replies3 = techCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + TechCommentsResponse techCommentsResponse4 = response.getContent().get(3); + List replies4 = techCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + TechCommentsResponse techCommentsResponse5 = response.getContent().get(4); + List replies5 = techCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + } + + @Test + @DisplayName("익명 회원은 커서 방식으로 기술블로그 댓글/답글을 조회할 수 있다. (기본 정렬은 최신순)") + void getTechCommentsSortByLATEST() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment4 = createMainTechComment(new CommentContents("최상위 댓글4"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), anonymousMember, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + + TechComment parentTechComment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1"), member, + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2"), member, techArticle, + originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment3 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1"), member, techArticle, + originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment4 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글2"), member, techArticle, + originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + + TechComment techcomment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1의 답글"), member, + techArticle, originParentTechComment1, parentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment techcomment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2의 답글"), member, + techArticle, originParentTechComment1, parentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + + techCommentRepository.saveAll(List.of( + originParentTechComment1, originParentTechComment2, originParentTechComment3, + originParentTechComment4, originParentTechComment5, originParentTechComment6, + parentTechComment1, parentTechComment2, parentTechComment3, parentTechComment4, + techcomment1, techcomment2 + )); + + Pageable pageable = PageRequest.of(0, 5); + + em.flush(); + em.clear(); + + // when + SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, + null, TechCommentSort.LATEST, pageable, anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment6.getId(), + null, + anonymousMember.getNickname(), + null, + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId() + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ) + ); + + TechCommentsResponse techCommentsResponse6 = response.getContent().get(0); + List replies6 = techCommentsResponse6.getReplies(); + assertThat(replies6).hasSize(0); + + TechCommentsResponse techCommentsResponse5 = response.getContent().get(1); + List replies5 = techCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + + TechCommentsResponse techCommentsResponse4 = response.getContent().get(2); + List replies4 = techCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + TechCommentsResponse techCommentsResponse3 = response.getContent().get(3); + List replies3 = techCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + TechCommentsResponse techCommentsResponse2 = response.getContent().get(4); + List replies2 = techCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(2) + .extracting("techCommentId") + .containsExactly(parentTechComment3.getId(), parentTechComment4.getId()); + } + + @Test + @DisplayName("익명 회원은 커서 방식으로 기술블로그 댓글/답글을 조회할 수 있다. (댓글 많은 순)") + void getTechCommentsSortByMostCommented() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, + techArticle, new Count(0L), new Count(0L), new Count(4L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment4 = createMainTechComment(new CommentContents("최상위 댓글4"), member, + techArticle, new Count(0L), new Count(0L), new Count(2L)); + TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + + TechComment parentTechComment1 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1"), anonymousMember, + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment2 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글2"), member, + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment3 = createRepliedTechComment(new CommentContents("최상위 댓글4의 답글1"), member, + techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment4 = createRepliedTechComment(new CommentContents("최상위 댓글4의 답글2"), member, + techArticle, originParentTechComment4, originParentTechComment4, new Count(0L), new Count(0L), new Count(0L)); + + TechComment techcomment1 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1의 답글"), member, + techArticle, originParentTechComment2, parentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment techcomment2 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글2의 답글"), member, + techArticle, originParentTechComment2, parentTechComment2, new Count(0L), new Count(0L), new Count(0L)); + + techCommentRepository.saveAll(List.of( + originParentTechComment1, originParentTechComment2, originParentTechComment3, + originParentTechComment4, originParentTechComment5, originParentTechComment6, + parentTechComment1, parentTechComment2, parentTechComment3, parentTechComment4, + techcomment1, techcomment2 + )); + + Pageable pageable = PageRequest.of(0, 5); + + em.flush(); + em.clear(); + + // when + SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, + null, TechCommentSort.MOST_COMMENTED, pageable, anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment6.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ) + ); + + TechCommentsResponse techCommentsResponse1 = response.getContent().get(0); + List replies1 = techCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(4) + .extracting( + "techCommentId", + "memberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" + ) + .containsExactly( + Tuple.tuple(parentTechComment1.getId(), + null, + originParentTechComment2.getId(), + originParentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + anonymousMember.getNickname(), + null, + parentTechComment1.getContents().getCommentContents(), + parentTechComment1.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId(), + null + ), + Tuple.tuple(parentTechComment2.getId(), + member.getId(), + originParentTechComment2.getId(), + originParentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment2.getContents().getCommentContents(), + parentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ), + Tuple.tuple(techcomment1.getId(), + member.getId(), + parentTechComment1.getId(), + null, + anonymousMember.getNickname(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + techcomment1.getContents().getCommentContents(), + techcomment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + parentTechComment1.getCreatedAnonymousBy().getId() + ), + Tuple.tuple(techcomment2.getId(), + member.getId(), + parentTechComment2.getId(), + parentTechComment2.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment2.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + techcomment2.getContents().getCommentContents(), + techcomment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ) + ); + + TechCommentsResponse techCommentsResponse2 = response.getContent().get(1); + List replies2 = techCommentsResponse2.getReplies(); + assertThat(replies2).hasSize(2) + .extracting( + "techCommentId", + "memberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" + ) + .containsExactly( + Tuple.tuple(parentTechComment3.getId(), + member.getId(), + originParentTechComment4.getId(), + originParentTechComment4.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment4.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment3.getContents().getCommentContents(), + parentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ), + Tuple.tuple(parentTechComment4.getId(), + member.getId(), + originParentTechComment4.getId(), + originParentTechComment4.getCreatedBy().getId(), + member.getNicknameAsString(), + originParentTechComment4.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + parentTechComment4.getContents().getCommentContents(), + parentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + null + ) + ); + + TechCommentsResponse techCommentsResponse3 = response.getContent().get(2); + List replies3 = techCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + TechCommentsResponse techCommentsResponse4 = response.getContent().get(3); + List replies4 = techCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + TechCommentsResponse techCommentsResponse5 = response.getContent().get(4); + List replies5 = techCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + } + + @Test + @DisplayName("익명 회원은 커서 방식으로 기술블로그 댓글/답글을 조회할 수 있다. (추천 많은 순)") + void getTechCommentsSortByMostRecommended() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member, + techArticle, new Count(0L), new Count(3L), new Count(0L)); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, + techArticle, new Count(0L), new Count(1L), new Count(0L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, + techArticle, new Count(0L), new Count(5L), new Count(0L)); + TechComment originParentTechComment4 = createMainTechComment(new CommentContents("최상위 댓글4"), member, + techArticle, new Count(0L), new Count(4L), new Count(0L)); + TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, + techArticle, new Count(0L), new Count(2L), new Count(0L)); + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), anonymousMember, + techArticle, new Count(0L), new Count(6L), new Count(0L)); + + techCommentRepository.saveAll(List.of( + originParentTechComment1, originParentTechComment2, originParentTechComment3, + originParentTechComment4, originParentTechComment5, originParentTechComment6 + )); + + Pageable pageable = PageRequest.of(0, 5); + + em.flush(); + em.clear(); + + // when + SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, + null, TechCommentSort.MOST_LIKED, pageable, anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(6L); + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment6.getId(), + null, + anonymousMember.getNickname(), + null, + originParentTechComment6.getContents().getCommentContents(), + originParentTechComment6.getReplyTotalCount().getCount(), + originParentTechComment6.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId() + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment1.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ) + ); + + TechCommentsResponse techCommentsResponse6 = response.getContent().get(0); + List replies6 = techCommentsResponse6.getReplies(); + assertThat(replies6).hasSize(0); + + TechCommentsResponse techCommentsResponse3 = response.getContent().get(1); + List replies3 = techCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + TechCommentsResponse techCommentsResponse4 = response.getContent().get(2); + List replies4 = techCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + TechCommentsResponse techCommentsResponse1 = response.getContent().get(3); + List replies1 = techCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(0); + + TechCommentsResponse techCommentsResponse5 = response.getContent().get(4); + List replies5 = techCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + } + + @Test + @DisplayName("익명 회원은 커서 방식으로 커서 다음의 기술블로그 댓글/답글을 조회할 수 있다.") + void getTechCommentsByCursor() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), anonymousMember, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment4 = createMainTechComment(new CommentContents("최상위 댓글4"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), member, + techArticle, new Count(0L), new Count(0L), new Count(0L)); + + originParentTechComment6.changeDeletedAt(LocalDateTime.now(), member); + + techCommentRepository.saveAll(List.of( + originParentTechComment1, originParentTechComment2, originParentTechComment3, + originParentTechComment4, originParentTechComment5, originParentTechComment6 + )); + + Pageable pageable = PageRequest.of(0, 5); + + em.flush(); + em.clear(); + + // when + SliceCommentCustom response = guestTechCommentServiceV2.getTechComments(techArticleId, + originParentTechComment6.getId(), null, pageable, anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response.getTotalOriginParentComments()).isEqualTo(5L); // 삭제된 댓글은 카운트하지 않는다 + assertThat(response).hasSizeLessThanOrEqualTo(pageable.getPageSize()) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment5.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment5.getContents().getCommentContents(), + originParentTechComment5.getReplyTotalCount().getCount(), + originParentTechComment5.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment4.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment4.getContents().getCommentContents(), + originParentTechComment4.getReplyTotalCount().getCount(), + originParentTechComment4.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment3.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment2.getId(), + member.getId(), + member.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment1.getId(), + null, + anonymousMember.getNickname(), + null, + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + true, + false, + false, + false, + anonymousMember.getId() + ) + ); + + TechCommentsResponse techCommentsResponse6 = response.getContent().get(0); + List replies6 = techCommentsResponse6.getReplies(); + assertThat(replies6).hasSize(0); + + TechCommentsResponse techCommentsResponse3 = response.getContent().get(1); + List replies3 = techCommentsResponse3.getReplies(); + assertThat(replies3).hasSize(0); + + TechCommentsResponse techCommentsResponse4 = response.getContent().get(2); + List replies4 = techCommentsResponse4.getReplies(); + assertThat(replies4).hasSize(0); + + TechCommentsResponse techCommentsResponse1 = response.getContent().get(3); + List replies1 = techCommentsResponse1.getReplies(); + assertThat(replies1).hasSize(0); + + TechCommentsResponse techCommentsResponse5 = response.getContent().get(4); + List replies5 = techCommentsResponse5.getReplies(); + assertThat(replies5).hasSize(0); + } + + @Test + @DisplayName("익명 회원이 아닌 경우 익명회원 전용 기술블로그 베스트 댓글 조회 메소드를 호출하면 예외가 발생한다.") + void findTechBestCommentsNotAnonymousMember() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + 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(() -> guestTechCommentServiceV2.findTechBestComments(3, 1L, anonymousMember.getAnonymousMemberId(), + authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명 회원이 offset에 정책에 맞게 기술블로그 베스트 댓글을 조회한다.") + void findTechBestComments() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto1 = createSocialDto("user1", name, "nickname1", password, "user1@gmail.com", + socialType, Role.ROLE_ADMIN.name()); + SocialMemberDto socialMemberDto2 = createSocialDto("user2", name, "nickname2", password, "user2@gmail.com", + socialType, role); + SocialMemberDto socialMemberDto3 = createSocialDto("user3", name, "nickname3", password, "user3@gmail.com", + socialType, role); + Member member1 = Member.createMemberBy(socialMemberDto1); + Member member2 = Member.createMemberBy(socialMemberDto2); + Member member3 = Member.createMemberBy(socialMemberDto3); + memberRepository.saveAll(List.of(member1, member2, member3)); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 회사 생성 + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + // 기술 블로그 생성 + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + + // 댓글 생성 + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), anonymousMember, + techArticle, new Count(0L), new Count(3L), new Count(0L)); + originParentTechComment1.modifyCommentContents(new CommentContents("최상위 댓글1 수정"), LocalDateTime.now()); + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글1"), member2, + techArticle, new Count(0L), new Count(2L), new Count(0L)); + TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글1"), member3, + techArticle, new Count(0L), new Count(1L), new Count(0L)); + techCommentRepository.saveAll( + List.of(originParentTechComment1, originParentTechComment2, originParentTechComment3)); + + // 추천 생성 + TechCommentRecommend techCommentRecommend = createTechCommentRecommend(true, originParentTechComment1, member2); + techCommentRecommendRepository.save(techCommentRecommend); + + // 답글 생성 + TechComment repliedTechComment = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1"), member3, + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + techCommentRepository.save(repliedTechComment); + + // when + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + List response = guestTechCommentServiceV2.findTechBestComments(3, techArticle.getId(), + anonymousMember.getAnonymousMemberId(), authentication); + + // then + assertThat(response).hasSize(3) + .extracting( + "techCommentId", + "memberId", + "author", + "maskedEmail", + "contents", + "replyTotalCount", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId" + ) + .containsExactly( + Tuple.tuple(originParentTechComment1.getId(), + null, + anonymousMember.getNickname(), + null, + originParentTechComment1.getContents().getCommentContents(), + originParentTechComment1.getReplyTotalCount().getCount(), + originParentTechComment1.getRecommendTotalCount().getCount(), + true, + false, + true, + false, + anonymousMember.getId() + ), + Tuple.tuple(originParentTechComment2.getId(), + member2.getId(), + member2.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member2.getEmailAsString()), + originParentTechComment2.getContents().getCommentContents(), + originParentTechComment2.getReplyTotalCount().getCount(), + originParentTechComment2.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ), + Tuple.tuple(originParentTechComment3.getId(), + member3.getId(), + member3.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), + originParentTechComment3.getContents().getCommentContents(), + originParentTechComment3.getReplyTotalCount().getCount(), + originParentTechComment3.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null + ) + ); + + TechCommentsResponse techCommentsResponse = response.get(0); + List replies = techCommentsResponse.getReplies(); + assertThat(replies).hasSize(1) + .extracting( + "techCommentId", + "memberId", + "techParentCommentId", + "techParentCommentMemberId", + "techParentCommentAuthor", + "techOriginParentCommentId", + "author", + "maskedEmail", + "contents", + "recommendTotalCount", + "isCommentAuthor", + "isRecommended", + "isModified", + "isDeleted", + "anonymousMemberId", + "techParentCommentAnonymousMemberId" + ).containsExactly( + Tuple.tuple(repliedTechComment.getId(), + member3.getId(), + repliedTechComment.getParent().getId(), + null, + repliedTechComment.getParent().getCreatedAnonymousBy().getNickname(), + repliedTechComment.getOriginParent().getId(), + member3.getNicknameAsString(), + CommonResponseUtil.sliceAndMaskEmail(member3.getEmailAsString()), + repliedTechComment.getContents().getCommentContents(), + repliedTechComment.getRecommendTotalCount().getCount(), + false, + false, + false, + false, + null, + repliedTechComment.getParent().getCreatedAnonymousBy().getId() + ) + ); + } + + @Test + @DisplayName("익명회원은 기술블로그 댓글을 작성할 수 있다.") + void registerTechComment() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + // 기술블로그 생성 + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + + // 댓글 등록 요청 생성 + RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, + anonymousMember.getAnonymousMemberId()); + + // when + TechCommentResponse techCommentResponse = guestTechCommentServiceV2.registerMainTechComment(techArticle.getId(), + registerCommentDto, authentication); + em.flush(); + + // then + assertThat(techCommentResponse.getTechCommentId()).isNotNull(); + + TechComment findTechComment = techCommentRepository.findById(techCommentResponse.getTechCommentId()) + .get(); + + assertAll( + // 댓글 생성 확인 + () -> assertThat(findTechComment.getContents().getCommentContents()).isEqualTo("댓글입니다."), + () -> assertThat(findTechComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findTechComment.getReplyTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findTechComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(techArticle.getId()), + // 기술블로그 댓글 수 증가 확인 + () -> assertThat(findTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo(2L) + ); + } + + @Test + @DisplayName("익명회원이 기술블로그 댓글을 작성할 때 존재하지 않는 기술블로그에 댓글을 작성하면 예외가 발생한다.") + void registerTechCommentNotFoundTechArticleException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, + anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.registerMainTechComment(0L, registerCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); + } + + @Test + @DisplayName("익명회원 전용 기술블로그 댓글을 작성할 때 익명회원이 아니면 예외가 발생한다.") + void registerTechCommentIllegalStateException() { + // given + UserPrincipal userPrincipal = UserPrincipal.createByEmailAndRoleAndSocialType(email, role, socialType); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.registerMainTechComment(1L, registerCommentDto, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원은 기술블로그 댓글에 답글을 작성할 수 있다.") + void registerRepliedTechComment() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, + techArticle); + techCommentRepository.save(parentTechComment); + Long parentTechCommentId = parentTechComment.getId(); + + RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, + anonymousMember.getAnonymousMemberId()); + + // when + TechCommentResponse techCommentResponse = guestTechCommentServiceV2.registerRepliedTechComment( + techArticleId, parentTechCommentId, parentTechCommentId, registerRepliedCommentDto, authentication); + + // then + assertThat(techCommentResponse.getTechCommentId()).isNotNull(); + + TechComment findRepliedTechComment = techCommentRepository.findById(techCommentResponse.getTechCommentId()) + .get(); + + assertAll( + // 답글 생성 확인 + () -> assertThat(findRepliedTechComment.getContents().getCommentContents()).isEqualTo("답글입니다."), + () -> assertThat(findRepliedTechComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getReplyTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findRepliedTechComment.getParent().getId()).isEqualTo(parentTechCommentId), + () -> assertThat(findRepliedTechComment.getOriginParent().getId()).isEqualTo(parentTechCommentId), + // 최상단 댓글의 답글 수 증가 확인 + () -> assertThat(findRepliedTechComment.getOriginParent().getReplyTotalCount().getCount()).isEqualTo( + 1L), + // 기술블로그 댓글 수 증가 확인 + () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( + 2L) + ); + } + + @Test + @DisplayName("익명회원은 기술블로그 댓글의 답글에 답글을 작성할 수 있다.") + void registerRepliedTechCommentToRepliedTechComment() { + // given + // 회원 생성 + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + // 댓글 생성 + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, + techArticle); + techCommentRepository.save(originParentTechComment); + Long originParentTechCommentId = originParentTechComment.getId(); + + // 답글 생성 + TechComment parentTechComment = TechComment.createRepliedTechCommentByAnonymousMember(new CommentContents("답글입니다."), + anonymousMember, techArticle, originParentTechComment, originParentTechComment); + techCommentRepository.save(parentTechComment); + Long parentTechCommentId = parentTechComment.getId(); + + RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, + anonymousMember.getAnonymousMemberId()); + + // when + TechCommentResponse techCommentResponse = guestTechCommentServiceV2.registerRepliedTechComment(techArticleId, + originParentTechCommentId, parentTechCommentId, registerRepliedCommentDto, authentication); + + // then + assertThat(techCommentResponse.getTechCommentId()).isNotNull(); + + TechComment findRepliedTechComment = techCommentRepository.findById(techCommentResponse.getTechCommentId()) + .get(); + + assertAll( + () -> assertThat(findRepliedTechComment.getContents().getCommentContents()).isEqualTo("답글입니다."), + () -> assertThat(findRepliedTechComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getReplyTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findRepliedTechComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findRepliedTechComment.getParent().getId()).isEqualTo(parentTechCommentId), + () -> assertThat(findRepliedTechComment.getOriginParent().getId()).isEqualTo(originParentTechCommentId), + // 최상단 댓글의 답글 수 증가 확인 + () -> assertThat(findRepliedTechComment.getOriginParent().getReplyTotalCount().getCount()).isEqualTo( + 1L), + // 기술블로그 댓글 수 증가 확인 + () -> assertThat(findRepliedTechComment.getTechArticle().getCommentTotalCount().getCount()).isEqualTo( + 2L) + ); + } + + @Test + @DisplayName("익명회원이 기술블로그 댓글에 답글을 작성할 때 존재하지 않는 댓글에 답글을 작성하면 예외가 발생한다.") + void registerRepliedTechCommentNotFoundTechCommentException() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + // 댓글 생성 + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); + techCommentRepository.save(techComment); + Long invalidTechCommentId = techComment.getId() + 1; + + // 답글 등록 요청 생성 + RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, + anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.registerRepliedTechComment(techArticleId, invalidTechCommentId, + invalidTechCommentId, registerRepliedCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원이 기술블로그 댓글에 답글을 작성할 때 삭제된 댓글에 답글을 작성하면 예외가 발생한다.") + void registerRepliedTechCommentDeletedTechCommentException() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + memberRepository.save(member); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + // 삭제 상태의 댓글 생성 + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); + techCommentRepository.save(techComment); + Long techCommentId = techComment.getId(); + + LocalDateTime deletedAt = LocalDateTime.of(2024, 10, 6, 0, 0, 0); + techComment.changeDeletedAt(deletedAt, member); + + em.flush(); + em.clear(); + + RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, + anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.registerRepliedTechComment(techArticleId, techCommentId, techCommentId, + registerRepliedCommentDto, authentication)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_CAN_NOT_REPLY_DELETED_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("회원이 익명회원 전용 기술블로그 댓글에 답글을 작성하면 예외가 발생한다.") + void registerRepliedTechCommentIllegalStateException() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + em.flush(); + em.clear(); + + RegisterTechCommentRequest registerRepliedTechComment = new RegisterTechCommentRequest("답글입니다."); + TechCommentDto registerRepliedCommentDto = TechCommentDto.createRegisterCommentDto(registerRepliedTechComment, null); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.registerRepliedTechComment(0L, 0L, 0L, + registerRepliedCommentDto, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("익명회원은 본인이 작성한 삭제되지 않은 댓글을 수정할 수 있다. 수정시 편집된 시각이 갱신된다.") + void modifyTechComment() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + // 댓글 생성 + TechComment techComment = TechComment.createMainTechCommentByAnonymousMember(new CommentContents("댓글입니다"), + anonymousMember, techArticle); + techCommentRepository.save(techComment); + Long techCommentId = techComment.getId(); + + ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, + anonymousMember.getAnonymousMemberId()); + + LocalDateTime modifiedDateTime = LocalDateTime.of(2024, 10, 6, 0, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(modifiedDateTime); + + // when + TechCommentResponse techCommentResponse = guestTechCommentServiceV2.modifyTechComment( + techArticleId, techCommentId, modifyCommentDto, authentication); + em.flush(); + + // then + assertThat(techCommentResponse.getTechCommentId()).isNotNull(); + + TechComment findTechComment = techCommentRepository.findById(techCommentResponse.getTechCommentId()) + .get(); + + assertAll( + () -> assertThat(findTechComment.getContents().getCommentContents()).isEqualTo("댓글 수정입니다."), + () -> assertThat(findTechComment.getBlameTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findTechComment.getRecommendTotalCount().getCount()).isEqualTo(0L), + () -> assertThat(findTechComment.getCreatedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findTechComment.getTechArticle().getId()).isEqualTo(techArticleId), + () -> assertThat(findTechComment.getId()).isEqualTo(techCommentId), + () -> assertThat(findTechComment.getContentsLastModifiedAt()).isEqualTo(modifiedDateTime) + ); + } + + @Test + @DisplayName("회원이 기술블로그 댓글을 수정할 때 익명회원 전용 기술블로그 댓글 수정 메소드를 호출하면 예외가 발생한다.") + void modifyTechCommentNotFoundMemberException() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + UserPrincipal userPrincipal = UserPrincipal.createByMember(member); + SecurityContext context = SecurityContextHolder.getContext(); + context.setAuthentication(new OAuth2AuthenticationToken(userPrincipal, userPrincipal.getAuthorities(), + userPrincipal.getSocialType().name())); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, null); + + // when // then + assertThatThrownBy(() -> guestTechCommentServiceV2.modifyTechComment(0L, 0L, modifyCommentDto, authentication)) + .isInstanceOf(IllegalStateException.class) + .hasMessage(INVALID_METHODS_CALL_MESSAGE); + } + + @Test + @DisplayName("회원이 기술블로그 댓글을 수정할 때 댓글이 존재하지 않으면 예외가 발생한다.") + void modifyTechCommentNotFoundTechArticleCommentException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정입니다."); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, + anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy(() -> guestTechCommentServiceV2.modifyTechComment(techArticleId, 0L, modifyCommentDto, authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원이 기술블로그 댓글을 수정할 때, 이미 삭제된 댓글이라면 예외가 발생한다.") + void modifyTechCommentAlreadyDeletedException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + // 회사 생성 + 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(); + + TechComment techComment = TechComment.createMainTechCommentByAnonymousMember(new CommentContents("댓글입니다"), + anonymousMember, + techArticle); + techCommentRepository.save(techComment); + Long techCommentId = techComment.getId(); + + LocalDateTime deletedAt = LocalDateTime.of(2024, 10, 6, 0, 0, 0); + techComment.changeDeletedAt(deletedAt, anonymousMember); + em.flush(); + + ModifyTechCommentRequest modifyTechCommentRequest = new ModifyTechCommentRequest("댓글 수정"); + TechCommentDto modifyCommentDto = TechCommentDto.createModifyCommentDto(modifyTechCommentRequest, + anonymousMember.getAnonymousMemberId()); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.modifyTechComment(techArticleId, techCommentId, modifyCommentDto, + authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원은 본인이 작성한, 아직 삭제되지 않은 댓글을 삭제할 수 있다.") + void deleteTechComment() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment techComment = TechComment.createMainTechCommentByAnonymousMember(new CommentContents("댓글입니다"), + anonymousMember, techArticle); + techCommentRepository.save(techComment); + Long techCommentId = techComment.getId(); + + LocalDateTime deletedAt = LocalDateTime.of(2024, 10, 6, 0, 0, 0); + when(timeProvider.getLocalDateTimeNow()).thenReturn(deletedAt); + + em.flush(); + + // when + guestTechCommentServiceV2.deleteTechComment(techArticleId, techCommentId, anonymousMember.getAnonymousMemberId(), + authentication); + + // then + TechComment findTechComment = techCommentRepository.findById(techCommentId).get(); + + assertAll( + () -> assertThat(findTechComment.getDeletedAt()).isNotNull(), + () -> assertThat(findTechComment.getDeletedAnonymousBy().getId()).isEqualTo(anonymousMember.getId()), + () -> assertThat(findTechComment.getDeletedAt()).isEqualTo(deletedAt) + ); + } + + @Test + @DisplayName("익명회원이 댓글을 삭제할 때, 이미 삭제된 댓글이라면 예외가 발생한다.") + void deleteTechCommentAlreadyDeletedException() { + // given + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + 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(); + + TechComment techComment = TechComment.createMainTechCommentByAnonymousMember(new CommentContents("댓글입니다"), + anonymousMember, techArticle); + techCommentRepository.save(techComment); + Long techCommentId = techComment.getId(); + + LocalDateTime deletedAt = LocalDateTime.of(2024, 10, 6, 0, 0, 0); + techComment.changeDeletedAt(deletedAt, anonymousMember); + em.flush(); + + // when // then + assertThatThrownBy(() -> guestTechCommentServiceV2.deleteTechComment(techArticleId, techCommentId, + anonymousMember.getAnonymousMemberId(), authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("익명회원이 댓글을 삭제할 때, 댓글이 존재하지 않으면 예외가 발생한다.") + void deleteTechCommentNotFoundException() { + // given + Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", + "https://example.com"); + companyRepository.save(company); + + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + + // 익명회원 목킹 + Authentication authentication = mock(Authentication.class); + when(authentication.getPrincipal()).thenReturn(AuthenticationMemberUtils.ANONYMOUS_USER); + + TechArticle techArticle = createTechArticle(company); + techArticleRepository.save(techArticle); + Long techArticleId = techArticle.getId(); + + // when // then + assertThatThrownBy( + () -> guestTechCommentServiceV2.deleteTechComment(techArticleId, 0L, anonymousMember.getAnonymousMemberId(), + authentication)) + .isInstanceOf(NotFoundException.class) + .hasMessage(INVALID_NOT_FOUND_TECH_COMMENT_MESSAGE); + } + + @Test + @DisplayName("댓글을 삭제할 때 회원이 익명회원 전용 댓글 삭제 메소드를 호출하면 예외가 발생한다.") + void deleteTechCommentIllegalStateException() { + // given + SocialMemberDto socialMemberDto = createSocialDto(userId, name, nickname, password, email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + + 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(() -> guestTechCommentServiceV2.deleteTechComment(0L, 0L, null, authentication)) + .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 048b8da7..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,163 +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.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; - -@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/global/security/oauth2/handler/OAuth2SuccessHandlerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandlerTest.java index 17cfbae9..f952fd57 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandlerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/handler/OAuth2SuccessHandlerTest.java @@ -56,7 +56,7 @@ void simulateOAuth2Login() { Map kakaoAttributes = new HashMap<>(); kakaoAttributes.put(KakaoMember.EMAIL, email); attributes.put(KakaoMember.KAKAO_ACCOUNT, kakaoAttributes); - UserPrincipal userPrincipal = UserPrincipal.createByMemberAndAttributes(member, attributes); + UserPrincipal userPrincipal = UserPrincipal.createByMemberAndAttributes(member, attributes, false); // OAuth2AuthenticationToken 생성 SecurityContext context = SecurityContextHolder.getContext(); @@ -82,6 +82,7 @@ public void onAuthenticationSuccessException() { @DisplayName("OAuth2.0 로그인 성공 시" + " 토큰을 생성하고 토큰을 쿠키에 저장하고" + " 로그인된 회원의 이메일과 닉네임을 쿠키에 저장하고" + + " 로그인된 회원의 신규회원 여부를 쿠키에 저장하고" + " 리다이렉트를 설정하고" + " 회원에 리프레시 토큰을 저장한다.") void onAuthenticationSuccess() throws IOException { @@ -105,6 +106,7 @@ void onAuthenticationSuccess() throws IOException { Cookie nicknameCookie = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_NICKNAME); Cookie emailCookie = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_EMAIL); Cookie isAdmin = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_IS_ADMIN); + Cookie isNewMember = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_IS_NEW); assertAll( () -> assertThat(accessCookie).isNotNull(), @@ -112,7 +114,8 @@ void onAuthenticationSuccess() throws IOException { () -> assertThat(loginStatusCookie).isNotNull(), () -> assertThat(nicknameCookie).isNotNull(), () -> assertThat(emailCookie).isNotNull(), - () -> assertThat(isAdmin).isNotNull() + () -> assertThat(isAdmin).isNotNull(), + () -> assertThat(isNewMember).isNotNull() ); assertAll( () -> assertThat(accessCookie.isHttpOnly()).isFalse(), diff --git a/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipalTest.java b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipalTest.java index efb9ec21..b997901a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipalTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/model/UserPrincipalTest.java @@ -52,7 +52,7 @@ void createByMemberAndAttributes() { Map attributes = new HashMap<>(); // when - UserPrincipal userPrincipal = UserPrincipal.createByMemberAndAttributes(member, attributes); + UserPrincipal userPrincipal = UserPrincipal.createByMemberAndAttributes(member, attributes, false); // then assertAll( diff --git a/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/AppOAuth2MemberServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/AppOAuth2MemberServiceTest.java index f7410f31..2942d324 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/AppOAuth2MemberServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/security/oauth2/service/AppOAuth2MemberServiceTest.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; + +import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -70,11 +72,12 @@ void register() { memberNicknameDictionaryRepository.saveAll(nicknameDictionaryWords); // when - oAuth2MemberService.register(mockOAuth2UserProvider, mockOAuth2User); + UserPrincipal userPrincipal = oAuth2MemberService.register(mockOAuth2UserProvider, mockOAuth2User); // then Member member = memberRepository.findMemberByUserIdAndSocialType(userId, socialType).get(); assertThat(member).isNotNull(); + assertThat(userPrincipal.isNewMember()).isEqualTo(true); } @Test diff --git a/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java index 0019e798..d55d547e 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/utils/CookieUtilsTest.java @@ -183,6 +183,7 @@ void addCookie() { int maxAge = 100; boolean isHttpOnly = true; boolean isSecure = false; + String sameSite = "None"; // when CookieUtils.addCookieToResponse(response, name, value, maxAge, isHttpOnly, isSecure); @@ -195,7 +196,8 @@ void addCookie() { () -> assertThat(cookie.getValue()).isEqualTo(value), () -> assertThat(cookie.getMaxAge()).isEqualTo(maxAge), () -> assertThat(cookie.isHttpOnly()).isEqualTo(isHttpOnly), - () -> assertThat(cookie.getSecure()).isEqualTo(isSecure) + () -> assertThat(cookie.getSecure()).isEqualTo(isSecure), + () -> assertThat(cookie.getAttribute("SameSite")).isEqualTo(sameSite) ); } @@ -265,19 +267,22 @@ void configMemberCookie(String inputIsAdmin, String expectedIsAdmin) { Member member = Member.createMemberBy(socialMemberDto); String encodedNickname = URLEncoder.encode(member.getNicknameAsString(), StandardCharsets.UTF_8); String email = member.getEmailAsString(); + boolean isNewMember = true; // when - CookieUtils.configMemberCookie(response, member); + CookieUtils.configMemberCookie(response, member, isNewMember); // then Cookie nicknameCookie = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_NICKNAME); Cookie emailCookie = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_EMAIL); Cookie isAdmin = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_IS_ADMIN); + Cookie isNewMemberCookie = response.getCookie(JwtCookieConstant.DEVDEVDEV_MEMBER_IS_NEW); assertAll( () -> assertThat(nicknameCookie).isNotNull(), () -> assertThat(emailCookie).isNotNull(), - () -> assertThat(isAdmin).isNotNull() + () -> assertThat(isAdmin).isNotNull(), + () -> assertThat(isNewMemberCookie).isNotNull() ); assertAll( @@ -303,6 +308,14 @@ void configMemberCookie(String inputIsAdmin, String expectedIsAdmin) { () -> assertThat(isAdmin.getSecure()).isTrue(), () -> assertThat(isAdmin.isHttpOnly()).isFalse() ); + + assertAll( + () -> assertThat(isNewMemberCookie.getName()).isEqualTo(JwtCookieConstant.DEVDEVDEV_MEMBER_IS_NEW), + () -> assertThat(isNewMemberCookie.getValue()).isEqualTo(String.valueOf(isNewMember)), + () -> assertThat(isNewMemberCookie.getMaxAge()).isEqualTo(CookieUtils.DEFAULT_MAX_AGE), + () -> assertThat(isNewMemberCookie.getSecure()).isTrue(), + () -> assertThat(isNewMemberCookie.isHttpOnly()).isFalse() + ); } private SocialMemberDto createSocialDto(String userId, String name, String nickname, String password, String email, diff --git a/src/test/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtilsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtilsTest.java new file mode 100644 index 00000000..0226a38e --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/global/utils/HangulUtilsTest.java @@ -0,0 +1,89 @@ +package com.dreamypatisiel.devdevdev.global.utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class HangulUtilsTest { + + @ParameterizedTest + @ValueSource(strings = {"꿈빛 파티시엘", "Hello꿈빛", "ㄱㄴㄷ", "댑댑댑", "123꿈빛파티시엘", "!@#꿈빛$%^"}) + @DisplayName("한글이 포함된 문자열이면 true를 리턴한다.") + void hasHangulWithKorean(String input) { + // when // then + assertThat(HangulUtils.hasHangul(input)).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"Hello World", "spring", "!@#$%", "", " ", "123456789"}) + @DisplayName("한글이 포함되지 않은 문자열은 false를 리턴한다.") + void hasHangulWithoutKorean(String input) { + // when // then + assertThat(HangulUtils.hasHangul(input)).isFalse(); + } + + @ParameterizedTest + @CsvSource({ + "꿈빛, ㄲㅜㅁㅂㅣㅊ", + "꿈빛 파티시엘, ㄲㅜㅁㅂㅣㅊ ㅍㅏㅌㅣㅅㅣㅇㅔㄹ", + "개발자, ㄱㅐㅂㅏㄹㅈㅏ", + "Hello꿈빛, Helloㄲㅜㅁㅂㅣㅊ" + }) + @DisplayName("한글 문자열을 자모음으로 분해한다.") + void convertToJamo(String input, String expected) { + // when + String result = HangulUtils.convertToJamo(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "안녕!@#하세요$%^, ㅇㅏㄴㄴㅕㅇ!@#ㅎㅏㅅㅔㅇㅛ$%^", + "Spring Boot 3.0, Spring Boot 3.0", + "한글123영어, ㅎㅏㄴㄱㅡㄹ123ㅇㅕㅇㅇㅓ" + }) + @DisplayName("특수문자와 혼합된 문자열을 자모음으로 분해한다.") + void convertToJamoWithSpecialCharacters(String input, String expected) { + // when + String result = HangulUtils.convertToJamo(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "꿈빛 파티시엘, ㄲㅂ ㅍㅌㅅㅇ", + "댑댑댑, ㄷㄷㄷ", + "댑구리 99, ㄷㄱㄹ 99" + }) + @DisplayName("한글 문자열에서 초성을 추출한다.") + void extractChosung(String input, String expected) { + // when + String result = HangulUtils.extractChosung(input); + + // then + assertThat(result).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource({ + "꿈빛!@#파티시엘$%^, ㄲㅂ!@#ㅍㅌㅅㅇ$%^", + "React.js개발자, React.jsㄱㅂㅈ", + "Spring Boot 3.0, Spring Boot 3.0", + "꿈빛123개발자, ㄲㅂ123ㄱㅂㅈ" + }) + @DisplayName("특수문자와 혼합된 문자열에서 초성을 추출한다.") + void extractChosungWithSpecialCharacters(String input, String expected) { + // when + String result = HangulUtils.extractChosung(input); + + // then + assertThat(result).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java new file mode 100644 index 00000000..8e981639 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java @@ -0,0 +1,99 @@ +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; +import org.testcontainers.containers.MySQLContainer; +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 테스트컨테이너를 제공하는 공통 클래스 + * 각 테스트 클래스가 독립적인 컨테이너를 사용하도록 변경(현재 사용X) + */ +@Testcontainers +public abstract class MySQLTestContainer { + + @Container + @ServiceConnection + protected 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" + ); + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + // 컨테이너 시작 확인 + 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/LogoutControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/LogoutControllerTest.java similarity index 96% rename from src/test/java/com/dreamypatisiel/devdevdev/web/controller/LogoutControllerTest.java rename to src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/LogoutControllerTest.java index a7a30895..23bd418a 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/LogoutControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/LogoutControllerTest.java @@ -1,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.web.controller; +package com.dreamypatisiel.devdevdev.web.controller.member; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.BEARER_PREFIX; @@ -17,6 +17,7 @@ import com.dreamypatisiel.devdevdev.global.security.jwt.model.Token; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; +import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.servlet.http.Cookie; import java.nio.charset.StandardCharsets; diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/MyPageControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java similarity index 93% rename from src/test/java/com/dreamypatisiel/devdevdev/web/controller/MyPageControllerTest.java rename to src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java index 8e813e2d..cc884906 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/MyPageControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerTest.java @@ -1,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.web.controller; +package com.dreamypatisiel.devdevdev.web.controller.member; import com.dreamypatisiel.devdevdev.domain.entity.*; import com.dreamypatisiel.devdevdev.domain.entity.embedded.*; @@ -16,13 +16,12 @@ 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; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.UserPrincipal; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; +import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.request.member.RecordMemberExitSurveyAnswerRequest; import com.dreamypatisiel.devdevdev.web.dto.request.member.RecordMemberExitSurveyQuestionOptionsRequest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; @@ -71,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; @@ -108,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(); } @@ -180,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()) @@ -852,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() @@ -897,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/MyPageControllerUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java similarity index 74% rename from src/test/java/com/dreamypatisiel/devdevdev/web/controller/MyPageControllerUsedMockServiceTest.java rename to src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java index 17b4e70c..fe0b13b2 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/MyPageControllerUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/MyPageControllerUsedMockServiceTest.java @@ -1,11 +1,12 @@ -package com.dreamypatisiel.devdevdev.web.controller; +package com.dreamypatisiel.devdevdev.web.controller.member; +import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.FAIL; import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -14,11 +15,16 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.service.member.MemberNicknameDictionaryService; import com.dreamypatisiel.devdevdev.domain.service.member.MemberService; +import com.dreamypatisiel.devdevdev.exception.NicknameException; 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.SliceCustom; +import com.dreamypatisiel.devdevdev.web.dto.request.member.ChangeNicknameRequest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import com.dreamypatisiel.devdevdev.web.dto.response.comment.MyWrittenCommentResponse; import java.nio.charset.StandardCharsets; @@ -26,6 +32,7 @@ import java.util.List; import com.dreamypatisiel.devdevdev.web.dto.response.subscription.SubscribedCompanyResponse; +import jakarta.persistence.EntityManager; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -43,6 +50,108 @@ public class MyPageControllerUsedMockServiceTest extends SupportControllerTest { MemberRepository memberRepository; @MockBean MemberService memberService; + @MockBean + MemberNicknameDictionaryService memberNicknameDictionaryService; + @Autowired + EntityManager em; + + @Test + @DisplayName("회원은 랜덤 닉네임을 생성할 수 있다.") + void getRandomNickname() throws Exception { + // given + String result = "주말에 공부하는 토마토"; + + // when + when(memberNicknameDictionaryService.createRandomNickname()).thenReturn(result); + + // then + mockMvc.perform(get("/devdevdev/api/v1/mypage/nickname/random") + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data").isString()); + } + + @Test + @DisplayName("회원은 닉네임을 변경할 수 있다.") + void changeNickname() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + request.setNickname(newNickname); + + // when + when(memberService.changeNickname(any(), any())).thenReturn(newNickname); + + // then + mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(request)) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andExpect(status().isOk()) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())) + .andExpect(jsonPath("$.data").value(newNickname)); + + // 서비스 메서드가 호출되었는지 검증 + verify(memberService).changeNickname(eq(newNickname), any()); + } + + @Test + @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + void changeNicknameThrowsExceptionWhenChangedWithin24Hours() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + request.setNickname(newNickname); + + // when + doThrow(new NicknameException(NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE)) + .when(memberService).changeNickname(any(), any()); + + // then + mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(request)) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) + .andExpect(jsonPath("$.message").isString()) + .andExpect(jsonPath("$.message").value(NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE)) + .andExpect(jsonPath("$.errorCode").value(HttpStatus.BAD_REQUEST.value())); + + // 서비스 메서드가 호출되었는지 검증 + verify(memberService).changeNickname(eq(newNickname), any()); + } + + @Test + @DisplayName("회원은 닉네임 변경 가능 여부를 확인할 수 있다.") + void canChangeNickname() throws Exception { + // given + boolean result = true; + + // when + when(memberService.canChangeNickname(any())).thenReturn(result); + + // then + mockMvc.perform(get("/devdevdev/api/v1/mypage/nickname/changeable") + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data").isBoolean()); + } @Test @DisplayName("회원이 내가 썼어요 댓글을 조회한다.") @@ -277,4 +386,10 @@ private SocialMemberDto createSocialDto(String userId, String name, String nickN .role(Role.valueOf(role)) .build(); } + + private ChangeNicknameRequest createChangeNicknameRequest(String nickname) { + return ChangeNicknameRequest.builder() + .nickname(nickname) + .build(); + } } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/TokenControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenControllerTest.java similarity index 98% rename from src/test/java/com/dreamypatisiel/devdevdev/web/controller/TokenControllerTest.java rename to src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenControllerTest.java index a107d2aa..c3168bd5 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/TokenControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/member/TokenControllerTest.java @@ -1,4 +1,4 @@ -package com.dreamypatisiel.devdevdev.web.controller; +package com.dreamypatisiel.devdevdev.web.controller.member; import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_ACCESS_TOKEN; import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_LOGIN_STATUS; @@ -19,6 +19,7 @@ import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.global.security.oauth2.model.SocialMemberDto; import com.dreamypatisiel.devdevdev.global.utils.CookieUtils; +import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.servlet.http.Cookie; import java.util.Date; 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..e3fb8a98 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) { + long commentCount, long viewCount, Boolean isBookmarked, Double 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/pick/PickCommentControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentControllerTest.java index 10bb1b5f..80dea908 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/pick/PickCommentControllerTest.java @@ -1,5 +1,6 @@ package com.dreamypatisiel.devdevdev.web.controller.pick; +import static com.dreamypatisiel.devdevdev.web.WebConstant.HEADER_ANONYMOUS_MEMBER_ID; import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -535,7 +536,7 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { new Count(0), member5, pick, null); PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), new Count(0), member6, pick, null); - originParentPickComment6.changeDeletedAt(LocalDateTime.now(), member6); + originParentPickComment6.changeDeletedAtByMember(LocalDateTime.now(), member6); pickCommentRepository.saveAll( List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, @@ -684,7 +685,7 @@ void getPickCommentsFirstPickOption(PickCommentSort pickCommentSort) throws Exce new Count(0), member5, pick, null); PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), new Count(0), member6, pick, null); - originParentPickComment6.changeDeletedAt(LocalDateTime.now(), member6); + originParentPickComment6.changeDeletedAtByMember(LocalDateTime.now(), member6); pickCommentRepository.saveAll( List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, originParentPickComment3, originParentPickComment2, originParentPickComment1)); @@ -912,7 +913,7 @@ void findPickBestComments() throws Exception { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("너무 행복하다"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("사랑해요~"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -939,6 +940,7 @@ void findPickBestComments() throws Exception { .andExpect(jsonPath("$.datas.[0].pickCommentId").isNumber()) .andExpect(jsonPath("$.datas.[0].createdAt").isString()) .andExpect(jsonPath("$.datas.[0].memberId").isNumber()) + .andExpect(jsonPath("$.datas.[0].anonymousMemberId").isEmpty()) .andExpect(jsonPath("$.datas.[0].author").isString()) .andExpect(jsonPath("$.datas.[0].isCommentOfPickAuthor").isBoolean()) .andExpect(jsonPath("$.datas.[0].isCommentAuthor").isBoolean()) @@ -966,6 +968,7 @@ void findPickBestComments() throws Exception { .andExpect(jsonPath("$.datas.[0].replies.[0].isModified").isBoolean()) .andExpect(jsonPath("$.datas.[0].replies.[0].isDeleted").isBoolean()) .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentMemberId").isNumber()) + .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentAnonymousMemberId").isEmpty()) .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentAuthor").isString()); } @@ -1038,7 +1041,7 @@ void findPickBestCommentsAnonymous() throws Exception { originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("너무 행복하다"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("사랑해요~"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -1055,6 +1058,7 @@ void findPickBestCommentsAnonymous() throws Exception { pick.getId()) .queryParam("size", "3") .contentType(MediaType.APPLICATION_JSON) + .header(HEADER_ANONYMOUS_MEMBER_ID, "anonymousMemberId") .characterEncoding(StandardCharsets.UTF_8)) .andDo(print()) .andExpect(status().isOk()) @@ -1064,6 +1068,7 @@ void findPickBestCommentsAnonymous() throws Exception { .andExpect(jsonPath("$.datas.[0].pickCommentId").isNumber()) .andExpect(jsonPath("$.datas.[0].createdAt").isString()) .andExpect(jsonPath("$.datas.[0].memberId").isNumber()) + .andExpect(jsonPath("$.datas.[0].anonymousMemberId").isEmpty()) .andExpect(jsonPath("$.datas.[0].author").isString()) .andExpect(jsonPath("$.datas.[0].isCommentOfPickAuthor").isBoolean()) .andExpect(jsonPath("$.datas.[0].isCommentAuthor").isBoolean()) @@ -1091,6 +1096,7 @@ void findPickBestCommentsAnonymous() throws Exception { .andExpect(jsonPath("$.datas.[0].replies.[0].isModified").isBoolean()) .andExpect(jsonPath("$.datas.[0].replies.[0].isDeleted").isBoolean()) .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentMemberId").isNumber()) + .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentAnonymousMemberId").isEmpty()) .andExpect(jsonPath("$.datas.[0].replies.[0].pickParentCommentAuthor").isString()); } diff --git a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordControllerTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordControllerTest.java index f78981e5..7193528c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordControllerTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/controller/techArticle/KeywordControllerTest.java @@ -1,52 +1,35 @@ package com.dreamypatisiel.devdevdev.web.controller.techArticle; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -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.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticKeywordService; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword.TechKeywordService; import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.ResultActions; -class KeywordControllerTest extends SupportControllerTest { +import java.nio.charset.StandardCharsets; +import java.util.List; - @Autowired - ElasticKeywordService elasticKeywordService; - @Autowired - ElasticKeywordRepository elasticKeywordRepository; - @Autowired - MemberRepository memberRepository; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @AfterEach - void afterEach() { - elasticKeywordRepository.deleteAll(); - } +class KeywordControllerTest extends SupportControllerTest { + + @MockBean + TechKeywordService techKeywordService; @Test @DisplayName("기술블로그 키워드를 검색하면 자동완성 키워드 후보 리스트를 최대 20개 반환한다.") void autocompleteKeyword() throws Exception { // 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 = "자"; + List result = List.of("자바", "자바 스크립트", "자바가 최고야"); + given(techKeywordService.autocompleteKeyword(prefix)).willReturn(result); // when // then ResultActions actions = mockMvc.perform(get(DEFAULT_PATH_V1 + "/keywords/auto-complete") 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 ec4acb50..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 @@ -1,5 +1,16 @@ package com.dreamypatisiel.devdevdev.web.controller.techArticle; +import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +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.Company; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.TechArticle; @@ -12,27 +23,28 @@ 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 static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; import com.dreamypatisiel.devdevdev.domain.repository.CompanyRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.TechArticleServiceStrategy; import com.dreamypatisiel.devdevdev.global.common.TimeProvider; 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; +import com.dreamypatisiel.devdevdev.web.WebConstant; import com.dreamypatisiel.devdevdev.web.controller.SupportControllerTest; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; -import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; 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; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -46,13 +58,6 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; class TechArticleCommentControllerTest extends SupportControllerTest { @@ -70,7 +75,10 @@ class TechArticleCommentControllerTest extends SupportControllerTest { TimeProvider timeProvider; @Autowired EntityManager em; + @Autowired + TechArticleServiceStrategy techArticleServiceStrategy; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -79,11 +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(); @@ -103,17 +107,13 @@ void registerTechCommentByAnonymous() throws Exception { @Test @DisplayName("회원은 기술블로그 댓글을 작성할 수 있다.") - void registerTechComment() throws Exception { + void registerTechCommentByMember() throws Exception { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "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(); @@ -139,6 +139,40 @@ void registerTechComment() throws Exception { .andExpect(jsonPath("$.data.techCommentId").isNumber()); } + @Test + @DisplayName("익명 회원은 기술블로그 댓글을 작성할 수 있다.") + void registerTechCommentByAnonymousMember() throws Exception { + // given + 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 id = techArticle.getId(); + + SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", + "꿈빛파티시엘", "1234", email, socialType, role); + Member member = Member.createMemberBy(socialMemberDto); + member.updateRefreshToken(refreshToken); + memberRepository.save(member); + + RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글 내용입니다."); + + // when // then + mockMvc.perform(post("/devdevdev/api/v1/articles/{id}/comments", id) + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8) + .header(WebConstant.HEADER_ANONYMOUS_MEMBER_ID, "anonymous-member-id") + .content(om.writeValueAsString(registerTechCommentRequest))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data").isMap()) + .andExpect(jsonPath("$.data.techCommentId").isNumber()); + } + @Test @DisplayName("회원이 기술블로그 댓글을 작성할 때 존재하지 않는 기술블로그라면 예외가 발생한다.") void registerTechCommentNotFoundTechArticleException() throws Exception { @@ -147,11 +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; @@ -184,11 +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(); @@ -216,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(); @@ -253,13 +275,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -295,13 +315,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -335,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(); @@ -370,13 +386,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -413,13 +427,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -451,13 +463,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); // when // then @@ -480,9 +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(); @@ -492,12 +500,12 @@ void registerRepliedTechComment() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechComment originParentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(originParentTechComment); Long originParentTechCommentId = originParentTechComment.getId(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("답글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("답글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); @@ -535,18 +543,16 @@ 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(); - TechComment originParentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(originParentTechComment); Long originParentTechCommentId = originParentTechComment.getId(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("답글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("답글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); @@ -585,9 +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(); @@ -703,12 +707,10 @@ 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.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); em.flush(); @@ -753,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); // 댓글 생성 @@ -841,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); // 댓글 생성 @@ -863,8 +861,7 @@ void getTechBestCommentsAnonymous() throws Exception { // 답글 생성 TechComment repliedTechComment = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1"), member3, - techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), - new Count(0L)); + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); techCommentRepository.save(repliedTechComment); // when // then @@ -872,6 +869,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), techArticle.getId()) .queryParam("size", "3") .contentType(MediaType.APPLICATION_JSON) + .header(WebConstant.HEADER_ANONYMOUS_MEMBER_ID, "anonymousMemberId") .characterEncoding(StandardCharsets.UTF_8)) .andDo(print()) .andExpect(status().isOk()) @@ -973,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..a1f532a5 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.0 + ); + + 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.0 + ); + + 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, Double 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/KeywordControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java index 5b5b088f..560dd455 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/KeywordControllerDocsTest.java @@ -1,9 +1,19 @@ package com.dreamypatisiel.devdevdev.web.docs; +import com.dreamypatisiel.devdevdev.domain.service.techArticle.keyword.TechKeywordService; +import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -15,47 +25,18 @@ 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.repository.member.MemberRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.document.ElasticKeyword; -import com.dreamypatisiel.devdevdev.elastic.domain.repository.ElasticKeywordRepository; -import com.dreamypatisiel.devdevdev.elastic.domain.service.ElasticKeywordService; -import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; -import java.nio.charset.StandardCharsets; -import java.util.List; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.ResultActions; - class KeywordControllerDocsTest extends SupportControllerDocsTest { - @Autowired - ElasticKeywordService elasticKeywordService; - @Autowired - ElasticKeywordRepository elasticKeywordRepository; - @Autowired - MemberRepository memberRepository; - - @AfterEach - void afterEach() { - elasticKeywordRepository.deleteAll(); - } + @MockBean + TechKeywordService techKeywordService; @Test @DisplayName("기술블로그 키워드를 검색하면 자동완성 키워드 후보 리스트를 최대 20개 반환한다.") void autocompleteKeyword() throws Exception { // 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 = "자"; + List result = List.of("자바", "자바 스크립트", "자바가 최고야"); + given(techKeywordService.autocompleteKeyword(prefix)).willReturn(result); // when // then ResultActions actions = mockMvc.perform(get(DEFAULT_PATH_V1 + "/keywords/auto-complete") @@ -81,5 +62,4 @@ void autocompleteKeyword() throws Exception { ) )); } - } \ 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/MyPageControllerDocsUsedMockServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java index 13fdc0f0..926d5e03 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsUsedMockServiceTest.java @@ -6,34 +6,40 @@ import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.myWrittenCommentSort; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.stringOrNull; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.uniqueCommentIdType; +import static com.dreamypatisiel.devdevdev.web.dto.response.ResultType.SUCCESS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.payload.JsonFieldType.*; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +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.Member; import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; +import com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; +import com.dreamypatisiel.devdevdev.domain.service.member.MemberNicknameDictionaryService; import com.dreamypatisiel.devdevdev.domain.service.member.MemberService; +import com.dreamypatisiel.devdevdev.exception.NicknameException; 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.request.member.ChangeNicknameRequest; +import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import com.dreamypatisiel.devdevdev.web.dto.response.comment.MyWrittenCommentResponse; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; @@ -47,6 +53,7 @@ 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 org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; @@ -57,6 +64,144 @@ public class MyPageControllerDocsUsedMockServiceTest extends SupportControllerDo MemberRepository memberRepository; @MockBean MemberService memberService; + @MockBean + MemberNicknameDictionaryService memberNicknameDictionaryService; + + @Test + @DisplayName("회원은 랜덤 닉네임을 생성할 수 있다.") + void getRandomNickname() throws Exception { + // given + String result = "주말에 공부하는 토마토"; + + // when + when(memberNicknameDictionaryService.createRandomNickname()).thenReturn(result); + + // then + ResultActions actions = mockMvc.perform(get("/devdevdev/api/v1/mypage/nickname/random") + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isOk()); + + // docs + actions.andDo(document("random-nickname", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + ), + responseFields( + fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), + fieldWithPath("data").type(JsonFieldType.STRING).description("응답 데이터(생성된 랜덤 닉네임)") + ) + )); + } + + @Test + @DisplayName("회원은 닉네임을 변경할 수 있다.") + void changeNickname() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + + // when + when(memberService.changeNickname(any(), any())).thenReturn(newNickname); + + // then + mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(request)) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andExpect(status().isOk()) + .andDo(document("change-nickname", + requestFields( + fieldWithPath("nickname").description("변경할 닉네임") + ), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + ), + responseFields( + fieldWithPath("resultType").description("성공 여부"), + fieldWithPath("data").description("변경된 닉네임") + ) + )); + + verify(memberService).changeNickname(eq(newNickname), any()); + } + + @Test + @DisplayName("회원이 24시간 이내에 닉네임을 변경한 적이 있다면 예외가 발생한다.") + void changeNicknameThrowsExceptionWhenChangedWithin24Hours() throws Exception { + // given + String newNickname = "변경된 닉네임"; + ChangeNicknameRequest request = createChangeNicknameRequest(newNickname); + request.setNickname(newNickname); + + // when + doThrow(new NicknameException(NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE)) + .when(memberService).changeNickname(any(), any()); + + // then + mockMvc.perform(patch("/devdevdev/api/v1/mypage/nickname") + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(request)) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.resultType").value(ResultType.FAIL.name())) + .andExpect(jsonPath("$.message").isString()) + .andExpect(jsonPath("$.message").value(NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE)) + .andExpect(jsonPath("$.errorCode").value(HttpStatus.BAD_REQUEST.value())) + + .andDo(document("change-nickname-within-24hours-exception", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + ), + responseFields( + fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), + fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지"), + fieldWithPath("errorCode").type(JsonFieldType.NUMBER).description("에러 코드") + ) + )); + } + + @Test + @DisplayName("회원은 닉네임 변경 가능 여부를 확인할 수 있다.") + void canChangeNickname() throws Exception { + // given + boolean result = true; + + // when + when(memberService.canChangeNickname(any())).thenReturn(result); + + // then + mockMvc.perform(get("/devdevdev/api/v1/mypage/nickname/changeable") + .contentType(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8) + .header(SecurityConstant.AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultType").value(SUCCESS.name())) + .andExpect(jsonPath("$.data").isNotEmpty()) + .andExpect(jsonPath("$.data").isBoolean()) + + .andDo(document("can-change-nickname", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + requestHeaders( + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + ), + responseFields( + fieldWithPath("resultType").type(JsonFieldType.STRING).description("응답 결과"), + fieldWithPath("data").type(JsonFieldType.BOOLEAN).description("응답 데이터(변경 가능 여부)") + ) + )); + } @Test @DisplayName("회원이 내가 썼어요 댓글을 조회한다.") @@ -389,4 +534,10 @@ private SocialMemberDto createSocialDto(String userId, String name, String nickN .role(Role.valueOf(role)) .build(); } + + private ChangeNicknameRequest createChangeNicknameRequest(String nickname) { + return ChangeNicknameRequest.builder() + .nickname(nickname) + .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..183d5e82 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) { + long commentCount, long viewCount, Boolean isBookmarked, Double 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/PickCommentControllerDocsTest.java b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java index 35794988..f7aca371 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/PickCommentControllerDocsTest.java @@ -1,6 +1,7 @@ package com.dreamypatisiel.devdevdev.web.docs; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; +import static com.dreamypatisiel.devdevdev.web.WebConstant.HEADER_ANONYMOUS_MEMBER_ID; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.pickCommentSortType; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.pickOptionType; @@ -34,6 +35,7 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; +import com.dreamypatisiel.devdevdev.domain.entity.AnonymousMember; import com.dreamypatisiel.devdevdev.domain.entity.Member; import com.dreamypatisiel.devdevdev.domain.entity.Pick; import com.dreamypatisiel.devdevdev.domain.entity.PickComment; @@ -50,6 +52,7 @@ import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; import com.dreamypatisiel.devdevdev.domain.entity.enums.SocialType; import com.dreamypatisiel.devdevdev.domain.policy.PickPopularScorePolicy; +import com.dreamypatisiel.devdevdev.domain.repository.member.AnonymousMemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRecommendRepository; import com.dreamypatisiel.devdevdev.domain.repository.pick.PickCommentRepository; @@ -100,6 +103,8 @@ public class PickCommentControllerDocsTest extends SupportControllerDocsTest { @Autowired MemberRepository memberRepository; @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired PickPopularScorePolicy pickPopularScorePolicy; @Autowired PickOptionImageRepository pickOptionImageRepository; @@ -115,7 +120,7 @@ public class PickCommentControllerDocsTest extends SupportControllerDocsTest { AmazonS3 amazonS3Client; @Test - @DisplayName("회원이 승인 상태의 픽픽픽에 댓글을 작성한다.") + @DisplayName("승인 상태의 픽픽픽에 댓글을 작성한다.") void registerPickComment() throws Exception { // given // 회원 생성 @@ -165,7 +170,8 @@ void registerPickComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디") @@ -236,7 +242,8 @@ void registerPickCommentBindExceptionPickVotePublicIsNull(Boolean isPickVotePubl preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디") @@ -298,7 +305,8 @@ void registerPickRepliedComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -364,7 +372,8 @@ void registerPickRepliedCommentBindException(String contents) throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -416,7 +425,8 @@ void modifyPickComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -472,7 +482,8 @@ void modifyPickCommentBindException(String contents) throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -520,7 +531,8 @@ void deletePickComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -575,7 +587,8 @@ void deletePickCommentOtherMemberException() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디"), @@ -614,6 +627,10 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { Member member7 = Member.createMemberBy(socialMemberDto7); memberRepository.saveAll(List.of(member1, member2, member3, member4, member5, member6, member7)); + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명의 댑댑이 123"); + anonymousMemberRepository.save(anonymousMember); + // 픽픽픽 생성 Pick pick = createPick(new Title("꿈파 워크샵 어디로 갈까요?"), ContentStatus.APPROVAL, new Count(6), member1); pickRepository.save(pick); @@ -642,10 +659,10 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { PickComment originParentPickComment4 = createPickComment(new CommentContents("나는 소영소"), false, new Count(0), new Count(0), member4, pick, member4PickVote); PickComment originParentPickComment5 = createPickComment(new CommentContents("힘들면 힘을내자!"), false, new Count(0), - new Count(0), member5, pick, null); + new Count(0), anonymousMember, pick, null); PickComment originParentPickComment6 = createPickComment(new CommentContents("댓글6"), false, new Count(0), - new Count(0), member6, pick, null); - originParentPickComment6.changeDeletedAt(LocalDateTime.now(), member6); + new Count(0), anonymousMember, pick, null); + originParentPickComment6.changeDeletedAtByMember(LocalDateTime.now(), member6); pickCommentRepository.saveAll( List.of(originParentPickComment6, originParentPickComment5, originParentPickComment4, originParentPickComment3, originParentPickComment2, originParentPickComment1)); @@ -657,9 +674,9 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { originParentPickComment1, pickReply1); PickComment pickReply3 = createReplidPickComment(new CommentContents("소주 없이는 못살아!!!!"), member4, pick, originParentPickComment2, originParentPickComment2); - PickComment pickReply4 = createReplidPickComment(new CommentContents("벌써 9월이당"), member5, pick, + PickComment pickReply4 = createReplidPickComment(new CommentContents("벌써 9월이당"), anonymousMember, pick, originParentPickComment2, originParentPickComment2); - pickReply4.changeDeletedAt(LocalDateTime.now(), member5); + pickReply4.changeDeletedAtByMember(LocalDateTime.now(), member5); pickCommentRepository.saveAll(List.of(pickReply4, pickReply3, pickReply2, pickReply1)); em.flush(); @@ -684,7 +701,8 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명 회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디") @@ -704,7 +722,8 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { fieldWithPath("data.content").type(ARRAY).description("픽픽픽 댓글/답글 메인 배열"), fieldWithPath("data.content[].pickCommentId").type(NUMBER).description("픽픽픽 댓글 아이디"), fieldWithPath("data.content[].createdAt").type(STRING).description("픽픽픽 댓글 작성일시"), - fieldWithPath("data.content[].memberId").type(NUMBER).description("픽픽픽 댓글 작성자 아이디"), + fieldWithPath("data.content[].memberId").optional().description("픽픽픽 댓글 작성자 아이디"), + fieldWithPath("data.content[].anonymousMemberId").optional().description("픽픽픽 댓글 익명 작성자 아이디"), fieldWithPath("data.content[].author").type(STRING).description("픽픽픽 댓글 작성자 닉네임"), fieldWithPath("data.content[].isCommentOfPickAuthor").type(BOOLEAN) .description("댓글 작성자가 픽픽픽 작성자인지 여부"), @@ -712,7 +731,7 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { .description("로그인한 회원이 댓글 작성자인지 여부"), fieldWithPath("data.content[].isRecommended").type(BOOLEAN) .description("로그인한 회원이 댓글 추천 여부"), - fieldWithPath("data.content[].maskedEmail").type(STRING).description("픽픽픽 댓글 작성자 이메일"), + fieldWithPath("data.content[].maskedEmail").optional().description("픽픽픽 댓글 작성자 이메일"), fieldWithPath("data.content[].votedPickOption").optional().type(STRING) .description("픽픽픽 투표 선택 타입").attributes(pickOptionType()), fieldWithPath("data.content[].votedPickOptionTitle").optional().type(STRING) @@ -729,7 +748,9 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { fieldWithPath("data.content[].replies").type(ARRAY).description("픽픽픽 답글 배열"), fieldWithPath("data.content[].replies[].pickCommentId").type(NUMBER).description("픽픽픽 답글 아이디"), - fieldWithPath("data.content[].replies[].memberId").type(NUMBER).description("픽픽픽 답글 작성자 아이디"), + fieldWithPath("data.content[].replies[].memberId").type(NUMBER).optional().description("픽픽픽 답글 작성자 아이디"), + fieldWithPath("data.content[].replies[].anonymousMemberId").type(NUMBER).optional() + .description("픽픽픽 답글 익명 작성자 아이디"), fieldWithPath("data.content[].replies[].pickParentCommentId").type(NUMBER) .description("픽픽픽 답글의 부모 댓글 아이디"), fieldWithPath("data.content[].replies[].pickOriginParentCommentId").type(NUMBER) @@ -742,8 +763,7 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { fieldWithPath("data.content[].replies[].isRecommended").type(BOOLEAN) .description("로그인한 회원이 답글 추천 여부"), fieldWithPath("data.content[].replies[].author").type(STRING).description("픽픽픽 답글 작성자 닉네임"), - fieldWithPath("data.content[].replies[].maskedEmail").type(STRING) - .description("픽픽픽 답글 작성자 이메일"), + fieldWithPath("data.content[].replies[].maskedEmail").optional().description("픽픽픽 답글 작성자 이메일"), fieldWithPath("data.content[].replies[].contents").type(STRING).description("픽픽픽 답글 내용"), fieldWithPath("data.content[].replies[].recommendTotalCount").type(NUMBER) .description("픽픽픽 답글 좋아요 총 갯수"), @@ -751,8 +771,10 @@ void getPickComments(PickCommentSort pickCommentSort) throws Exception { .description("픽픽픽 답글 삭제 여부"), fieldWithPath("data.content[].replies[].isModified").type(BOOLEAN) .description("픽픽픽 답글 수정 여부"), - fieldWithPath("data.content[].replies[].pickParentCommentMemberId").type(NUMBER) + fieldWithPath("data.content[].replies[].pickParentCommentMemberId").optional() .description("픽픽픽 부모 댓글 작성자 아이디"), + fieldWithPath("data.content[].replies[].pickParentCommentAnonymousMemberId").optional() + .description("픽픽픽 부모 댓글 익명 작성자 아이디"), fieldWithPath("data.content[].replies[].pickParentCommentAuthor").type(STRING) .description("픽픽픽 부모 댓글 작성자 닉네임"), @@ -918,6 +940,10 @@ void findPickBestComments() throws Exception { Member member7 = Member.createMemberBy(socialMemberDto7); memberRepository.saveAll(List.of(member1, member2, member3, member4, member5, member6, member7)); + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명의 댑댑이 123"); + anonymousMemberRepository.save(anonymousMember); + // 픽픽픽 생성 Pick pick = createPick(new Title("무엇이 정답일까요?"), ContentStatus.APPROVAL, new Count(6), member1); pickRepository.save(pick); @@ -938,7 +964,7 @@ void findPickBestComments() throws Exception { // 픽픽픽 최초 댓글 생성 PickComment originParentPickComment1 = createPickComment(new CommentContents("여기가 꿈파?"), true, new Count(2), - new Count(3), member1, pick, member1PickVote); + new Count(3), anonymousMember, pick, member1PickVote); originParentPickComment1.modifyCommentContents(new CommentContents("행복한~"), LocalDateTime.now()); PickComment originParentPickComment2 = createPickComment(new CommentContents("꿈빛!"), true, new Count(1), new Count(2), member2, pick, member2PickVote); @@ -955,11 +981,11 @@ void findPickBestComments() throws Exception { originParentPickComment3, originParentPickComment2, originParentPickComment1)); // 픽픽픽 답글 생성 - PickComment pickReply1 = createReplidPickComment(new CommentContents("진짜 너무 좋아"), member1, pick, + PickComment pickReply1 = createReplidPickComment(new CommentContents("진짜 너무 좋아"), anonymousMember, pick, originParentPickComment1, originParentPickComment1); PickComment pickReply2 = createReplidPickComment(new CommentContents("너무 행복하다"), member6, pick, originParentPickComment1, pickReply1); - pickReply2.changeDeletedAt(LocalDateTime.now(), member1); + pickReply2.changeDeletedAtByMember(LocalDateTime.now(), member1); PickComment pickReply3 = createReplidPickComment(new CommentContents("사랑해요~"), member6, pick, originParentPickComment2, originParentPickComment2); pickCommentRepository.saveAll(List.of(pickReply1, pickReply2, pickReply3)); @@ -986,7 +1012,8 @@ void findPickBestComments() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명 회원 아이디") ), pathParameters( parameterWithName("pickId").description("픽픽픽 아이디") @@ -1000,7 +1027,8 @@ void findPickBestComments() throws Exception { fieldWithPath("datas.[].pickCommentId").type(NUMBER).description("픽픽픽 댓글 아이디"), fieldWithPath("datas.[].createdAt").type(STRING).description("픽픽픽 댓글 작성일시"), - fieldWithPath("datas.[].memberId").type(NUMBER).description("픽픽픽 댓글 작성자 아이디"), + fieldWithPath("datas.[].memberId").optional().description("픽픽픽 댓글 작성자 아이디"), + fieldWithPath("datas.[].anonymousMemberId").optional().description("픽픽픽 댓글 익명 작성자 아이디"), fieldWithPath("datas.[].author").type(STRING).description("픽픽픽 댓글 작성자 닉네임"), fieldWithPath("datas.[].isCommentOfPickAuthor").type(BOOLEAN) .description("댓글 작성자가 픽픽픽 작성자인지 여부"), @@ -1008,7 +1036,7 @@ void findPickBestComments() throws Exception { .description("로그인한 회원이 댓글 작성자인지 여부"), fieldWithPath("datas.[].isRecommended").type(BOOLEAN) .description("로그인한 회원이 댓글 추천 여부"), - fieldWithPath("datas.[].maskedEmail").type(STRING).description("픽픽픽 댓글 작성자 이메일"), + fieldWithPath("datas.[].maskedEmail").optional().type(STRING).description("픽픽픽 댓글 작성자 이메일"), fieldWithPath("datas.[].votedPickOption").optional().type(STRING) .description("픽픽픽 투표 선택 타입").attributes(pickOptionType()), fieldWithPath("datas.[].votedPickOptionTitle").optional().type(STRING) @@ -1025,7 +1053,8 @@ void findPickBestComments() throws Exception { fieldWithPath("datas.[].replies").type(ARRAY).description("픽픽픽 답글 배열"), fieldWithPath("datas.[].replies[].pickCommentId").type(NUMBER).description("픽픽픽 답글 아이디"), - fieldWithPath("datas.[].replies[].memberId").type(NUMBER).description("픽픽픽 답글 작성자 아이디"), + fieldWithPath("datas.[].replies[].memberId").optional().description("픽픽픽 답글 작성자 아이디"), + fieldWithPath("datas.[].replies[].anonymousMemberId").optional().description("픽픽픽 답글 익명 작성자 아이디"), fieldWithPath("datas.[].replies[].pickParentCommentId").type(NUMBER) .description("픽픽픽 답글의 부모 댓글 아이디"), fieldWithPath("datas.[].replies[].pickOriginParentCommentId").type(NUMBER) @@ -1038,7 +1067,7 @@ void findPickBestComments() throws Exception { fieldWithPath("datas.[].replies[].isRecommended").type(BOOLEAN) .description("로그인한 회원이 답글 추천 여부"), fieldWithPath("datas.[].replies[].author").type(STRING).description("픽픽픽 답글 작성자 닉네임"), - fieldWithPath("datas.[].replies[].maskedEmail").type(STRING) + fieldWithPath("datas.[].replies[].maskedEmail").optional().type(STRING) .description("픽픽픽 답글 작성자 이메일"), fieldWithPath("datas.[].replies[].contents").type(STRING).description("픽픽픽 답글 내용"), fieldWithPath("datas.[].replies[].recommendTotalCount").type(NUMBER) @@ -1047,8 +1076,9 @@ void findPickBestComments() throws Exception { .description("픽픽픽 답글 삭제 여부"), fieldWithPath("datas.[].replies[].isModified").type(BOOLEAN) .description("픽픽픽 답글 수정 여부"), - fieldWithPath("datas.[].replies[].pickParentCommentMemberId").type(NUMBER) - .description("픽픽픽 부모 댓글 작성자 아이디"), + fieldWithPath("datas.[].replies[].pickParentCommentMemberId").optional().description("픽픽픽 부모 댓글 작성자 아이디"), + fieldWithPath("datas.[].replies[].pickParentCommentAnonymousMemberId").optional() + .description("픽픽픽 부모 댓글 익명 작성자 아이디"), fieldWithPath("datas.[].replies[].pickParentCommentAuthor").type(STRING) .description("픽픽픽 부모 댓글 작성자 닉네임") ) @@ -1119,6 +1149,24 @@ private PickComment createPickComment(CommentContents contents, Boolean isPublic return pickComment; } + private PickComment createPickComment(CommentContents contents, Boolean isPublic, Count replyTotalCount, + Count recommendTotalCount, AnonymousMember anonymousMember, Pick pick, + PickVote pickVote) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .isPublic(isPublic) + .createdAnonymousBy(anonymousMember) + .replyTotalCount(replyTotalCount) + .recommendTotalCount(recommendTotalCount) + .pick(pick) + .pickVote(pickVote) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + private PickComment createReplidPickComment(CommentContents contents, Member member, Pick pick, PickComment originParent, PickComment parent) { PickComment pickComment = PickComment.builder() @@ -1137,6 +1185,24 @@ private PickComment createReplidPickComment(CommentContents contents, Member mem return pickComment; } + private PickComment createReplidPickComment(CommentContents contents, AnonymousMember anonymousMember, Pick pick, + PickComment originParent, PickComment parent) { + PickComment pickComment = PickComment.builder() + .contents(contents) + .createdAnonymousBy(anonymousMember) + .pick(pick) + .originParent(originParent) + .isPublic(false) + .parent(parent) + .recommendTotalCount(new Count(0)) + .replyTotalCount(new Count(0)) + .build(); + + pickComment.changePick(pick); + + return pickComment; + } + private PickComment createReplidPickComment(CommentContents contents, Boolean isPublic, Member member, Pick pick, PickComment originParent, PickComment parent) { PickComment pickComment = PickComment.builder() 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 afa5ff38..0f48c41c 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/TechArticleCommentControllerDocsTest.java @@ -2,7 +2,13 @@ import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; import static com.dreamypatisiel.devdevdev.domain.exception.TechArticleExceptionMessage.NOT_FOUND_TECH_ARTICLE_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createCompany; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createMainTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createRepliedTechComment; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createSocialDto; +import static com.dreamypatisiel.devdevdev.domain.service.techArticle.TechTestUtils.createTechCommentRecommend; import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; +import static com.dreamypatisiel.devdevdev.web.WebConstant.HEADER_ANONYMOUS_MEMBER_ID; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.techCommentSortType; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; @@ -30,19 +36,18 @@ 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.AnonymousMember; 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.TechComment; import com.dreamypatisiel.devdevdev.domain.entity.TechCommentRecommend; import com.dreamypatisiel.devdevdev.domain.entity.embedded.CommentContents; -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.AnonymousMemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.member.MemberRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechArticleRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentRecommendRepository; @@ -50,14 +55,15 @@ import com.dreamypatisiel.devdevdev.domain.repository.techArticle.TechCommentSort; 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; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.ModifyTechCommentRequest; import com.dreamypatisiel.devdevdev.web.dto.request.techArticle.RegisterTechCommentRequest; 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; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -68,10 +74,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.security.core.Authentication; -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.web.servlet.ResultActions; public class TechArticleCommentControllerDocsTest extends SupportControllerDocsTest { @@ -91,9 +93,13 @@ public class TechArticleCommentControllerDocsTest extends SupportControllerDocsT @Autowired TechCommentRecommendRepository techCommentRecommendRepository; + @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired EntityManager em; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -102,9 +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(); @@ -138,18 +142,14 @@ void registerTechCommentByAnonymous() throws Exception { } @Test - @DisplayName("회원은 기술블로그 댓글을 작성할 수 있다.") + @DisplayName("기술블로그 댓글을 작성할 수 있다.") void registerTechComment() throws Exception { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "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(); @@ -180,7 +180,8 @@ void registerTechComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디") @@ -204,11 +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; @@ -256,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(); @@ -304,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(); @@ -353,13 +345,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -385,7 +375,8 @@ void modifyTechComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디"), @@ -417,13 +408,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -473,9 +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(); @@ -524,13 +511,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); Long techCommentId = techComment.getId(); @@ -553,7 +538,8 @@ void deleteTechComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디"), @@ -581,13 +567,11 @@ 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(); - TechComment techComment = TechComment.createMainTechComment(new CommentContents("댓글입니다"), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다"), member, techArticle); techCommentRepository.save(techComment); // when // then @@ -619,16 +603,14 @@ void deleteTechCommentNotFoundException() throws Exception { } @Test - @DisplayName("회원은 기술블로그 댓글에 답글을 작성할 수 있다.") + @DisplayName("기술블로그 댓글에 답글을 작성할 수 있다.") void registerTechReply() throws Exception { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "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(); @@ -638,12 +620,12 @@ void registerTechReply() throws Exception { member.updateRefreshToken(refreshToken); memberRepository.save(member); - TechComment originParentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(originParentTechComment); Long originParentTechCommentId = originParentTechComment.getId(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("답글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("답글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); @@ -670,7 +652,8 @@ void registerTechReply() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디"), @@ -703,18 +686,16 @@ 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(); - TechComment originParentTechComment = TechComment.createMainTechComment(new CommentContents("댓글입니다."), member, + TechComment originParentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(originParentTechComment); Long originParentTechCommentId = originParentTechComment.getId(); - TechComment parentTechComment = TechComment.createMainTechComment(new CommentContents("답글입니다."), member, + TechComment parentTechComment = TechComment.createMainTechCommentByMember(new CommentContents("답글입니다."), member, techArticle); techCommentRepository.save(parentTechComment); Long parentTechCommentId = parentTechComment.getId(); @@ -754,30 +735,25 @@ void registerTechReplyContentsIsNullException(String contents) throws Exception @DisplayName("기술블로그 댓글/답글을 정렬 조건에 따라서 조회한다.") void getTechComments() throws Exception { // given - SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", - "꿈빛파티시엘", "1234", email, socialType, role); + SocialMemberDto socialMemberDto = createSocialDto("dreamy5patisiel", "꿈빛파티시엘", "꿈빛파티시엘", "1234", 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(); + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "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(); TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member, techArticle, new Count(0L), new Count(0L), new Count(0L)); - TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), member, + TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글2"), anonymousMember, techArticle, new Count(0L), new Count(0L), new Count(0L)); TechComment originParentTechComment3 = createMainTechComment(new CommentContents("최상위 댓글3"), member, techArticle, new Count(0L), new Count(0L), new Count(0L)); @@ -785,29 +761,21 @@ void getTechComments() throws Exception { techArticle, new Count(0L), new Count(0L), new Count(0L)); TechComment originParentTechComment5 = createMainTechComment(new CommentContents("최상위 댓글5"), member, techArticle, new Count(0L), new Count(0L), new Count(0L)); - TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), member, + TechComment originParentTechComment6 = createMainTechComment(new CommentContents("최상위 댓글6"), anonymousMember, techArticle, new Count(0L), new Count(0L), new Count(0L)); TechComment parentTechComment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1"), member, - techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), - new Count(0L)); - TechComment parentTechComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2"), member, - techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), - new Count(0L)); + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); + TechComment parentTechComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2"), anonymousMember, + techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), new Count(0L), new Count(0L)); TechComment parentTechComment3 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글1"), member, - techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), - new Count(0L)); + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); TechComment parentTechComment4 = createRepliedTechComment(new CommentContents("최상위 댓글2의 답글2"), member, - techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), - new Count(0L)); + techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), new Count(0L), new Count(0L)); TechComment techComment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1의 답글"), member, techArticle, originParentTechComment1, parentTechComment1, new Count(0L), new Count(0L), new Count(0L)); - TechComment techComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2의 답글"), member, - techArticle, originParentTechComment1, parentTechComment2, new Count(0L), new Count(0L), new Count(0L)); - TechComment techcomment1 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글1의 답글"), member, - techArticle, originParentTechComment1, parentTechComment1, new Count(0L), new Count(0L), new Count(0L)); - TechComment techcomment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2의 답글"), member, + TechComment techComment2 = createRepliedTechComment(new CommentContents("최상위 댓글1의 답글2의 답글"), anonymousMember, techArticle, originParentTechComment1, parentTechComment2, new Count(0L), new Count(0L), new Count(0L)); techCommentRepository.saveAll(List.of( @@ -837,60 +805,15 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), .header(AUTHORIZATION_HEADER, SecurityConstant.BEARER_PREFIX + accessToken) .characterEncoding(StandardCharsets.UTF_8)) .andDo(print()) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.resultType").value(ResultType.SUCCESS.name())) - .andExpect(jsonPath("$.data").isNotEmpty()) - .andExpect(jsonPath("$.data.content").isArray()) - .andExpect(jsonPath("$.data.content.[0].techCommentId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].createdAt").isString()) - .andExpect(jsonPath("$.data.content.[0].memberId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].author").isString()) - .andExpect(jsonPath("$.data.content.[0].maskedEmail").isString()) - .andExpect(jsonPath("$.data.content.[0].contents").isString()) - .andExpect(jsonPath("$.data.content.[0].replyTotalCount").isNumber()) - .andExpect(jsonPath("$.data.content.[0].recommendTotalCount").isNumber()) - .andExpect(jsonPath("$.data.content.[0].isDeleted").isBoolean()) - .andExpect(jsonPath("$.data.content.[0].isRecommended").isBoolean()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].techCommentId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].memberId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].techParentCommentId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].techOriginParentCommentId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].createdAt").isString()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].author").isString()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].maskedEmail").isString()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].contents").isString()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].techParentCommentMemberId").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].techParentCommentAuthor").isString()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].recommendTotalCount").isNumber()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].isDeleted").isBoolean()) - .andExpect(jsonPath("$.data.content.[0].replies.[0].isRecommended").isBoolean()) - .andExpect(jsonPath("$.data.pageable").isNotEmpty()) - .andExpect(jsonPath("$.data.pageable.pageNumber").isNumber()) - .andExpect(jsonPath("$.data.pageable.pageSize").isNumber()) - .andExpect(jsonPath("$.data.pageable.sort").isNotEmpty()) - .andExpect(jsonPath("$.data.pageable.sort.empty").isBoolean()) - .andExpect(jsonPath("$.data.pageable.sort.sorted").isBoolean()) - .andExpect(jsonPath("$.data.pageable.sort.unsorted").isBoolean()) - .andExpect(jsonPath("$.data.pageable.offset").isNumber()) - .andExpect(jsonPath("$.data.pageable.paged").isBoolean()) - .andExpect(jsonPath("$.data.pageable.unpaged").isBoolean()) - .andExpect(jsonPath("$.data.first").isBoolean()) - .andExpect(jsonPath("$.data.last").isBoolean()) - .andExpect(jsonPath("$.data.size").isNumber()) - .andExpect(jsonPath("$.data.number").isNumber()) - .andExpect(jsonPath("$.data.sort").isNotEmpty()) - .andExpect(jsonPath("$.data.sort.empty").isBoolean()) - .andExpect(jsonPath("$.data.sort.sorted").isBoolean()) - .andExpect(jsonPath("$.data.sort.unsorted").isBoolean()) - .andExpect(jsonPath("$.data.numberOfElements").isNumber()) - .andExpect(jsonPath("$.data.empty").isBoolean()); + .andExpect(status().isOk()); // docs actions.andDo(document("get-tech-comments", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디") @@ -908,9 +831,11 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), fieldWithPath("data.content").type(ARRAY).description("기술블로그 댓글/답글 메인 배열"), fieldWithPath("data.content[].techCommentId").type(NUMBER).description("기술블로그 댓글 아이디"), fieldWithPath("data.content[].createdAt").type(STRING).description("기술블로그 댓글 작성일시"), - fieldWithPath("data.content[].memberId").type(NUMBER).description("기술블로그 댓글 작성자 아이디"), + fieldWithPath("data.content[].memberId").optional().type(NUMBER).description("기술블로그 댓글 작성자 아이디"), + fieldWithPath("data.content[].anonymousMemberId").optional().type(NUMBER) + .description("기술블로그 댓글 익명 작성자 아이디"), fieldWithPath("data.content[].author").type(STRING).description("기술블로그 댓글 작성자 닉네임"), - fieldWithPath("data.content[].maskedEmail").type(STRING).description("기술블로그 댓글 작성자 이메일"), + fieldWithPath("data.content[].maskedEmail").type(STRING).optional().description("기술블로그 댓글 작성자 이메일"), fieldWithPath("data.content[].contents").type(STRING).description("기술블로그 댓글 내용"), fieldWithPath("data.content[].isCommentAuthor").type(BOOLEAN) .description("회원의 기술블로그 댓글 작성자 여부"), @@ -929,19 +854,23 @@ techArticle, originParentTechComment2, originParentTechComment2, new Count(0L), fieldWithPath("data.content[].replies[].techCommentId").type(NUMBER) .description("기술블로그 답글 아이디"), fieldWithPath("data.content[].replies[].memberId").type(NUMBER).description("기술블로그 답글 작성자 아이디"), + fieldWithPath("data.content[].replies[].anonymousMemberId").optional().type(NUMBER) + .description("기술블로그 답글 익명 작성자 아이디"), fieldWithPath("data.content[].replies[].techParentCommentId").type(NUMBER) .description("기술블로그 답글의 부모 댓글 아이디"), fieldWithPath("data.content[].replies[].techOriginParentCommentId").type(NUMBER) .description("기술블로그 답글의 최상위 부모 댓글 아이디"), fieldWithPath("data.content[].replies[].createdAt").type(STRING).description("기술블로그 답글 작성일시"), - fieldWithPath("data.content[].replies[].techParentCommentMemberId").type(NUMBER) + fieldWithPath("data.content[].replies[].techParentCommentMemberId").optional().type(NUMBER) .description("기술블로그 답글의 부모 댓글 작성자 아이디"), + fieldWithPath("data.content[].replies[].techParentCommentAnonymousMemberId").optional().type(NUMBER) + .description("기술블로그 답글의 부모 댓글 익명 작성자 아이디"), fieldWithPath("data.content[].replies[].techParentCommentAuthor").type(STRING) .description("기술블로그 답글의 부모 댓글 작성자 닉네임"), fieldWithPath("data.content[].replies[].author").type(STRING).description("기술블로그 답글 작성자 닉네임"), fieldWithPath("data.content[].replies[].isCommentAuthor").type(BOOLEAN) .description("회원의 기술블로그 답글 작성자 여부"), - fieldWithPath("data.content[].replies[].maskedEmail").type(STRING) + fieldWithPath("data.content[].replies[].maskedEmail").type(STRING).optional() .description("기술블로그 답글 작성자 이메일"), fieldWithPath("data.content[].replies[].contents").type(STRING).description("기술블로그 답글 내용"), fieldWithPath("data.content[].replies[].recommendTotalCount").type(NUMBER) @@ -997,12 +926,10 @@ 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.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); // when // then @@ -1024,7 +951,8 @@ void recommendTechComment() throws Exception { preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디"), @@ -1055,12 +983,10 @@ 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.createMainTechComment(new CommentContents("댓글입니다."), member, techArticle); + TechComment techComment = TechComment.createMainTechCommentByMember(new CommentContents("댓글입니다."), member, techArticle); techCommentRepository.save(techComment); // when // then @@ -1109,19 +1035,21 @@ void getTechBestComments() throws Exception { Member member3 = Member.createMemberBy(socialMemberDto3); memberRepository.saveAll(List.of(member1, member2, member3)); + // 익명회원 생성 + AnonymousMember anonymousMember = AnonymousMember.create("anonymousMemberId", "익명으로 개발하는 댑댑이"); + anonymousMemberRepository.save(anonymousMember); + // 회사 생성 Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", "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); // 댓글 생성 - TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), member1, + TechComment originParentTechComment1 = createMainTechComment(new CommentContents("최상위 댓글1"), anonymousMember, techArticle, new Count(0L), new Count(3L), new Count(0L)); originParentTechComment1.modifyCommentContents(new CommentContents("최상위 댓글1 수정"), LocalDateTime.now()); TechComment originParentTechComment2 = createMainTechComment(new CommentContents("최상위 댓글1"), member2, @@ -1156,7 +1084,8 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestHeaders( - headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰") + headerWithName(AUTHORIZATION_HEADER).optional().description("Bearer 엑세스 토큰"), + headerWithName(HEADER_ANONYMOUS_MEMBER_ID).optional().description("익명회원 아이디") ), pathParameters( parameterWithName("techArticleId").description("기술블로그 아이디") @@ -1170,26 +1099,25 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), fieldWithPath("datas.[].techCommentId").type(NUMBER).description("기술블로그 댓글 아이디"), fieldWithPath("datas.[].createdAt").type(STRING).description("기술블로그 댓글 작성일시"), - fieldWithPath("datas.[].memberId").type(NUMBER).description("기술블로그 댓글 작성자 아이디"), + fieldWithPath("datas.[].memberId").type(NUMBER).optional().description("기술블로그 댓글 작성자 아이디"), + fieldWithPath("datas.[].anonymousMemberId").type(NUMBER).optional().description("기술블로그 댓글 익명 작성자 아이디"), fieldWithPath("datas.[].author").type(STRING).description("기술블로그 댓글 작성자 닉네임"), fieldWithPath("datas.[].isCommentAuthor").type(BOOLEAN) .description("로그인한 회원이 댓글 작성자인지 여부"), fieldWithPath("datas.[].isRecommended").type(BOOLEAN) .description("로그인한 회원이 댓글 추천 여부"), - fieldWithPath("datas.[].maskedEmail").type(STRING).description("기술블로그 댓글 작성자 이메일"), + fieldWithPath("datas.[].maskedEmail").optional().type(STRING).description("기술블로그 댓글 작성자 이메일"), fieldWithPath("datas.[].contents").type(STRING).description("기술블로그 댓글 내용"), - fieldWithPath("datas.[].replyTotalCount").type(NUMBER) - .description("기술블로그 댓글의 답글 총 갯수"), - fieldWithPath("datas.[].recommendTotalCount").type(NUMBER) - .description("기술블로그 댓글 좋아요 총 갯수"), - fieldWithPath("datas.[].isDeleted").type(BOOLEAN) - .description("기술블로그 댓글 삭제 여부"), - fieldWithPath("datas.[].isModified").type(BOOLEAN) - .description("기술블로그 댓글 수정 여부"), + fieldWithPath("datas.[].replyTotalCount").type(NUMBER).description("기술블로그 댓글의 답글 총 갯수"), + fieldWithPath("datas.[].recommendTotalCount").type(NUMBER).description("기술블로그 댓글 좋아요 총 갯수"), + fieldWithPath("datas.[].isDeleted").type(BOOLEAN).description("기술블로그 댓글 삭제 여부"), + fieldWithPath("datas.[].isModified").type(BOOLEAN).description("기술블로그 댓글 수정 여부"), fieldWithPath("datas.[].replies").type(ARRAY).description("기술블로그 답글 배열"), fieldWithPath("datas.[].replies[].techCommentId").type(NUMBER).description("기술블로그 답글 아이디"), - fieldWithPath("datas.[].replies[].memberId").type(NUMBER).description("기술블로그 답글 작성자 아이디"), + fieldWithPath("datas.[].replies[].memberId").optional().type(NUMBER).description("기술블로그 답글 작성자 아이디"), + fieldWithPath("datas.[].replies[].anonymousMemberId").optional().type(NUMBER) + .description("기술블로그 답글 익명 작성자 아이디"), fieldWithPath("datas.[].replies[].techParentCommentId").type(NUMBER) .description("기술블로그 답글의 부모 댓글 아이디"), fieldWithPath("datas.[].replies[].techOriginParentCommentId").type(NUMBER) @@ -1209,78 +1137,29 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), .description("기술블로그 답글 삭제 여부"), fieldWithPath("datas.[].replies[].isModified").type(BOOLEAN) .description("기술블로그 답글 수정 여부"), - fieldWithPath("datas.[].replies[].techParentCommentMemberId").type(NUMBER) - .description("기술블로그 부모 댓글 작성자 아이디"), + fieldWithPath("datas.[].replies[].techParentCommentMemberId").optional().type(NUMBER).description( + "기술블로그 부모 댓글 작성자 아이디"), + fieldWithPath("datas.[].replies[].techParentCommentAnonymousMemberId").optional().type(NUMBER) + .description("기술블로그 부모 댓글 익명 작성자 아이디"), fieldWithPath("datas.[].replies[].techParentCommentAuthor").type(STRING) .description("기술블로그 부모 댓글 작성자 닉네임") ) )); } - private TechCommentRecommend createTechCommentRecommend(Boolean recommendedStatus, TechComment techComment, - Member member) { - TechCommentRecommend techCommentRecommend = TechCommentRecommend.builder() - .recommendedStatus(recommendedStatus) - .techComment(techComment) - .member(member) - .build(); - - techCommentRecommend.changeTechComment(techComment); - - return techCommentRecommend; - } - - 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 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 TechComment createMainTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .build(); - } - - private static TechComment createRepliedTechComment(CommentContents contents, Member createdBy, - TechArticle techArticle, - TechComment originParent, TechComment parent, - Count blameTotalCount, Count recommendTotalCount, - Count replyTotalCount) { - return TechComment.builder() - .contents(contents) - .createdBy(createdBy) - .techArticle(techArticle) - .blameTotalCount(blameTotalCount) - .recommendTotalCount(recommendTotalCount) - .replyTotalCount(replyTotalCount) - .originParent(originParent) - .parent(parent) + 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..dd806847 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.0 + ); + + 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, Double 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