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/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java index a3dc2f7e..97d552cb 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/LocalInitData.java @@ -208,15 +208,12 @@ private List createBookmarks(Member member, List techArti private List createTechArticles(Map companyIdMap) { 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); + if (company != null) { + TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); + techArticles.add(techArticle); } - TechArticle techArticle = TechArticle.createTechArticle(elasticTechArticle, company); - techArticles.add(techArticle); } return techArticles; } 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/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/PickRepository.java b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickRepository.java index f7c56f9a..1ff6838b 100644 --- a/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickRepository.java +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/pick/PickRepository.java @@ -15,4 +15,6 @@ public interface PickRepository extends JpaRepository, PickRepositor Optional findPickWithPickOptionByIdAndMember(Long id, Member member); List findTop1000ByContentStatusAndEmbeddingsIsNotNullOrderByCreatedAtDesc(ContentStatus contentStatus); + + Long countByMember(Member member); } 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/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/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..46d782a5 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/domain/repository/techArticle/custom/TechKeywordRepositoryImpl.java @@ -0,0 +1,56 @@ +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"; + 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 + NumberTemplate jamoScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_FUNCTION + "', {0}, {1})", + techKeyword.jamoKey, inputJamo + ); + NumberTemplate chosungScore = Expressions.numberTemplate(Double.class, + "function('" + MATCH_AGAINST_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 5aca6bbe..d0962c3d 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 @@ -1,7 +1,17 @@ package com.dreamypatisiel.devdevdev.domain.service.member; -import com.dreamypatisiel.devdevdev.domain.entity.*; +import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; + +import com.dreamypatisiel.devdevdev.domain.entity.Company; +import com.dreamypatisiel.devdevdev.domain.entity.Member; +import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyAnswer; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyQuestion; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyQuestionOption; +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; @@ -14,6 +24,7 @@ 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; @@ -29,6 +40,11 @@ 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; @@ -44,6 +60,9 @@ import java.util.stream.Stream; import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.NicknameExceptionMessage.NICKNAME_CHANGE_RATE_LIMIT_MESSAGE; + +import org.springframework.beans.factory.annotation.Value; @Service @RequiredArgsConstructor @@ -61,6 +80,7 @@ public class MemberService { private final SurveyAnswerJdbcTemplateRepository surveyAnswerJdbcTemplateRepository; private final CommentRepository commentRepository; private final CompanyRepository companyRepository; + private final NicknameChangePolicy nicknameChangePolicy; /** * 회원 탈퇴 회원의 북마크와 회원 정보를 삭제합니다. @@ -101,12 +121,15 @@ public Slice findMyPickMain(Pageable pageable, Long pickId, // 회원이 작성한 픽픽픽 조회 Slice findPicks = pickRepository.findPicksByMemberAndCursor(pageable, findMember, pickId); + // 전체 갯수 + Long totalElements = pickRepository.countByMember(findMember); + // 데이터 가공 List myPickMainsResponse = findPicks.stream() .map(MyPickMainResponse::from) .toList(); - return new SliceImpl<>(myPickMainsResponse, pageable, findPicks.hasNext()); + return new SliceCustom<>(myPickMainsResponse, pageable, totalElements); } /** @@ -286,4 +309,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/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/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/global/config/CustomMySQLFunctionContributor.java b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java new file mode 100644 index 00000000..1328d9a2 --- /dev/null +++ b/src/main/java/com/dreamypatisiel/devdevdev/global/config/CustomMySQLFunctionContributor.java @@ -0,0 +1,18 @@ +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)"; + + @Override + public void contributeFunctions(FunctionContributions functionContributions) { + functionContributions.getFunctionRegistry() + .registerPattern(MATCH_AGAINST_FUNCTION, MATCH_AGAINST_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..50402c69 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") @@ -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/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/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 1f004d1b..82718e14 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/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/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..1df770b8 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; @@ -33,8 +34,10 @@ 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; @@ -51,14 +54,16 @@ import com.dreamypatisiel.devdevdev.web.dto.response.techArticle.TechArticleMainResponse; import jakarta.persistence.EntityManager; 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; @@ -116,6 +121,8 @@ class MemberServiceTest extends ElasticsearchSupportTest { PickCommentRepository pickCommentRepository; @Autowired SubscriptionRepository subscriptionRepository; + @MockBean + TimeProvider timeProvider; @Test @DisplayName("회원이 회원탈퇴 설문조사를 완료하지 않으면 탈퇴가 불가능하다.") @@ -455,6 +462,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 +1186,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() 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/GuestTechCommentServiceTest.java b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/GuestTechCommentServiceTest.java index eade4b29..ac0d746c 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,30 +1,40 @@ 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; @@ -33,13 +43,9 @@ import jakarta.persistence.EntityManager; 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; @@ -108,10 +114,11 @@ void registerTechComment() { Long id = savedTechArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when // then assertThatThrownBy(() -> guestTechCommentService.registerMainTechComment( - id, registerTechCommentRequest, authentication)) + id, registerCommentDto, authentication)) .isInstanceOf(AccessDeniedException.class) .hasMessage(INVALID_ANONYMOUS_CAN_NOT_USE_THIS_FUNCTION_MESSAGE); } @@ -131,21 +138,21 @@ void registerRepliedTechComment() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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); } @@ -170,7 +177,7 @@ void recommendTechComment() { 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 @@ -246,7 +253,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); @@ -540,7 +547,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); @@ -709,7 +716,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); @@ -983,7 +990,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); @@ -1132,7 +1139,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 +1256,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); } @@ -1306,7 +1313,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 @@ -1399,71 +1406,4 @@ 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)) - .build(); - } } 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..f47eb2c2 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; @@ -130,10 +135,11 @@ void registerTechComment() { Long id = savedTechArticle.getId(); RegisterTechCommentRequest registerTechCommentRequest = new RegisterTechCommentRequest("댓글입니다."); + TechCommentDto registerCommentDto = TechCommentDto.createRegisterCommentDto(registerTechCommentRequest, null); // when TechCommentResponse techCommentResponse = memberTechCommentService.registerMainTechComment( - id, registerTechCommentRequest, authentication); + id, registerCommentDto, authentication); em.flush(); // then @@ -180,10 +186,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(id, registerCommentDto, authentication)) .isInstanceOf(NotFoundException.class) .hasMessage(NOT_FOUND_TECH_ARTICLE_MESSAGE); } @@ -200,10 +207,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); } @@ -227,23 +235,23 @@ void modifyTechComment() { companyRepository.save(company); TechArticle techArticle = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), - new Count(1L), - new Count(1L), new Count(1L), new Count(1L), null, company); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +285,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); @@ -311,10 +320,11 @@ void modifyTechCommentNotFoundTechArticleCommentException() { 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); @@ -340,12 +350,11 @@ void modifyTechCommentAlreadyDeletedException() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +363,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); } @@ -382,12 +391,11 @@ void deleteTechComment() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +405,7 @@ void deleteTechComment() { em.flush(); // when - memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication); + memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication); // then TechComment findTechComment = techCommentRepository.findById(techCommentId).get(); @@ -429,12 +437,11 @@ void deleteTechCommentAlreadyDeletedException() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +451,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); } @@ -469,14 +476,13 @@ void deleteTechCommentNotFoundException() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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); } @@ -505,12 +511,11 @@ void deleteTechCommentAdmin() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +525,7 @@ void deleteTechCommentAdmin() { em.flush(); // when - memberTechCommentService.deleteTechComment(techArticleId, techCommentId, authentication); + memberTechCommentService.deleteTechComment(techArticleId, techCommentId, null, authentication); // then TechComment findTechComment = techCommentRepository.findById(techCommentId).get(); @@ -556,18 +561,17 @@ void deleteTechCommentNotByMemberException() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +590,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); } @@ -611,21 +614,21 @@ void registerRepliedTechComment() { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 @@ -676,21 +679,22 @@ void registerRepliedTechCommentToRepliedTechComment() { 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(); @@ -741,16 +745,17 @@ void registerRepliedTechCommentNotFoundTechCommentException() { 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); } @@ -779,7 +784,7 @@ void registerRepliedTechCommentDeletedTechCommentException() { 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 +795,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 +822,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); } @@ -894,7 +901,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); @@ -1191,7 +1198,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); @@ -1363,7 +1370,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); @@ -1640,7 +1647,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); @@ -1793,7 +1800,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); // 삭제된 댓글은 카운트하지 않는다 @@ -1919,7 +1926,7 @@ void recommendTechComment() { 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 @@ -1957,7 +1964,7 @@ void recommendTechCommentCancel() { 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); @@ -1998,7 +2005,7 @@ void recommendTechCommentNotFoundTechCommentException() { 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; @@ -2033,7 +2040,7 @@ void recommendTechCommentDeletedTechCommentException() { 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 +2091,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); } @@ -2145,7 +2152,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 @@ -2296,7 +2303,7 @@ techArticle, originParentTechComment1, originParentTechComment1, new Count(0L), // when List response = memberTechCommentService.findTechBestComments(3, techArticle.getId(), - authentication); + null, authentication); // then assertThat(response).hasSize(1) @@ -2364,71 +2371,4 @@ 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)) - .build(); - } } 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..45f05d0d --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/TechKeywordServiceTest.java @@ -0,0 +1,182 @@ +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 com.dreamypatisiel.devdevdev.test.MySQLTestContainer; +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.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +class TechKeywordServiceTest extends MySQLTestContainer { + + @Autowired + EntityManager em; + + @Autowired + TechKeywordService techKeywordService; + + @Autowired + TechKeywordRepository techKeywordRepository; + + @Autowired + DataSource dataSource; + + 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 = dataSource.getConnection(); + Statement statement = connection.createStatement(); + + connection.setAutoCommit(false); // 트랜잭션 시작 + + 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(); // 트랜잭션 커밋 + } + + @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..c345159d --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/domain/service/techArticle/techComment/GuestTechCommentServiceV2Test.java @@ -0,0 +1,2048 @@ +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.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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), + new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), + new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(2L), new Count(1L), null, 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( + 3L) + ); + } + + @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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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); + } +} \ 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..03b9e4f0 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 @@ -8,6 +8,7 @@ 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; @@ -15,6 +16,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +@Disabled @SpringBootTest class ElasticKeywordServiceTest { 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..6884b394 --- /dev/null +++ b/src/test/java/com/dreamypatisiel/devdevdev/test/MySQLTestContainer.java @@ -0,0 +1,39 @@ +package com.dreamypatisiel.devdevdev.test; + +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; + +/** + * MySQL 테스트컨테이너를 제공하는 공통 클래스 + * 1. 테스트 클래스에서 이 클래스를 상속받거나 + * 2. @ExtendWith(MySQLTestContainer.class) 어노테이션을 사용 + */ +@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) { + registry.add("spring.datasource.url", mysql::getJdbcUrl); + 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"); + } +} 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 99% 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..8073c50e 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.*; @@ -23,6 +23,7 @@ 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; 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/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..8f61b87d 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,27 @@ 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.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 +57,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 +74,10 @@ class TechArticleCommentControllerTest extends SupportControllerTest { TimeProvider timeProvider; @Autowired EntityManager em; + @Autowired + TechArticleServiceStrategy techArticleServiceStrategy; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -80,10 +87,7 @@ void registerTechCommentByAnonymous() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -103,7 +107,7 @@ 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"); @@ -139,6 +143,41 @@ 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 = TechArticle.createTechArticle(new Title("기술블로그 제목"), new Url("https://example.com"), + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 { @@ -148,10 +187,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId() + 1; @@ -185,10 +221,7 @@ void registerTechCommentNotFoundMemberException() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); TechArticle savedTechArticle = techArticleRepository.save(techArticle); Long id = savedTechArticle.getId(); @@ -259,7 +292,7 @@ void modifyTechComment() throws Exception { 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(); @@ -301,7 +334,7 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception 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(); @@ -376,7 +409,7 @@ void modifyTechCommentAlreadyDeletedException() throws Exception { 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(); @@ -419,7 +452,7 @@ void deleteTechComment() throws Exception { 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(); @@ -457,7 +490,7 @@ void deleteTechCommentNotFoundException() throws Exception { 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 @@ -481,8 +514,7 @@ void registerRepliedTechComment() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -492,12 +524,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(); @@ -536,17 +568,16 @@ void registerRepliedTechCommentContentsIsNullException(String contents) throws E 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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(); @@ -586,8 +617,7 @@ void getTechComments() 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); + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -708,7 +738,7 @@ void recommendTechComment() throws Exception { new Count(1L), new Count(1L), new Count(1L), null, 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(); @@ -863,8 +893,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 +901,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()) 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 0473a739..ca146955 100644 --- a/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java +++ b/src/test/java/com/dreamypatisiel/devdevdev/web/docs/MyPageControllerDocsTest.java @@ -1,7 +1,56 @@ package com.dreamypatisiel.devdevdev.web.docs; -import com.dreamypatisiel.devdevdev.domain.entity.*; -import com.dreamypatisiel.devdevdev.domain.entity.embedded.*; +import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; +import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; +import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; +import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_LOGIN_STATUS; +import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_REFRESH_TOKEN; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.authenticationType; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.bookmarkSortType; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.contentStatusType; +import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.stringOrNull; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; +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.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +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.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; +import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; +import static org.springframework.restdocs.payload.JsonFieldType.OBJECT; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +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.Bookmark; +import com.dreamypatisiel.devdevdev.domain.entity.Company; +import com.dreamypatisiel.devdevdev.domain.entity.Member; +import com.dreamypatisiel.devdevdev.domain.entity.Pick; +import com.dreamypatisiel.devdevdev.domain.entity.PickOption; +import com.dreamypatisiel.devdevdev.domain.entity.PickVote; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyAnswer; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyQuestion; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyQuestionOption; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyVersion; +import com.dreamypatisiel.devdevdev.domain.entity.SurveyVersionQuestionMapper; +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.PickOptionContents; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Title; +import com.dreamypatisiel.devdevdev.domain.entity.embedded.Url; import com.dreamypatisiel.devdevdev.domain.entity.enums.ContentStatus; import com.dreamypatisiel.devdevdev.domain.entity.enums.PickOptionType; import com.dreamypatisiel.devdevdev.domain.entity.enums.Role; @@ -11,7 +60,11 @@ 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.repository.survey.*; +import com.dreamypatisiel.devdevdev.domain.repository.survey.SurveyAnswerRepository; +import com.dreamypatisiel.devdevdev.domain.repository.survey.SurveyQuestionOptionRepository; +import com.dreamypatisiel.devdevdev.domain.repository.survey.SurveyQuestionRepository; +import com.dreamypatisiel.devdevdev.domain.repository.survey.SurveyVersionQuestionMapperRepository; +import com.dreamypatisiel.devdevdev.domain.repository.survey.SurveyVersionRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkRepository; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.BookmarkSort; import com.dreamypatisiel.devdevdev.domain.repository.techArticle.SubscriptionRepository; @@ -25,6 +78,12 @@ import com.dreamypatisiel.devdevdev.web.dto.request.member.RecordMemberExitSurveyQuestionOptionsRequest; import com.dreamypatisiel.devdevdev.web.dto.response.ResultType; import jakarta.persistence.EntityManager; +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.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; @@ -42,34 +101,6 @@ import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.test.web.servlet.ResultActions; -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.concurrent.ThreadLocalRandom; - -import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.INVALID_MEMBER_NOT_FOUND_MESSAGE; -import static com.dreamypatisiel.devdevdev.domain.exception.MemberExceptionMessage.MEMBER_INCOMPLETE_SURVEY_MESSAGE; -import static com.dreamypatisiel.devdevdev.global.constant.SecurityConstant.AUTHORIZATION_HEADER; -import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_LOGIN_STATUS; -import static com.dreamypatisiel.devdevdev.global.security.jwt.model.JwtCookieConstant.DEVDEVDEV_REFRESH_TOKEN; -import static com.dreamypatisiel.devdevdev.web.docs.format.ApiDocsFormatGenerator.*; -import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; -import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; -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.*; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; -import static org.springframework.restdocs.payload.JsonFieldType.*; -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; - public class MyPageControllerDocsTest extends SupportControllerDocsTest { private static final int TEST_ARTICLES_COUNT = 20; @@ -592,7 +623,8 @@ void getMyPicksMain() throws Exception { fieldWithPath("data.sort.sorted").type(BOOLEAN).description("정렬 상태 여부"), fieldWithPath("data.sort.unsorted").type(BOOLEAN).description("비정렬 상태 여부"), fieldWithPath("data.numberOfElements").type(NUMBER).description("현재 페이지 데이터 수"), - fieldWithPath("data.empty").type(BOOLEAN).description("현재 빈 페이지 여부") + fieldWithPath("data.empty").type(BOOLEAN).description("현재 빈 페이지 여부"), + fieldWithPath("data.totalElements").type(NUMBER).description("총 데이터 갯수") ) )); } 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/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..3d192657 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,7 +55,6 @@ 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; @@ -58,6 +62,7 @@ import java.nio.charset.StandardCharsets; 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 +73,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 +92,13 @@ public class TechArticleCommentControllerDocsTest extends SupportControllerDocsT @Autowired TechCommentRecommendRepository techCommentRecommendRepository; + @Autowired + AnonymousMemberRepository anonymousMemberRepository; + @Autowired EntityManager em; + @Disabled("GuestTechCommentServiceV2는 테스트가 불가능하다. 익명 회원은 댓글 작성이 가능 하기 때문") @Test @DisplayName("익명 사용자는 기술블로그 댓글을 작성할 수 없다.") void registerTechCommentByAnonymous() throws Exception { @@ -103,8 +108,7 @@ void registerTechCommentByAnonymous() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -138,7 +142,7 @@ void registerTechCommentByAnonymous() throws Exception { } @Test - @DisplayName("회원은 기술블로그 댓글을 작성할 수 있다.") + @DisplayName("기술블로그 댓글을 작성할 수 있다.") void registerTechComment() throws Exception { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", @@ -146,10 +150,7 @@ void registerTechComment() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long id = techArticle.getId(); @@ -180,7 +181,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("기술블로그 아이디") @@ -205,10 +207,7 @@ void registerTechCommentNotFoundTechArticleException() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long id = techArticle.getId() + 1; @@ -354,12 +353,11 @@ void modifyTechComment() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +383,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("기술블로그 아이디"), @@ -418,12 +417,11 @@ void modifyTechCommentContentsIsNullException(String contents) throws Exception 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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(); @@ -474,8 +472,7 @@ void modifyTechCommentNotFoundException() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -525,12 +522,11 @@ void deleteTechComment() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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 +549,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("기술블로그 아이디"), @@ -582,12 +579,11 @@ void deleteTechCommentNotFoundException() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, 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,7 +615,7 @@ void deleteTechCommentNotFoundException() throws Exception { } @Test - @DisplayName("회원은 기술블로그 댓글에 답글을 작성할 수 있다.") + @DisplayName("기술블로그 댓글에 답글을 작성할 수 있다.") void registerTechReply() throws Exception { // given Company company = createCompany("꿈빛 파티시엘", "https://example.com/company.png", "https://example.com", @@ -627,8 +623,7 @@ void registerTechReply() throws Exception { 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); + new Count(1L), new Count(1L), new Count(1L), new Count(1L), null, company); techArticleRepository.save(techArticle); Long techArticleId = techArticle.getId(); @@ -638,12 +633,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 +665,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("기술블로그 아이디"), @@ -709,12 +705,12 @@ void registerTechReplyContentsIsNullException(String contents) throws Exception 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 +750,26 @@ 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); + new Count(1L), new Count(1L), new Count(12L), new Count(1L), null, 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 +777,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 +821,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 +847,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 +870,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) @@ -1002,7 +947,7 @@ void recommendTechComment() throws Exception { new Count(1L), new Count(1L), new Count(1L), null, 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 +969,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("기술블로그 아이디"), @@ -1060,7 +1006,7 @@ void recommendTechCommentNotFoundTechComment() throws Exception { new Count(1L), new Count(1L), new Count(1L), null, 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,6 +1055,10 @@ 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"); @@ -1121,7 +1071,7 @@ void getTechBestComments() throws Exception { 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 +1106,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 +1121,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 +1159,13 @@ 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) - .build(); - } }